Plan step 17, on N-06, N-07 and C-07. The shell now changes at 600, 840 and 1200 and nowhere else: a compact window keeps the navigation bar and the modal rail; `medium` (600-839) gets the collapsed rail in the layout and no bar; `expanded` (840-1199) gets a standard rail, collapsed, whose menu button expands it in place rather than over a scrim; `large` and above start it expanded, which is what M3 prefers once there is room. `data-rail` alone could not say "collapsed at expanded, expanded at large", since it carries `rail.default` for a visitor who never chose. <x-theme-script> now also writes `data-rail-auto` while nothing is stored, the `rail-collapsed:` variant reads it in the 840-1199 band, and `$store.rail.auto` mirrors it for Alpine; the first press of the menu button drops it, so a remembered choice still wins in both bands. `rail.default` and the rest of `$store.rail` are unchanged, and the attribute rides through `wire:navigate` with the others. `--material-margin` carries M3's window margin on the shell -- 16px compact, 24px from `medium` -- and the content region is padded with it, so the showcase pages drop their own gutters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
140 lines
7.3 KiB
PHP
140 lines
7.3 KiB
PHP
{{-- M3 Expressive's navigation rail: destinations down the start edge of a `medium` or wider
|
||
window, collapsed (96px, icon over label) or expanded (icon beside label in a full-width pill).
|
||
|
||
<div class="flex min-h-dvh">
|
||
<x-navigation-rail mode="collapsible">
|
||
<x-slot:brand><span class="type-title-lg">Mail</span></x-slot:brand>
|
||
<x-slot:header>
|
||
<x-fab icon="edit" tooltip-right="Compose" />
|
||
</x-slot:header>
|
||
|
||
<x-navigation-rail-item label="Inbox" icon="inbox" link="/inbox" active badge="12" />
|
||
<x-navigation-rail-section label="Labels">
|
||
<x-navigation-rail-item label="Travel" icon="label" link="/labels/travel" />
|
||
</x-navigation-rail-section>
|
||
|
||
<x-slot:footer>
|
||
<x-navigation-rail-item label="Settings" icon="settings" link="/settings" />
|
||
</x-slot:footer>
|
||
</x-navigation-rail>
|
||
|
||
<main class="min-w-0 flex-1">…</main>
|
||
</div>
|
||
|
||
`mode` says what decides its width:
|
||
- `collapsed` — always collapsed; `expanded` — always expanded.
|
||
- `collapsible` (the default) — the visitor's choice: expanded until the menu button collapses
|
||
it. The choice is `$store.rail`, remembered in localStorage and applied by <x-theme-script>
|
||
before the first paint (<html data-rail>), so the rail never paints wide and snaps shut.
|
||
- `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
|
||
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)
|
||
the same standard rail, expanded to begin with. A visitor who has used the menu button keeps
|
||
that choice in both standard bands.
|
||
|
||
Slots: `brand` beside the menu button, only while expanded; `header` under it — a FAB, drawn
|
||
as an extended FAB when expanded (`rail-collapsed:` below); the destinations in the default
|
||
slot, which alone scroll when the window is too short; `footer`, pinned to the foot. Header and
|
||
footer never scroll, so nothing in them is cut off by the scroller's edge.
|
||
|
||
Anything inside can take both shapes with the `rail-collapsed:` variant, which applies while
|
||
the rail is drawn collapsed for whatever reason:
|
||
`<span class="rail-collapsed:hidden"><x-fab label="Compose" icon="edit" /></span>`
|
||
`<span class="hidden rail-collapsed:inline-flex"><x-fab icon="edit" tooltip-right="Compose" /></span>`.
|
||
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`,
|
||
`modal` and `adaptive`). The rail does not scroll with the page: in a flex row it sticks to
|
||
the top of the viewport, as tall as the viewport at most.
|
||
|
||
Values from androidx Compose Material 3 (Apache-2.0), androidx-main
|
||
27cf9a7d5788aa0f5f2d8b6699ce279560daf326: NavigationRailCollapsedTokens.kt,
|
||
NavigationRailExpandedTokens.kt, NavigationRailBaselineItemTokens.kt and
|
||
WideNavigationRail.kt under
|
||
https://github.com/androidx/androidx/tree/androidx-main/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3
|
||
— surface (surface-container and elevation 2 with a large inner corner when modal), 44px above
|
||
the header and 40px under it, 4px between collapsed items. The styles are
|
||
resources/css/components/navigation.css; the behaviour resources/js/navigation.js. --}}
|
||
|
||
@props([
|
||
'mode' => 'collapsible',
|
||
'label' => null,
|
||
'width' => '16rem',
|
||
'menu' => null,
|
||
])
|
||
|
||
@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);
|
||
$menu ??= $interactive;
|
||
$collapsedAtFirst = in_array($mode, ['collapsed', 'modal'], true);
|
||
@endphp
|
||
|
||
<div
|
||
data-navigation-rail="{{ $mode }}"
|
||
@if ($interactive)
|
||
x-data="materialNavigationRail('{{ $mode }}')"
|
||
x-bind:data-open="open"
|
||
@endif
|
||
{{ $attributes->merge(['style' => "--navigation-rail-width: {$width}"]) }}
|
||
>
|
||
@if ($canOpen)
|
||
<div data-navigation-rail-scrim aria-hidden="true" x-on:click="$store.rail.hide()"></div>
|
||
@endif
|
||
|
||
<nav
|
||
data-navigation-rail-panel
|
||
aria-label="{{ $label ?? __('Main') }}"
|
||
@if ($canOpen)
|
||
x-trap.inert.noscroll="open"
|
||
x-on:keydown.escape.window="open && $store.rail.hide()"
|
||
@endif
|
||
>
|
||
@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])>
|
||
@if ($menu)
|
||
<button
|
||
type="button"
|
||
data-navigation-rail-menu
|
||
aria-label="{{ $collapsedAtFirst ? __('Expand navigation') : __('Collapse navigation') }}"
|
||
aria-expanded="{{ $collapsedAtFirst ? 'false' : 'true' }}"
|
||
x-on:click="menu()"
|
||
x-bind:aria-label="expanded ? @js(__('Collapse navigation')) : @js(__('Expand navigation'))"
|
||
x-bind:aria-expanded="expanded.toString()"
|
||
class="state-layer focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant after:absolute after:top-1/2 after:left-1/2 after:size-12 after:-translate-x-1/2 after:-translate-y-1/2"
|
||
>
|
||
<span class="contents rail-collapsed:hidden"><x-livewire-material::icon name="menu_open" class="size-6" /></span>
|
||
<span class="hidden rail-collapsed:contents"><x-livewire-material::icon name="menu" class="size-6" /></span>
|
||
</button>
|
||
@endif
|
||
|
||
@isset($brand)
|
||
<div class="min-w-0 flex-1 rail-collapsed:hidden">{{ $brand }}</div>
|
||
@endisset
|
||
</div>
|
||
@endif
|
||
|
||
@isset($header)
|
||
<div {{ $header->attributes->class(['flex w-full flex-col items-start gap-2 px-5']) }}>{{ $header }}</div>
|
||
@endisset
|
||
</div>
|
||
@endif
|
||
|
||
<div data-navigation-rail-destinations>
|
||
{{ $slot }}
|
||
</div>
|
||
|
||
@isset($footer)
|
||
<div data-navigation-rail-footer {{ $footer->attributes }}>{{ $footer }}</div>
|
||
@endisset
|
||
</nav>
|
||
</div>
|