Merge branch 'worktree-agent-ab53265ffbd837742'

This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:21:01 +02:00
23 changed files with 572 additions and 195 deletions
@@ -38,10 +38,12 @@
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"
class="state-layer touch-target focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full bg-primary-container type-label-lg text-on-primary-container"
>
@if ($image)
<img src="{{ $avatar }}" alt="" class="size-full object-cover" />
{{-- Behind the state layer (`state-layer` paints its ::before at z-index -1), so an
avatar image still shows the hover and pressed states over it. --}}
<img src="{{ $avatar }}" alt="" class="relative -z-20 size-full rounded-corner-full object-cover" />
@elseif (filled($initials))
{{ $initials }}
@else
+58 -42
View File
@@ -45,13 +45,23 @@
it), `bar` (`false` keeps it out of the bottom bar; M3 wants three to five there) and
`navigate` (`false` for a full page load instead of `wire:navigate`).
Slots, each rendered once: `brand` (beside the rail's menu button while it is expanded),
`rail-header` (under it: a FAB see `<x-navigation-rail>` for its two shapes), `rail-footer`
(at the foot of the rail: footer destinations, an account), `actions` (a row of icon buttons at
the very foot, stacked when the rail is collapsed: a theme toggle, sign out), `top` (the app
bar, above the page at every width) and the page itself. The rail is one element at every
width, so what is in it is also in the modal rail a phone opens. `label` names both navigation
landmarks ("Main"); `rail-width` is the expanded rail's width.
Slots, each rendered once: `banner` (a bar across the whole window, above the rail and the
page — M3's scaffold is bars, then rails, then panes), `brand` (beside the rail's menu button
while it is expanded), `rail-header` (under it: one `<x-fab label="…" icon="…">`, which the
rail morphs between a FAB and an extended FAB as it opens), `rail-footer` (at the foot of the
rail: footer destinations, an account), `actions`
(a row of icon buttons at the very foot, stacked when the rail is collapsed: a theme toggle,
sign out), `top` (the page's own bar, above the page and *beside* the rail) and the page
itself. The rail is one element at every width, so what is in it is also in the modal rail a
phone opens. `label` names both navigation landmarks ("Main"); `rail-width` is the expanded
rail's width.
`banner` or `top` is a decision about what the bar belongs to: an application-wide bar — one
search, one account menu, the same on every page — spans the window and the rail starts under
it; a bar that titles the page belongs to the page, beside the rail. Put an app bar in one or
the other, never both. A banner that pins itself to the top of the window says how tall it is
— `style="--material-banner: 4rem"` on `<x-app-shell>` — so the rail sticks under it instead
of behind it.
The page is `<main id="content">` with `wire:transition.navigate`, behind a skip link that is
the first thing a keyboard reaches. The snackbar host (`<x-toast />`) is part of the shell;
@@ -102,7 +112,7 @@
<div
data-app-shell
@class([
'min-h-dvh bg-surface text-on-surface [--material-margin:1rem] medium:flex medium:[--material-margin:1.5rem]',
'flex min-h-dvh flex-col bg-surface text-on-surface [--material-margin:1rem] medium:[--material-margin:1.5rem]',
'max-medium:[--material-bottom-bar:calc(4rem+var(--material-safe-bottom,env(safe-area-inset-bottom))+var(--material-bottom-extra,0px))]' => $barItems->isNotEmpty(),
])
>
@@ -112,48 +122,54 @@
class="sr-only focus:not-sr-only focus:fixed focus:start-4 focus:top-[calc(var(--material-safe-top,env(safe-area-inset-top))+1rem)] focus:z-[60] focus:rounded-corner-full focus:bg-inverse-surface focus:px-4 focus:py-2 focus:type-label-lg focus:text-inverse-on-surface focus:shadow-elevation-3 focus:outline-none"
>{{ __('Skip to content') }}</a>
<x-livewire-material::navigation-rail mode="adaptive" :label="$label" :width="$railWidth">
@isset($brand)
<x-slot:brand>{{ $brand }}</x-slot:brand>
@endisset
@isset($banner)
<div data-app-shell-banner class="shrink-0">{{ $banner }}</div>
@endisset
@isset($railHeader)
<x-slot:header>{{ $railHeader }}</x-slot:header>
@endisset
<div class="flex-1 medium:flex">
<x-livewire-material::navigation-rail mode="adaptive" :label="$label" :width="$railWidth">
@isset($brand)
<x-slot:brand>{{ $brand }}</x-slot:brand>
@endisset
@foreach ($groups as $group)
@if ($group->first()['section'] !== null)
<x-livewire-material::navigation-rail-section :label="$group->first()['section']">
@isset($railHeader)
<x-slot:header>{{ $railHeader }}</x-slot:header>
@endisset
@foreach ($groups as $group)
@if ($group->first()['section'] !== null)
<x-livewire-material::navigation-rail-section :label="$group->first()['section']">
@foreach ($group as $item)
<x-livewire-material::navigation-rail-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :badge-label="$item['badgeLabel']" :no-wire-navigate="! $item['navigate']" />
@endforeach
</x-livewire-material::navigation-rail-section>
@else
@foreach ($group as $item)
<x-livewire-material::navigation-rail-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :badge-label="$item['badgeLabel']" :no-wire-navigate="! $item['navigate']" />
@endforeach
</x-livewire-material::navigation-rail-section>
@else
@foreach ($group as $item)
<x-livewire-material::navigation-rail-item :label="$item['title']" :icon="$item['icon']" :link="$item['url']" :active="$item['active']" :badge="$item['badge']" :badge-label="$item['badgeLabel']" :no-wire-navigate="! $item['navigate']" />
@endforeach
@endif
@endforeach
@if (isset($railFooter) || isset($actions))
<x-slot:footer>
{{ $railFooter ?? '' }}
@isset($actions)
<div data-app-shell-actions class="flex items-center gap-1 px-5 pt-2 rail-collapsed:flex-col">
{{ $actions }}
</div>
@endisset
</x-slot:footer>
@endif
@endforeach
</x-livewire-material::navigation-rail>
@if (isset($railFooter) || isset($actions))
<x-slot:footer>
{{ $railFooter ?? '' }}
<div class="flex min-w-0 flex-1 flex-col">
{{ $top ?? '' }}
@isset($actions)
<div data-app-shell-actions class="flex items-center gap-1 px-5 pt-2 rail-collapsed:flex-col">
{{ $actions }}
</div>
@endisset
</x-slot:footer>
@endif
</x-livewire-material::navigation-rail>
<div class="flex min-w-0 flex-1 flex-col">
{{ $top ?? '' }}
<main id="content" tabindex="-1" wire:transition.navigate class="min-w-0 flex-1 px-(--material-margin) outline-none max-expanded:overflow-x-clip max-medium:pb-(--material-bottom-bar)">
{{ $slot }}
</main>
<main id="content" tabindex="-1" wire:transition.navigate class="min-w-0 flex-1 px-(--material-margin) outline-none max-expanded:overflow-x-clip max-medium:pb-(--material-bottom-bar)">
{{ $slot }}
</main>
</div>
</div>
@if ($barItems->isNotEmpty())
@@ -5,7 +5,7 @@
<x-navigation-rail mode="collapsible">
<x-slot:brand><span class="type-title-lg">Mail</span></x-slot:brand>
<x-slot:header>
<x-fab icon="edit" tooltip-right="Compose" />
<x-fab label="Compose" icon="edit" />
</x-slot:header>
<x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" active badge="12" />
@@ -36,21 +36,30 @@
the same standard rail, expanded to begin with. A visitor who has used the menu button keeps
that choice in both standard bands.
Slots: `brand` beside the menu button, only while expanded; `header` under it — a FAB, drawn
as an extended FAB when expanded (`rail-collapsed:` below); the destinations in the default
slot, which alone scroll when the window is too short; `footer`, pinned to the foot. Header and
footer never scroll, so nothing in them is cut off by the scroller's edge.
Slots: `brand` beside the menu button, only while expanded; `header` under it — one
`<x-fab label="…" icon="…">`, which the rail morphs: the label's width springs open and shut
with the rail, so the FAB becomes an extended FAB and back rather than one being swapped for
the other, and its label names it at both widths. It also rests flat, because M3 puts a FAB
nested in another component at elevation 0, not the 3 a standalone one has. Then the
destinations in the default slot, which alone scroll when the window is too short, and
`footer`, pinned to the foot. Header and footer never scroll, so nothing in them is cut off by
the scroller's edge.
Anything inside can take both shapes with the `rail-collapsed:` variant, which applies while
the rail is drawn collapsed for whatever reason:
`<span class="rail-collapsed:hidden"><x-fab label="Compose" icon="edit" /></span>`
`<span class="hidden rail-collapsed:inline-flex"><x-fab icon="edit" tooltip-right="Compose" /></span>`.
Anything else inside can take both shapes with the `rail-collapsed:` variant, which applies
while the rail is drawn collapsed for whatever reason:
`<span class="rail-collapsed:hidden">…only while expanded…</span>`
`<span class="hidden rail-collapsed:inline-flex">…only while collapsed…</span>`.
Nothing that shows while collapsed may be wider than 96px.
Props: `label` names the landmark ("Main"); `width` is the expanded width (`16rem`, held
between M3's 220 and 360dp); `menu` shows the menu button (by default for `collapsible`,
`modal` and `adaptive`). The rail does not scroll with the page: in a flex row it sticks to
the top of the viewport, as tall as the viewport at most.
`modal` and `adaptive`); `divider` draws M3's optional vertical divider on the edge the page
is on — which is also what M3 asks for when a page scrolls underneath a fixed rail; `fill`
(`false`) drops the container colour for a transparent rail over the page's own background,
which M3 allows as long as the items keep a 3:1 contrast against what is behind them. A rail
open over a scrim keeps its fill and drops the divider whatever those say: it is a surface
over the page then. The rail does not scroll with the page: in a flex row it sticks to the top
of the viewport, as tall as the viewport at most.
Values from androidx Compose Material 3 (Apache-2.0), androidx-main
27cf9a7d5788aa0f5f2d8b6699ce279560daf326: NavigationRailCollapsedTokens.kt,
@@ -66,6 +75,8 @@
'label' => null,
'width' => '16rem',
'menu' => null,
'divider' => false,
'fill' => true,
])
@php
@@ -78,6 +89,8 @@
<div
data-navigation-rail="{{ $mode }}"
@if ($divider) data-divider @endif
@unless ($fill) data-fill="false" @endunless
@if ($interactive)
x-data="materialNavigationRail('{{ $mode }}')"
x-bind:data-open="open"
@@ -109,7 +122,7 @@
x-on:click="menu()"
x-bind:aria-label="expanded ? @js(__('Collapse navigation')) : @js(__('Expand navigation'))"
x-bind:aria-expanded="expanded.toString()"
class="state-layer focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant after:absolute after:top-1/2 after:left-1/2 after:size-12 after:-translate-x-1/2 after:-translate-y-1/2"
class="state-layer touch-target focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant"
>
<span class="contents rail-collapsed:hidden"><x-livewire-material::icon name="menu_open" class="size-6" /></span>
<span class="hidden rail-collapsed:contents"><x-livewire-material::icon name="menu" class="size-6" /></span>
@@ -13,10 +13,15 @@
The page's URL is `Livewire::originalUrl()`: while a Livewire component on the page updates, the
request is Livewire's update endpoint, and comparing with it left no section lit.
A row too long for its column wraps onto a grid rather than scrolling: below `large` (1200px)
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
Up to four sections share the row as fixed tabs. From five the row is M3's scrollable tab bar
each tab as wide as its own label, the set scrolling sideways, offset 52dp from the leading
edge so it reads as scrollable which is M3's own answer to a row that will not fit, and the
one its accessibility page blesses ("horizontal scrolling tabs meet accessibility requirements
because they need to increase in width to respond to label text without affecting the
layout"). An earlier version wrapped them onto a grid instead, which left the bar's divider
under the last row only and stranded the upper rows' indicators against nothing.
`label` names the navigation ("Sections"). Links use `wire:navigate` unless
`no-wire-navigate`. --}}
@props([
@@ -31,11 +36,8 @@
$isCurrent = fn (array $item): bool => ($item['active'] ?? false) || (filled($item['url'] ?? null) && $page === url($item['url']));
$current = collect($items)->first($isCurrent) ?? ($items[0] ?? null);
$layout = match (true) {
count($items) < 5 => 'medium:flex',
count($items) < 7 => 'medium:grid medium:grid-cols-3 large:flex',
default => 'medium:grid medium:grid-cols-4 large:flex',
};
// Four fit a row at the widths this bar is used at; from five they are M3's scrollable tabs.
$scrollable = count($items) >= 5;
@endphp
<div {{ $attributes->class(['min-w-0']) }} data-section-nav>
@@ -45,7 +47,7 @@
<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-livewire-material::icon :name="$current['icon']" class="size-5 text-on-surface-variant" />
<x-livewire-material::icon :name="$current['icon']" optical="20" class="size-5 text-on-surface-variant" />
@endisset
<span class="min-w-0 flex-1 truncate">{{ $current['title'] }}</span>
<x-livewire-material::icon name="arrow_drop_down" class="size-6 text-on-surface-variant" />
@@ -60,7 +62,7 @@
@endif
<nav aria-label="{{ $label }}" class="max-medium:hidden">
<ul data-tabs-bar data-variant="secondary" class="{{ $layout }}">
<ul data-tabs-bar data-variant="secondary" @if ($scrollable) data-scrollable @endif>
@foreach ($items as $item)
@php($on = $isCurrent($item))
@@ -73,7 +75,7 @@
>
<span data-tab-content>
@isset($item['icon'])
<x-livewire-material::icon :name="$item['icon']" :filled="$on" class="size-5" />
<x-livewire-material::icon :name="$item['icon']" :filled="$on" optical="20" class="size-5" />
@endisset
<span class="truncate">{{ $item['title'] }}</span>
@if (filled($item['badge'] ?? null))
+29 -3
View File
@@ -1,17 +1,43 @@
{{-- 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). --}}
`class` lands on the panel (it has `pt-6` above its content by default).
The id and the `hidden` panels come from the server, not from Alpine: until Alpine boots, every
panel would otherwise be on screen at once — four `role="tabpanel"` regions for a screen reader
to walk — and the tab buttons' `aria-controls` would point at nothing. `<x-tabs>` renders this
slot before its own view, so it cannot hand the panel anything; the panel reads the attributes
written on `<x-tabs>` off the component stack (Blade's `@aware` mechanism) and repeats the two
lines tabs.blade.php runs on them. The two must agree, so change them together. --}}
@props([
'name',
])
@php
$aware = fn (string $key, $default = null) => $__env->getConsumableComponentData($key, $default);
$tabs = $aware('tabs', []);
// `wire:model` is a key, not a prop, and its modifiers are part of the key: the spellings that
// make sense on a tab set. An exotic one only means the panel falls back to the first tab.
$model = collect(['wire:model', 'wire:model.live', 'wire:model.blur', 'wire:model.change', 'wire:model.lazy'])
->map(fn (string $key) => $aware($key))
->first(fn ($value): bool => filled($value));
$tabsId = $aware('id') ?? 'tabs-'.substr(md5($model.'|'.implode('|', array_column($tabs, 'name'))), 0, 8);
$initial = $aware('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-show="selected === @js($name)"
role="tabpanel"
x-bind:id="tabsId + '-' + @js($name) + '-panel'"
x-bind:aria-labelledby="tabsId + '-' + @js($name)"
id="{{ $tabsId }}-{{ $name }}-panel"
aria-labelledby="{{ $tabsId }}-{{ $name }}"
tabindex="0"
@if ((string) $name !== (string) $initial) style="display: none" @endif
{{ $attributes->class(['pt-6 outline-none']) }}
>
{{ $slot }}
+4 -1
View File
@@ -3,7 +3,8 @@
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.
on the server, and each arrives with its id and with every panel but the chosen one already
hidden, so nothing flashes and no screen reader meets four panels at once.
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
@@ -24,6 +25,8 @@
@php
$model = $attributes->wire('model')->value() ?: null;
$variant = $variant === 'secondary' ? 'secondary' : 'primary';
// tab.blade.php repeats these two lines off the component stack, because a slot renders before
// the component around it: change them together, or a panel's id stops matching aria-controls.
$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);
@@ -6,76 +6,79 @@
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.
- `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`) rather than render a guess. `class` lands on the button or the group. --}}
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 ($mode === 'contrast')
@if ($row !== null)
<div
role="radiogroup"
aria-label="{{ __('Contrast') }}"
x-data
data-theme-toggle="contrast"
{{ $attributes->class(['inline-flex h-10 rounded-corner-full border border-outline']) }}
role="group"
aria-label="{{ $label ?? $row['name'] }}"
data-theme-toggle="{{ $mode }}"
x-data="{
get chosen() {
return this.{{ $row['read'] }}
},
set chosen(value) {
this.{{ $row['write'] }}
},
}"
>
@foreach (['standard' => ['Standard', 'contrast'], 'medium' => ['Medium', 'contrast_circle'], 'high' => ['High', 'contrast_square']] as $level => [$text, $icon])
<button
type="button"
role="radio"
aria-checked="false"
x-bind:aria-checked="($store.theme.resolvedContrast === '{{ $level }}').toString()"
x-bind:tabindex="$store.theme.resolvedContrast === '{{ $level }}' ? 0 : -1"
x-on:click="$store.theme.setContrast('{{ $level }}')"
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-contrast-option="{{ $level }}"
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-livewire-material::icon name="check" class="hidden size-4.5 in-aria-checked:block" />
<x-livewire-material::icon :name="$icon" class="size-4.5 in-aria-checked:hidden" />
{{ __($text) }}
</button>
@endforeach
</div>
@elseif ($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-livewire-material::icon name="check" class="hidden size-4.5 in-aria-checked:block" />
<x-livewire-material::icon :name="$icon" class="size-4.5 in-aria-checked:hidden" />
{{ __($text) }}
</button>
@endforeach
<x-livewire-material::group
inline
x-model="chosen"
:label="$label"
:name="$row['field']"
:options="$row['options']"
{{ $attributes }}
/>
</div>
@else
<button
@@ -93,7 +96,7 @@
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']) }}
{{ $attributes->class(['state-layer touch-target 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-livewire-material::icon name="light_mode" x-cloak x-show="$store.theme.choice === 'light'" />
@@ -7,6 +7,11 @@
toolbar sits where it is written. A `fab` slot sets an `<x-fab>` beside a floating toolbar.
`label` names it for screen readers.
A docked toolbar and a navigation bar occupy the same region of the screen and M3 says never to
show both at once: the bar belongs on a primary page, the toolbar on a secondary or contextual
one. Either way a toolbar placed at `bottom` clears `--material-bottom-bar`, so it is never
buried under `<x-app-shell>`'s bar.
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). --}}
+1 -1
View File
@@ -57,7 +57,7 @@
fetched on first focus, and / or Ctrl+K (⌘K) reaches it from anywhere. Its names
keep clear of <x-search>'s own (`results`, `open`), which the slot also sees. --}}
<div
class="mx-auto w-full max-w-2xl"
class="w-full"
x-data="{
query: '',
entries: null,
@@ -34,9 +34,9 @@
'Collapsed and expanded rails' => <<<'BLADE'
<div class="flex w-full flex-wrap items-start gap-6">
<div class="flex h-[36rem] overflow-hidden rounded-corner-lg border border-outline-variant">
<x-navigation-rail mode="collapsed" label="Collapsed example">
<x-navigation-rail mode="collapsed" label="Collapsed example" divider>
<x-slot:header>
<x-fab icon="edit" tooltip-right="Compose" />
<x-fab label="Compose" icon="edit" />
</x-slot:header>
<x-navigation-rail-item label="Inbox" icon="inbox" link="#navigation" no-wire-navigate active badge="12" />
@@ -82,8 +82,7 @@
</x-slot:brand>
<x-slot:header>
<span class="rail-collapsed:hidden"><x-fab label="Compose" icon="edit" /></span>
<span class="hidden rail-collapsed:inline-flex"><x-fab icon="edit" tooltip-right="Compose" /></span>
<x-fab label="Compose" icon="edit" />
</x-slot:header>
<x-navigation-rail-item label="Inbox" icon="inbox" link="#navigation" no-wire-navigate active badge="12" />
@@ -92,7 +91,7 @@
</x-navigation-rail>
<div class="min-w-0 flex-1 bg-surface-container-low p-6 type-body-md text-on-surface-variant">
The menu button collapses and expands the rail. The choice is remembered and applied before the next page paints every collapsible rail follows it, <code>&lt;x-app-shell&gt;</code>'s from <code>lg</code> too.
The menu button collapses and expands the rail, and the FAB morphs into an extended FAB with it. The choice is remembered and applied before the next page paints every collapsible rail follows it, <code>&lt;x-app-shell&gt;</code>'s from <code>expanded</code> (840px) too.
</div>
</div>
BLADE,
@@ -104,7 +103,7 @@
<p class="max-w-3xl type-body-md text-on-surface-variant">
<code>&lt;x-navigation-bar&gt;</code> and <code>&lt;x-navigation-bar-item&gt;</code>, <code>&lt;x-navigation-rail&gt;</code>, <code>&lt;x-navigation-rail-item&gt;</code> and <code>&lt;x-navigation-rail-section&gt;</code>,
and <code>&lt;x-app-shell&gt;</code>, which puts them together: a bar below <code>sm</code>, a collapsed rail that opens as a modal to <code>lg</code>, a collapsible rail from there.
and <code>&lt;x-app-shell&gt;</code>, which puts them together on M3's window size classes: a bar below <code>medium</code> (600px), a collapsed rail that opens as a modal through <code>medium</code>, a standard rail from <code>expanded</code> (840px) and an expanded one from <code>large</code> (1200px).
<a href="{{ route('livewire-material.shell') }}" class="link text-primary">Open the app shell</a> and change the window's width.
</p>
+1 -2
View File
@@ -45,8 +45,7 @@
</x-slot:brand>
<x-slot:rail-header>
<span class="rail-collapsed:hidden"><x-livewire-material::fab label="Compose" icon="edit" x-on:click="materialToast('Compose opens here')" /></span>
<span class="hidden rail-collapsed:inline-flex"><x-livewire-material::fab icon="edit" tooltip-right="Compose" x-on:click="materialToast('Compose opens here')" /></span>
<x-livewire-material::fab label="Compose" icon="edit" x-on:click="materialToast('Compose opens here')" />
</x-slot:rail-header>
<x-slot:rail-footer>