Rename the app shell to the scaffold and give it a FAB slot
Plan step 35: <x-app-shell> becomes <x-scaffold>, M3's and Compose's name for the structure of bars, rails and panes, with no alias; every package view, the showcase, the tests, the skills, README and UPGRADE follow. Its markup and Tailwind classes stay for the navigation rewrite. The new fab slot places the FAB as Compose's Scaffold does - bottom-end, 16px from the edges below medium and 24px from it, clear of the navigation bar and of a snackbar on screen (resources/css/layout/scaffold.css) - and the content region tells a pane inside that the margin is already drawn. 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
acd6ba21ae
commit
0013e0154d
@@ -24,12 +24,12 @@
|
||||
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>`
|
||||
web gets to M3's "never hide it while a screen reader is active". Inside `<x-scaffold>`
|
||||
`--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
|
||||
and hides it where a rail takes over. `<x-scaffold>` does both, and lifts the snackbar and a
|
||||
`fab` button above it through `--material-bottom-bar`.
|
||||
|
||||
`label` names the landmark ("Main" by default); `tall` picks the 80px container;
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
- `modal` — collapsed in the layout; the menu button (or `$store.rail.show()` from anywhere)
|
||||
opens it expanded over a scrim, holding focus until Escape, the scrim, the menu button or
|
||||
leaving the page closes it (Compose's ModalWideNavigationRail).
|
||||
- `adaptive` — what `<x-app-shell>` uses, one rail per M3 window size class: on a compact
|
||||
- `adaptive` — what `<x-scaffold>` uses, one rail per M3 window size class: on a compact
|
||||
window (below `medium`, 600px) nothing until `$store.rail.show()` slides it in as a modal;
|
||||
at `medium` (600–839) collapsed in the layout, opening as a modal; at `expanded` (840–1199)
|
||||
a standard rail, collapsed until its menu button expands it in place; from `large` (1200)
|
||||
|
||||
+33
-12
@@ -1,17 +1,20 @@
|
||||
{{-- The adaptive app shell: navigation that changes shape with the window, around the page.
|
||||
{{-- The scaffold: M3's structure of bars, rails and panes around the page, and the FAB, with
|
||||
navigation that changes shape with the window. M3 and Compose call it the scaffold
|
||||
(docs/reference/m3/foundations.md § Layout → Scaffold); before 2.0.0 it was `<x-app-shell>`.
|
||||
|
||||
<x-app-shell :destinations="[
|
||||
<x-scaffold :destinations="[
|
||||
['title' => 'Shares', 'icon' => 'folder_shared', 'url' => route('shares'), 'active' => request()->routeIs('shares*'), 'badge' => 3],
|
||||
['title' => 'Upload', 'icon' => 'upload', 'url' => route('upload')],
|
||||
['title' => 'Users', 'icon' => 'group', 'url' => route('users'), 'section' => 'Admin', 'bar' => false],
|
||||
]">
|
||||
<x-slot:brand><a href="/" wire:navigate class="type-title-lg">SealShare</a></x-slot:brand>
|
||||
<x-slot:top>…the page's app bar…</x-slot:top>
|
||||
<x-slot:fab><x-fab icon="add" tooltip="New share" /></x-slot:fab>
|
||||
|
||||
…the page…
|
||||
</x-app-shell>
|
||||
</x-scaffold>
|
||||
|
||||
The navigation is M3's per window size class (docs/reference/m3/foundations.md § Layout and
|
||||
The navigation is M3's per breakpoint (docs/reference/m3/foundations.md § Layout and
|
||||
foundations-supplement.md § Breakpoints), and only those four numbers:
|
||||
|
||||
- **Compact**, below `medium` (600px): a navigation bar with the destinations marked `bar`,
|
||||
@@ -28,14 +31,15 @@
|
||||
From `expanded` the choice the menu button makes is remembered and applied before the first
|
||||
paint (`$store.rail`, <x-theme-script>), so the rail never paints one width and snaps to the
|
||||
other. A visitor who has chosen keeps that choice in both bands; one who never has gets the
|
||||
class's own default.
|
||||
breakpoint's own default.
|
||||
|
||||
`--material-margin` is M3's window margin — 16px on a compact window, 24px from `medium` — and
|
||||
the content region is padded with it, so a page inside the shell writes no gutters of its own.
|
||||
the content region is padded with it, so a page inside the scaffold writes no gutters of its own.
|
||||
Something meant to 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, 360dp
|
||||
wide, in an `expanded:flex expanded:items-start expanded:gap-6` row inside the page.
|
||||
Panes are `<x-pane>`, and two side by side are M3's canonical layouts from `expanded`,
|
||||
`<x-list-detail>` and `<x-supporting-pane>`; inside the content region they draw no margin of
|
||||
their own (resources/css/layout/scaffold.css sets `--md-layout-margin` there).
|
||||
|
||||
`destinations` is a list of arrays: `title`, `icon` (a Material Symbol), `url`, and optionally
|
||||
`active` (by default: the URL is the page's; during a Livewire update request, the page the
|
||||
@@ -51,8 +55,8 @@
|
||||
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
|
||||
sign out), `top` (the page's own bar, above the page and *beside* the rail), `fab` (the
|
||||
page's floating action button, see below) 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; `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
|
||||
@@ -66,16 +70,28 @@
|
||||
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
|
||||
— `style="--material-banner: 4rem"` on `<x-scaffold>` — 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;
|
||||
the first thing a keyboard reaches. The snackbar host (`<x-toast />`) is part of the scaffold;
|
||||
on a compact window it, and a `fab` button, sit above the bottom bar through `--material-bottom-bar`:
|
||||
the bar's 64px, the bottom safe area (`--material-safe-bottom`, else the device's inset) and
|
||||
`--material-bottom-extra` (0px unless the application docks something, an offline banner, on
|
||||
top of the bar).
|
||||
|
||||
`fab` places an `<x-fab>` as Compose's Scaffold places its floating action button: fixed at the
|
||||
bottom-end corner, 16px from the window's edges on a compact window and 24px from `medium`
|
||||
(M3's margins, foundations-supplement.md § Breakpoints; the FAB's own adaptive placement is
|
||||
the lower trailing corner, components-actions-communication-containment.md § FAB), above the
|
||||
navigation bar and the bottom safe area, and lifted over a snackbar while one shows —
|
||||
M3: a snackbar appears above a FAB, never in front of or behind one. Compose raises the
|
||||
snackbar over the FAB instead; here the snackbar host is one fixed element every page shares,
|
||||
so the FAB is what moves (resources/css/layout/scaffold.css). Use it or `rail-header`'s FAB,
|
||||
not both: M3 says not to show more than one FAB on a screen. It comes after the page's bar and
|
||||
before the page in focus order, where M3 puts a FAB (§ FAB → Accessibility: "the FAB should be
|
||||
prioritized in the page's focus order").
|
||||
|
||||
`max-expanded:overflow-x-clip` on the content region is the backstop under every page, and it stays
|
||||
`clip`: `overflow-x: hidden` would force `overflow-y` to `auto`, turn the region into a scroll
|
||||
container and break every `position: sticky` inside it (an app bar, a list-detail pane). Below
|
||||
@@ -120,6 +136,7 @@
|
||||
|
||||
<div
|
||||
data-app-shell
|
||||
data-md-scaffold
|
||||
@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() && ! $tallBar,
|
||||
@@ -176,6 +193,10 @@
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
{{ $top ?? '' }}
|
||||
|
||||
@isset($fab)
|
||||
<div data-md-scaffold-fab>{{ $fab }}</div>
|
||||
@endisset
|
||||
|
||||
<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>
|
||||
@@ -31,7 +31,7 @@
|
||||
rail's width is CSS keyed on it (the `rail-collapsed:` variant). Set any later, a collapsed
|
||||
rail would paint wide and snap shut on every load. `$store.rail` (resources/js/navigation.js)
|
||||
changes it. <html data-rail-auto> rides with it and says nothing was stored — the value is only
|
||||
`rail.default`, not a choice — so `<x-app-shell>`'s adaptive rail can start collapsed in the
|
||||
`rail.default`, not a choice — so `<x-scaffold>`'s adaptive rail can start collapsed in the
|
||||
expanded class (840–1199) and expanded from large, as M3 asks, while still obeying a visitor
|
||||
who has chosen. `$store.rail` drops it the first time they do.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
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.
|
||||
buried under `<x-scaffold>`'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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{-- The showcase's frame, and the package's own app shell at work: the rail groups every section,
|
||||
{{-- The showcase's frame, and the package's own scaffold at work: the rail groups every section,
|
||||
collapses and expands from `lg`, and opens as a modal from the app bar's menu button on a phone.
|
||||
Pages move with wire:navigate, so the rail keeps its place and the theme stays. --}}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="bg-surface font-sans text-on-surface antialiased">
|
||||
<x-livewire-material::app-shell :destinations="$destinations" label="Showcase" rail-width="17rem">
|
||||
<x-livewire-material::scaffold :destinations="$destinations" label="Showcase" rail-width="17rem">
|
||||
<x-slot:brand>
|
||||
<a href="{{ route('livewire-material.showcase') }}" wire:navigate class="block truncate rounded-corner-xs type-title-lg focus-ring">Livewire Material</a>
|
||||
</x-slot:brand>
|
||||
@@ -152,7 +152,7 @@
|
||||
</x-slot:top>
|
||||
|
||||
@yield('content')
|
||||
</x-livewire-material::app-shell>
|
||||
</x-livewire-material::scaffold>
|
||||
|
||||
@livewireScripts
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
</div>
|
||||
|
||||
<p class="max-w-3xl type-body-md text-on-surface-variant">
|
||||
<code><x-app-shell></code> does all of this: resize this showcase and watch the bar, the rail and the
|
||||
<code><x-scaffold></code> does all of this: resize this showcase and watch the bar, the rail and the
|
||||
content margin (<code>--material-margin</code>) change at 600, 840 and 1200px. A visitor who has chosen a rail
|
||||
width keeps it; until then the rail follows the class.
|
||||
</p>
|
||||
@@ -113,6 +113,7 @@
|
||||
</p>
|
||||
|
||||
<x-livewire-material::stack as="ul" gap="space100" class="md-type-body-md">
|
||||
<li><code><x-scaffold></code> — the bars, the rail and the FAB around the page (this showcase is one).</li>
|
||||
<li><code><x-pane></code> — a content region with M3's margins and its own app bar.</li>
|
||||
<li><code><x-list-detail></code> — a list and the detail of its selection: one pane below 840px, two from it.</li>
|
||||
<li><code><x-supporting-pane></code> — a focus pane and a pane beside it from 840px, below it or a bottom sheet before.</li>
|
||||
|
||||
@@ -134,7 +134,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, 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><x-app-shell></code>'s from <code>expanded</code> (840px) 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><x-scaffold></code>'s from <code>expanded</code> (840px) too.
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
@@ -164,8 +164,8 @@
|
||||
|
||||
<p class="max-w-3xl type-body-md text-on-surface-variant">
|
||||
<code><x-navigation-bar></code> and <code><x-navigation-bar-item></code>, <code><x-navigation-rail></code>, <code><x-navigation-rail-item></code> and <code><x-navigation-rail-section></code>,
|
||||
and <code><x-app-shell></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.
|
||||
and <code><x-scaffold></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 scaffold</a> and change the window's width.
|
||||
</p>
|
||||
|
||||
@foreach ($examples as $title => $code)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{-- The app shell, as a whole page: /material/shell and its sibling pages, so the rail, the bar, the
|
||||
{{-- The scaffold, as a whole page: /material/shell and its sibling pages, so the rail, the bar, the
|
||||
modal rail and wire:navigate between pages can be tried at any window width. --}}
|
||||
|
||||
@php
|
||||
@@ -31,7 +31,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="robots" content="noindex" />
|
||||
|
||||
<title>{{ $current['title'] }} · App shell · Livewire Material</title>
|
||||
<title>{{ $current['title'] }} · Scaffold · Livewire Material</title>
|
||||
|
||||
<x-livewire-material::theme-script />
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="bg-surface font-sans text-on-surface antialiased">
|
||||
<x-livewire-material::app-shell :destinations="$destinations">
|
||||
<x-livewire-material::scaffold :destinations="$destinations">
|
||||
<x-slot:brand>
|
||||
<a href="{{ route('livewire-material.showcase') }}" class="block truncate rounded-corner-xs type-title-lg focus-ring">Livewire Material</a>
|
||||
</x-slot:brand>
|
||||
@@ -79,7 +79,7 @@
|
||||
<x-livewire-material::button label="Back to the showcase" link="{{ route('livewire-material.section', 'navigation') }}" no-wire-navigate />
|
||||
</div>
|
||||
</div>
|
||||
</x-livewire-material::app-shell>
|
||||
</x-livewire-material::scaffold>
|
||||
|
||||
@livewireScripts
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user