Plan step 36 (navigation group, second batch): <x-navigation-rail>'s, <x-navigation-rail-item>'s and <x-navigation-rail-section>'s class lists move into navigation-rail.css, navigation-rail-item.css and navigation-rail-section.css, keyed on data-md-navigation-rail (the mode, data-md-width, data-md-align, data-md-hide-when-collapsed, data-md-divider, data-md-fill, data-md-open), data-md-navigation-rail- item (data-md-active) and data-md-navigation-rail-section. Every Tailwind wrapper class in the view — the menu row's centring padding, the FAB row, the two swapped menu glyphs, the brand's visibility — becomes a hook the stylesheet draws instead; the menu button itself renders the shared md-state-layer/md-focus-ring/md-touch-target classes (N-01's pattern) since it draws its own layer on itself, not a child. `<x-icon>` and `<x-badge>` take size and floating props instead of size/position classes; a small data-md-navigation-icon hook (the shared navigation-item.css) replaces the ad hoc "relative inline-flex" wrapper a floating badge anchors to. `rail-collapsed` (a Tailwind @custom-variant, forbidden in Phase F) is reproduced as plain selectors, branch for branch: the three width- independent conditions (a fixed collapsed mode; a collapsible rail the visitor collapsed and not open; a modal rail not open) merge into one :where() group, provably the same match set as three separate rules since :where(A, B, C) on an element is true exactly when :where(A) or :where(B) or :where(C) is; the four width-gated conditions stay separate media blocks, since CSS cannot merge different `@media` queries. Every rem length becomes px, since these are dp-based M3 tokens, not a text measure (unlike <x-pane>'s rem widths). The FAB overrides for a rail's header — elevation 0 (N-03), morphing into an extended FAB instead of swapping two by display (N-23) — move from unlayered into this file's own material.components, like toolbar.css's FAB override: fab.css's `[data-md-fab]` is one attribute, so a doubled selector here always outranks it without needing to sit outside the layer. navigation.js: the arriving-indicator stylesheet and every code comment follow the new hooks; resources/css/components/navigation.css is deleted (nothing imports it any more) and its line in tailwind.css with it. Behaviour is unchanged except one thing Tailwind's `rail-collapsed:` variant could do that plain CSS in this shape cannot: it is gone for consuming applications too, since the definition lived only in the file this commit removes. The development skill's guidance for it is rewritten to point at navigation-rail.css's own selectors instead of teaching a Tailwind variant that no longer exists. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
178 lines
10 KiB
PHP
178 lines
10 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 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-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)
|
||
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 by matching the rail's own state directly, the same
|
||
selectors resources/css/components/navigation-rail.css uses for every branch of "collapsed" —
|
||
`data-md-navigation-rail`'s value, `:not([data-md-open])`, and the window band each mode
|
||
collapses in; that file is the one place the numbers need to stay right, rather than a
|
||
Tailwind variant repeating them. Nothing that shows while collapsed may be wider than 96px.
|
||
|
||
Props: `label` names the landmark ("Main"); `width` is the expanded width (`256px`, 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 256px; `align` is `top` (the default) or `center`, which
|
||
puts the destinations at the rail's vertical centre — M3 prefers that on a tablet, for reach —
|
||
while the menu button, the brand and the FAB stay at the top and the footer at the foot, as M3
|
||
asks; more destinations than fit go back to the top rather than out of reach above the
|
||
scroller; `hide-when-collapsed` is M3's other expanded behaviour, for a `collapsible` or
|
||
`adaptive` rail: collapsing it takes it out of the layout altogether instead of narrowing it
|
||
to 96px, and it comes back expanded over a scrim when something calls `$store.rail.show()` —
|
||
a menu button in the app bar, which is the only way back, so put one there. Its own menu
|
||
button then docks it into the layout again. Not below `medium` for a collapsible rail, nor at
|
||
`medium` for an adaptive one: there it is the window and not the visitor that collapses a
|
||
rail, and M3's collapsed rail may never hide; `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-rail.css; the behaviour resources/js/navigation.js. --}}
|
||
|
||
@props([
|
||
'mode' => 'collapsible',
|
||
'label' => null,
|
||
'width' => '256px',
|
||
'align' => 'top',
|
||
'hideWhenCollapsed' => false,
|
||
'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);
|
||
// Only a rail that has a collapsed *and* an expanded state of its own can hide instead of
|
||
// narrowing; a `modal` one is already over the page, and the two fixed modes mean what they say.
|
||
$hideWhenCollapsed = $hideWhenCollapsed && in_array($mode, ['collapsible', 'adaptive'], true);
|
||
$canOpen = in_array($mode, ['modal', 'adaptive'], true) || $hideWhenCollapsed;
|
||
$menu ??= $interactive;
|
||
$collapsedAtFirst = in_array($mode, ['collapsed', 'modal'], true);
|
||
$narrow = $width === 'narrow';
|
||
$width = $narrow ? '256px' : $width;
|
||
$centred = $align === 'center';
|
||
@endphp
|
||
|
||
<div
|
||
data-md-navigation-rail="{{ $mode }}"
|
||
@if ($narrow) data-md-width="narrow" @endif
|
||
@if ($centred) data-md-align="center" @endif
|
||
@if ($hideWhenCollapsed) data-md-hide-when-collapsed @endif
|
||
@if ($divider) data-md-divider @endif
|
||
@unless ($fill) data-md-fill="false" @endunless
|
||
@if ($interactive)
|
||
x-data="materialNavigationRail('{{ $mode }}', {{ $hideWhenCollapsed ? 'true' : 'false' }})"
|
||
x-bind:data-md-open="open"
|
||
@endif
|
||
{{ $attributes->merge(['style' => "--navigation-rail-width: {$width}"]) }}
|
||
>
|
||
@if ($canOpen)
|
||
<div data-md-navigation-rail-scrim aria-hidden="true" x-on:click="$store.rail.hide()"></div>
|
||
@endif
|
||
|
||
<nav
|
||
data-md-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-md-navigation-rail-header>
|
||
@if ($menu || isset($brand))
|
||
<div data-md-navigation-rail-menu-row>
|
||
@if ($menu)
|
||
<button
|
||
type="button"
|
||
data-md-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="md-state-layer md-focus-ring md-touch-target"
|
||
>
|
||
<span data-md-navigation-rail-menu-collapse-icon><x-livewire-material::icon name="menu_open" /></span>
|
||
<span data-md-navigation-rail-menu-expand-icon><x-livewire-material::icon name="menu" /></span>
|
||
</button>
|
||
@endif
|
||
|
||
@isset($brand)
|
||
<div data-md-navigation-rail-brand>{{ $brand }}</div>
|
||
@endisset
|
||
</div>
|
||
@endif
|
||
|
||
@isset($header)
|
||
<div data-md-navigation-rail-fab-row {{ $header->attributes }}>{{ $header }}</div>
|
||
@endisset
|
||
</div>
|
||
@endif
|
||
|
||
<div data-md-navigation-rail-destinations>
|
||
{{ $slot }}
|
||
</div>
|
||
|
||
@isset($footer)
|
||
<div data-md-navigation-rail-footer {{ $footer->attributes }}>{{ $footer }}</div>
|
||
@endisset
|
||
</nav>
|
||
</div>
|