Merge branch 'worktree-agent-aed4618fbd0b6aeac'

This commit is contained in:
Andreas Reinhold / reini
2026-09-14 10:38:57 +02:00
14 changed files with 663 additions and 79 deletions
+14 -4
View File
@@ -54,7 +54,13 @@
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.
rail's width; `tall-bar` picks M3's 80px navigation bar over the 64px one, and the bottom
offset every pinned thing reads follows it; `hide-bar-on-scroll` lets the bar leave the window
while the page scrolls down, and `--material-bottom-bar` goes down and comes back with it;
`hide-rail-when-collapsed` is M3's immersive configuration — from `expanded` the rail leaves
the layout when the menu button collapses it, rather than narrowing to 96px, so the page has
the whole window. The only way back is `$store.rail.show()`, so put a menu button in the app
bar at every width, not just below `medium`.
`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
@@ -82,6 +88,9 @@
'destinations' => [],
'label' => null,
'railWidth' => '16rem',
'tallBar' => false,
'hideBarOnScroll' => false,
'hideRailWhenCollapsed' => false,
])
@php
@@ -113,7 +122,8 @@
data-app-shell
@class([
'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(),
'max-medium:[--material-bottom-bar:calc(4rem+var(--material-safe-bottom,env(safe-area-inset-bottom))+var(--material-bottom-extra,0px))]' => $barItems->isNotEmpty() && ! $tallBar,
'max-medium:[--material-bottom-bar:calc(5rem+var(--material-safe-bottom,env(safe-area-inset-bottom))+var(--material-bottom-extra,0px))]' => $barItems->isNotEmpty() && $tallBar,
])
>
<a
@@ -127,7 +137,7 @@
@endisset
<div class="flex-1 medium:flex">
<x-livewire-material::navigation-rail mode="adaptive" :label="$label" :width="$railWidth">
<x-livewire-material::navigation-rail mode="adaptive" :label="$label" :width="$railWidth" :hide-when-collapsed="$hideRailWhenCollapsed">
@isset($brand)
<x-slot:brand>{{ $brand }}</x-slot:brand>
@endisset
@@ -174,7 +184,7 @@
@if ($barItems->isNotEmpty())
<div data-app-shell-bar class="fixed inset-x-0 bottom-0 z-30 medium:hidden">
<x-livewire-material::navigation-bar :label="$label">
<x-livewire-material::navigation-bar :label="$label" :tall="$tallBar" :hide-on-scroll="$hideBarOnScroll">
@foreach ($barItems as $item)
<x-livewire-material::navigation-bar-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
@@ -16,11 +16,24 @@
Centered arrangement gives three to six items. Both follow the bar's own width (a container
query), so a bar in a narrow column keeps the compact items.
`tall` is M3's other container: 80px (NavigationBarTokens.TallContainerHeight) with the
vertical item layout at every width — icon over label, never side by side — for a bar whose
labels need the room. The short bar stays the default.
`hide-on-scroll` is M3's scrolling behaviour: the bar slides out of the window on a scroll
down and springs back on a scroll up, never before the first screenful has gone by, and never
while a snackbar, a bottom sheet or a drawer is on screen those are anchored to the bar's
edge and would slide with it. Focus reaching the bar brings it back, which is as close as the
web gets to M3's "never hide it while a screen reader is active". Inside `<x-app-shell>`
`--material-bottom-bar` follows the bar down and up, so a `fab` button, the snackbar and the
page's bottom padding keep their distance from it rather than from where it was.
It does not position itself: wrap it in the element that pins it (`fixed inset-x-0 bottom-0`)
and hides it where a rail takes over. `<x-app-shell>` does both, and lifts the snackbar and a
`fab` button above it through `--material-bottom-bar`.
`label` names the landmark ("Main" by default).
`label` names the landmark ("Main" by default); `tall` picks the 80px container;
`hide-on-scroll` lets it leave the window while the page scrolls down.
Values from androidx Compose Material 3 (Apache-2.0), androidx-main
27cf9a7d5788aa0f5f2d8b6699ce279560daf326: NavigationBarTokens.kt,
@@ -31,9 +44,22 @@
@props([
'label' => null,
'tall' => false,
'hideOnScroll' => false,
])
<nav aria-label="{{ $label ?? __('Main') }}" data-navigation-bar {{ $attributes }}>
<nav
aria-label="{{ $label ?? __('Main') }}"
data-navigation-bar
@if ($tall) data-tall @endif
@if ($hideOnScroll)
data-hide-on-scroll
x-data="materialNavigationBar"
x-bind:data-hidden="away ? '' : null"
x-on:focusin="show()"
@endif
{{ $attributes }}
>
<div data-navigation-bar-items>
{{ $slot }}
</div>
@@ -52,7 +52,21 @@
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`,
between M3's 220 and 360dp) or the word `narrow`, M3's other *collapsed* width
(NavigationRailCollapsedTokens.NarrowContainerWidth, 80px against the default 96), where the
items are their icons alone because no label fits under a 56px indicator at that width; the
labels stay in the accessibility tree, since they are what name the destinations, and a
narrow rail expands to the default 16rem; `align` is `top` (the default) or `center`, which
puts the destinations at the rail's vertical centre M3 prefers that on a tablet, for reach
while the menu button, the brand and the FAB stay at the top and the footer at the foot, as M3
asks; more destinations than fit go back to the top rather than out of reach above the
scroller; `hide-when-collapsed` is M3's other expanded behaviour, for a `collapsible` or
`adaptive` rail: collapsing it takes it out of the layout altogether instead of narrowing it
to 96px, and it comes back expanded over a scrim when something calls `$store.rail.show()` —
a menu button in the app bar, which is the only way back, so put one there. Its own menu
button then docks it into the layout again. Not below `medium` for a collapsible rail, nor at
`medium` for an adaptive one: there it is the window and not the visitor that collapses a
rail, and M3's collapsed rail may never hide; `menu` shows the menu button (by default for `collapsible`,
`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,
@@ -74,6 +88,8 @@
'mode' => 'collapsible',
'label' => null,
'width' => '16rem',
'align' => 'top',
'hideWhenCollapsed' => false,
'menu' => null,
'divider' => false,
'fill' => true,
@@ -82,17 +98,26 @@
@php
$mode = in_array($mode, ['collapsed', 'expanded', 'collapsible', 'modal', 'adaptive'], true) ? $mode : 'collapsible';
$interactive = in_array($mode, ['collapsible', 'modal', 'adaptive'], true);
$canOpen = in_array($mode, ['modal', 'adaptive'], true);
// Only a rail that has a collapsed *and* an expanded state of its own can hide instead of
// narrowing; a `modal` one is already over the page, and the two fixed modes mean what they say.
$hideWhenCollapsed = $hideWhenCollapsed && in_array($mode, ['collapsible', 'adaptive'], true);
$canOpen = in_array($mode, ['modal', 'adaptive'], true) || $hideWhenCollapsed;
$menu ??= $interactive;
$collapsedAtFirst = in_array($mode, ['collapsed', 'modal'], true);
$narrow = $width === 'narrow';
$width = $narrow ? '16rem' : $width;
$centred = $align === 'center';
@endphp
<div
data-navigation-rail="{{ $mode }}"
@if ($narrow) data-width="narrow" @endif
@if ($centred) data-align="center" @endif
@if ($hideWhenCollapsed) data-hide-when-collapsed @endif
@if ($divider) data-divider @endif
@unless ($fill) data-fill="false" @endunless
@if ($interactive)
x-data="materialNavigationRail('{{ $mode }}')"
x-data="materialNavigationRail('{{ $mode }}', {{ $hideWhenCollapsed ? 'true' : 'false' }})"
x-bind:data-open="open"
@endif
{{ $attributes->merge(['style' => "--navigation-rail-width: {$width}"]) }}
@@ -112,7 +137,9 @@
@if ($menu || isset($brand) || isset($header))
<div data-navigation-rail-header>
@if ($menu || isset($brand))
<div @class(['flex w-full min-w-0 items-center gap-3 pe-5', 'ps-7' => $menu, 'ps-5' => ! $menu])>
{{-- The menu button is centred on the collapsed rail: 28px either side of a
40px button in 96, 20px in a narrow 80. --}}
<div @class(['flex w-full min-w-0 items-center gap-3 pe-5', 'ps-7' => $menu, 'rail-collapsed:ps-5' => $menu && $narrow, 'ps-5' => ! $menu])>
@if ($menu)
<button
type="button"
@@ -136,7 +163,8 @@
@endif
@isset($header)
<div {{ $header->attributes->class(['flex w-full flex-col items-start gap-2 px-5']) }}>{{ $header }}</div>
{{-- And so is a 56px FAB: 20px either side in 96, 12px in a narrow 80. --}}
<div {{ $header->attributes->class(['flex w-full flex-col items-start gap-2 px-5', 'rail-collapsed:px-3' => $narrow]) }}>{{ $header }}</div>
@endisset
</div>
@endif
+11 -2
View File
@@ -4,8 +4,12 @@
`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.
toolbar sits where it is written. A `fab` slot sets an `<x-fab>` beside a floating toolbar,
and at the end of a docked one — M3's "With FAB" configuration for both. On a docked toolbar the
controls then gather at the start and the FAB rests on the bar, flat: M3 gives a FAB nested in
another component elevation 0 (Compose's bottomAppBarFabElevation is 0 at every state), where
one beside a floating toolbar keeps its own. It is one of the toolbar's controls then, so the
arrow keys reach it. `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
@@ -29,6 +33,7 @@
$vertical = $vertical && $variant === 'floating';
$place = in_array($place, ['bottom', 'end'], true) ? $place : null;
$grouped = isset($fab) && $variant === 'floating';
$docksFab = isset($fab) && $variant === 'docked';
@endphp
@if ($grouped)
@@ -48,6 +53,10 @@
{{ $attributes }}
>
{{ $slot }}
@if ($docksFab)
<div data-toolbar-fab>{{ $fab }}</div>
@endif
</div>
@if ($grouped)