Files
livewire-material/resources/views/components/navigation-rail.blade.php
T
Andreas Reinhold / reiniandClaude Fable 5.1 cef350d739 Add the rail's narrow collapsed width
Plan step 25 (navigation Missing): NavigationRailCollapsedTokens
.NarrowContainerWidth, 80dp, had no way in — only the default 96.
`<x-navigation-rail width="narrow">` takes it; the collapsed width is now a
variable, so the four rules that each have their own reason to name it follow
without a specificity fight. No label fits under a 56px indicator at 80px, so a
narrow rail's items are their icons alone, with the label taken out of the
drawing and left in the accessibility tree, where it names the destination.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 06:39:31 +02:00

163 lines
8.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{-- 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 label="Compose" icon="edit" />
</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` (600839) collapsed in the layout, opening as a modal; at `expanded` (8401199)
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 — one
`<x-fab label="…" icon="…">`, which the rail morphs: the label's width springs open and shut
with the rail, so the FAB becomes an extended FAB and back rather than one being swapped for
the other, and its label names it at both widths. It also rests flat, because M3 puts a FAB
nested in another component at elevation 0, not the 3 a standalone one has. Then the
destinations in the default slot, which alone scroll when the window is too short, and
`footer`, pinned to the foot. Header and footer never scroll, so nothing in them is cut off by
the scroller's edge.
Anything else 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">…only while expanded…</span>`
`<span class="hidden rail-collapsed:inline-flex">…only while collapsed…</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) or the word `narrow`, M3's other *collapsed* width
(NavigationRailCollapsedTokens.NarrowContainerWidth, 80px against the default 96), where the
items are their icons alone because no label fits under a 56px indicator at that width; the
labels stay in the accessibility tree, since they are what name the destinations, and a
narrow rail expands to the default 16rem; `menu` shows the menu button (by default for `collapsible`,
`modal` and `adaptive`); `divider` draws M3's optional vertical divider on the edge the page
is on which is also what M3 asks for when a page scrolls underneath a fixed rail; `fill`
(`false`) drops the container colour for a transparent rail over the page's own background,
which M3 allows as long as the items keep a 3:1 contrast against what is behind them. A rail
open over a scrim keeps its fill and drops the divider whatever those say: it is a surface
over the page then. 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,
'divider' => false,
'fill' => true,
])
@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);
$narrow = $width === 'narrow';
$width = $narrow ? '16rem' : $width;
@endphp
<div
data-navigation-rail="{{ $mode }}"
@if ($narrow) data-width="narrow" @endif
@if ($divider) data-divider @endif
@unless ($fill) data-fill="false" @endunless
@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))
{{-- The menu button is centred on the collapsed rail: 28px either side of a
40px button in 96, 20px in a narrow 80. --}}
<div @class(['flex w-full min-w-0 items-center gap-3 pe-5', 'ps-7' => $menu, 'rail-collapsed:ps-5' => $menu && $narrow, '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 touch-target focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant"
>
<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)
{{-- And so is a 56px FAB: 20px either side in 96, 12px in a narrow 80. --}}
<div {{ $header->attributes->class(['flex w-full flex-col items-start gap-2 px-5', 'rail-collapsed:px-3' => $narrow]) }}>{{ $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>