Offer the app shell a window-wide banner above the rail
M3's scaffold is bars, then rails, then panes, and the shell had no slot that rendered above the rail: an application-wide bar could only be a pane bar beside it. `banner` is that slot — the shell is a column now, with the rail and the page as one row inside it — and the rail sticks under a pinned banner through --material-banner rather than behind it. `top` still means the page's own bar. Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-14). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
c3bed743aa
commit
5205aa8842
@@ -712,7 +712,8 @@ A visitor who has pressed the menu button keeps that choice in both standard ban
|
||||
```
|
||||
|
||||
- `destinations`: `title`, `icon`, `url`; optional `active` (default: the URL is the page's, also during a Livewire update request), `badge` (`true` for a dot, or a count), `badgeLabel` (what a screen reader hears for the badge: "3 unread"), `section` (a heading in the rail, shown only while it is expanded; consecutive destinations with the same section are grouped), `bar` (default `true`; `false` keeps it out of the bottom bar — M3 wants three to five there), `navigate` (`false` for a full page load instead of `wire:navigate`).
|
||||
- Slots, each rendered once: `brand` (beside the rail's menu button, expanded only), `rail-header` (a FAB), `rail-footer` (pinned to the foot of the rail), `actions` (a row of icon buttons at the very foot, stacked when collapsed), `top` (the app bar, above the page at every width), and the page. `label` names the landmarks ("Main"); `rail-width` is the expanded width (`16rem`).
|
||||
- Slots, each rendered once: `banner` (a bar across the whole window, above the rail and the page), `brand` (beside the rail's menu button, expanded only), `rail-header` (a FAB), `rail-footer` (pinned to the foot of the rail), `actions` (a row of icon buttons at the very foot, stacked when collapsed), `top` (the page's own bar, above the page and beside the rail), and the page. `label` names the landmarks ("Main"); `rail-width` is the expanded width (`16rem`).
|
||||
- `banner` or `top`: M3's scaffold is bars, then rails, then panes. An application-wide bar — one search, one account menu, the same on every page — goes in `banner` and the rail starts under it; a bar that titles the page goes in `top`, beside the rail. 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 rail is one element at every width: what is in it is also what a phone sees in the modal rail. On a compact window nothing opens it but `$store.rail.show()`, so a page whose destinations are not all in the bar needs a menu button in its app bar (hidden from `medium`).
|
||||
- `--material-margin` is M3's window margin (16px compact, 24px from `medium`) and the content region already carries it, so a page inside the shell writes no gutters of its own; something that must reach the window's edges opts out with `-mx-(--material-margin)`.
|
||||
- Two panes side by side are M3's from `expanded`: `<x-drawer pane>` is the second one, in an `expanded:flex expanded:items-start expanded:gap-6` row inside the page.
|
||||
|
||||
@@ -217,14 +217,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* `--material-banner` is the height of a bar pinned across the top of the window — what
|
||||
<x-app-shell>'s `banner` slot holds, if the application made it sticky. The rail sticks
|
||||
under it rather than behind it; 0 without one. */
|
||||
[data-navigation-rail-panel] {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
top: var(--material-banner, 0px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100dvh;
|
||||
max-height: calc(100dvh - var(--material-banner, 0px));
|
||||
/* Clip, not hide: no scroll container, so the destinations below can still scroll and
|
||||
nothing sticky breaks. What only an expanded rail draws — a label, the brand — is drawn
|
||||
at once when the rail expands, while the width is still growing; the clip keeps it
|
||||
|
||||
@@ -45,13 +45,22 @@
|
||||
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: 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 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 +111,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 +121,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())
|
||||
|
||||
Reference in New Issue
Block a user