{{-- The adaptive app shell: navigation that changes shape with the window, around the page. SealShare …the page's app bar… …the page… The navigation is M3's per window size class (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`, pinned to the bottom. Everything else is in the modal rail, which slides in when something calls `$store.rail.show()` — put a menu button in the app bar for it, hidden from `medium`: ``. - **Medium** (600–839): the collapsed rail in the layout, 96px, and no bar; its menu button opens it expanded over a scrim, since 256px beside the page would leave the page too little. - **Expanded** (840–1199): a standard rail — in the layout, nothing covered — collapsed until the menu button expands it in place. - **Large and extra-large** (from 1200): the same standard rail, expanded to begin with (`rail.default`), which is what M3 prefers once there is room. From `expanded` the choice the menu button makes is remembered and applied before the first paint (`$store.rail`, ), 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. `--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. Something meant to reach the window's edges opts out with `-mx-(--material-margin)`. Two panes side by side are M3's from `expanded`: `` is the second one, 360dp wide, in an `expanded:flex expanded:items-start expanded:gap-6` row inside the page. `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 component was rendered on rather than the update endpoint), `badge` (`true` for a dot, or a count), `badgeLabel` (what a screen reader hears for the badge instead: "3 unread"), `section` (a heading the destination is grouped under in the rail; only an expanded rail shows 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 `` 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. The page is `
` with `wire:transition.navigate`, behind a skip link that is the first thing a keyboard reaches. The snackbar host (``) is part of the shell; 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). `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 `expanded` only, so a wide window never clips what overhangs on purpose. Nothing application-specific belongs in here: an app's destinations and chrome come in through the props and slots. --}} @props([ 'destinations' => [], 'label' => null, 'railWidth' => '16rem', ]) @php $label ??= __('Main'); $current = \Livewire\Livewire::isLivewireRequest() ? \Livewire\Livewire::originalUrl() : request()->url(); $items = collect($destinations) ->filter(fn ($item): bool => is_array($item) && filled($item['title'] ?? null)) ->map(fn (array $item): array => [ 'title' => (string) $item['title'], 'icon' => $item['icon'] ?? null, 'url' => $item['url'] ?? null, 'active' => (bool) ($item['active'] ?? (filled($item['url'] ?? null) && rtrim(url($item['url']), '/') === rtrim($current, '/'))), 'badge' => $item['badge'] ?? null, 'badgeLabel' => filled($item['badgeLabel'] ?? null) ? (string) $item['badgeLabel'] : null, 'section' => filled($item['section'] ?? null) ? (string) $item['section'] : null, 'bar' => ($item['bar'] ?? true) !== false, 'navigate' => ($item['navigate'] ?? true) !== false, ]) ->values(); $barItems = $items->where('bar', true)->values(); // Consecutive destinations under the same heading form one group, in the order given. $groups = $items->chunkWhile(fn (array $item, int $key, $chunk): bool => $item['section'] === $chunk->last()['section']); @endphp
$barItems->isNotEmpty(), ]) > {{ __('Skip to content') }} @isset($brand) {{ $brand }} @endisset @isset($railHeader) {{ $railHeader }} @endisset @foreach ($groups as $group) @if ($group->first()['section'] !== null) @foreach ($group as $item) @endforeach @else @foreach ($group as $item) @endforeach @endif @endforeach @if (isset($railFooter) || isset($actions)) {{ $railFooter ?? '' }} @isset($actions)
{{ $actions }}
@endisset
@endif
{{ $top ?? '' }}
{{ $slot }}
@if ($barItems->isNotEmpty())
@foreach ($barItems as $item) @endforeach
@endif