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:
Andreas Reinhold / reini
2026-09-14 06:11:49 +02:00
co-authored by Claude Opus 5
parent c3bed743aa
commit 5205aa8842
4 changed files with 81 additions and 46 deletions
+17 -1
View File
@@ -109,6 +109,7 @@ it('reads the safe area and anything docked on the bar through variables an appl
it('places each slot once', function () {
$html = (string) $this->blade(<<<'BLADE'
<x-app-shell :destinations="$destinations">
<x-slot:banner><header>WINDOW BAR</header></x-slot:banner>
<x-slot:brand><span>BRAND</span></x-slot:brand>
<x-slot:rail-header><span>FAB</span></x-slot:rail-header>
<x-slot:rail-footer><span>FOOTER</span></x-slot:rail-footer>
@@ -118,13 +119,28 @@ it('places each slot once', function () {
</x-app-shell>
BLADE, ['destinations' => shellDestinations()]);
foreach (['BRAND', 'FAB', 'FOOTER', 'ACTIONS', 'APP BAR', 'PAGE'] as $slot) {
foreach (['WINDOW BAR', 'BRAND', 'FAB', 'FOOTER', 'ACTIONS', 'APP BAR', 'PAGE'] as $slot) {
expect(substr_count($html, $slot))->toBe(1);
}
expect($html)->toMatch('/data-navigation-rail-header.*BRAND.*FAB.*data-navigation-rail-footer.*FOOTER.*data-app-shell-actions.*ACTIONS.*APP BAR.*<main.*PAGE/s');
});
it('spans the window with a banner, above the rail, and renders none without the slot', function () {
$html = (string) $this->blade(<<<'BLADE'
<x-app-shell :destinations="$destinations">
<x-slot:banner><header>WINDOW BAR</header></x-slot:banner>
PAGE
</x-app-shell>
BLADE, ['destinations' => shellDestinations()]);
// Bars, then rails, then panes: the banner is outside the row the rail and the page share.
expect($html)
->toMatch('/data-app-shell-banner.*WINDOW BAR.*data-navigation-rail=.*<main/s')
->and((string) $this->blade('<x-app-shell :destinations="$destinations" />', ['destinations' => shellDestinations()]))
->not->toContain('data-app-shell-banner');
});
it('serves the showcase\'s app shell pages', function () {
$html = $this->withoutVite()
->get('/material/shell/starred')