From ab692b66bbc62f56abf1432269753efa37ba46e9 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Sun, 13 Sep 2026 08:54:51 +0200 Subject: [PATCH] Add the navigation bar, navigation rail and app shell M3 Expressive's flexible navigation bar, the collapsed, expanded and modal navigation rail with its state applied before the first paint, and an adaptive app shell composing them. The head script now restores the theme and rail attributes that wire:navigate strips from . Completes Phase 8. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy --- config/livewire-material.php | 16 + docs/plans/livewire-material.md | 22 + .../livewire-material-development/SKILL.md | 78 +++ resources/css/components/navigation.css | 584 ++++++++++++++++++ resources/css/material.css | 1 + resources/js/material.js | 1 + resources/js/navigation.js | 141 +++++ .../views/components/app-shell.blade.php | 146 +++++ .../components/navigation-bar-item.blade.php | 69 +++ .../views/components/navigation-bar.blade.php | 40 ++ .../components/navigation-rail-item.blade.php | 74 +++ .../navigation-rail-section.blade.php | 24 + .../components/navigation-rail.blade.php | 135 ++++ .../views/components/theme-script.blade.php | 42 +- resources/views/showcase/index.blade.php | 1 + resources/views/showcase/layout.blade.php | 2 +- .../showcase/sections/navigation.blade.php | 114 ++++ resources/views/showcase/shell.blade.php | 87 +++ routes/showcase.php | 4 + tests/Browser/NavigationTest.php | 175 ++++++ tests/Feature/Components/AppShellTest.php | 95 +++ .../Feature/Components/NavigationBarTest.php | 48 ++ .../Feature/Components/NavigationRailTest.php | 77 +++ tests/Feature/Components/ThemeScriptTest.php | 24 +- 24 files changed, 1995 insertions(+), 5 deletions(-) create mode 100644 resources/css/components/navigation.css create mode 100644 resources/js/navigation.js create mode 100644 resources/views/components/app-shell.blade.php create mode 100644 resources/views/components/navigation-bar-item.blade.php create mode 100644 resources/views/components/navigation-bar.blade.php create mode 100644 resources/views/components/navigation-rail-item.blade.php create mode 100644 resources/views/components/navigation-rail-section.blade.php create mode 100644 resources/views/components/navigation-rail.blade.php create mode 100644 resources/views/showcase/sections/navigation.blade.php create mode 100644 resources/views/showcase/shell.blade.php create mode 100644 tests/Browser/NavigationTest.php create mode 100644 tests/Feature/Components/AppShellTest.php create mode 100644 tests/Feature/Components/NavigationBarTest.php create mode 100644 tests/Feature/Components/NavigationRailTest.php diff --git a/config/livewire-material.php b/config/livewire-material.php index 6ddfc30c..eaf4f2f2 100644 --- a/config/livewire-material.php +++ b/config/livewire-material.php @@ -34,6 +34,22 @@ return [ 'legacy_keys' => [], ], + /* + |-------------------------------------------------------------------------- + | Navigation rail + |-------------------------------------------------------------------------- + | + | Whether a collapsible navigation rail starts 'expanded' or 'collapsed' + | until the visitor toggles it. The head script applies the choice before + | the first paint, from localStorage under 'storage_key'. + | + */ + + 'rail' => [ + 'default' => 'expanded', + 'storage_key' => 'material-rail', + ], + /* |-------------------------------------------------------------------------- | Fields diff --git a/docs/plans/livewire-material.md b/docs/plans/livewire-material.md index 9e369782..43948aa8 100644 --- a/docs/plans/livewire-material.md +++ b/docs/plans/livewire-material.md @@ -524,6 +524,28 @@ worktrees. What changed from the step above: the theme script), content region with `wire:transition.navigate`, snackbar host. Nothing app-specific inside; apps pass destinations and extra chrome as slots. +**Phase 8 is done (2026-09-13).** What changed from the steps above: + +- **App bars, toolbars, tabs, section nav, account menu and theme toggle were built in main; the + navigation bar, rail and app shell by an agent in its own worktree.** +- **A medium or large app bar collapses without script moving anything**: the bar is sticky at a + negative top (its measured height less the 64px row, so a wrapped title still fits) and its row + is sticky at 0 inside it; script only reports `scrolled` and `collapsed`. +- **The tab indicator moves in a view transition** (`view-transition-name` from a per-tablist + custom property, `view-transition-class` for the timing); every tab draws its own indicator, so it + is right before Alpine starts. +- **Livewire 4.4's `wire:navigate` gives `` the next page's attributes and removes the rest**, + which dropped `data-theme` on every in-app navigation. The head script saves the theme and rail + attributes on `livewire:navigating` and puts them back in `onSwap`, before anything paints. +- **The rail's collapsed state is applied before first paint** by the head script + (``, `rail.default`, `rail.storage_key`) and read by a `rail-collapsed:` variant; + `$store.rail` changes it. The shell draws no phone menu button: the app bar in `top` calls + `$store.rail.show()`. +- **Playwright's locators are strict**: `assertAttribute` on a selector matching two elements fails. +- **Tailwind only compiles classes it can see**: a class written only in a test probe (`h-[200vh]`) + does not exist in the Workbench build; use inline styles there. +- **A Tailwind `@variant` nested under a pseudo-element or a `* +` selector compiles to broken CSS.** + ### Phase 9 — Data, pages, mail 30. `table` (`.data-table`, descendant selectors, fine-pointer density, `position: relative`), diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index a1b5e73b..449c3d07 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -535,6 +535,84 @@ M3 search bar that opens into a search view: docked under the bar from `sm`, ful The docked view overlaps what is under it; never place a search inside an element with `overflow-hidden` (a card), which clips it. +### `` + +The adaptive app shell, a whole layout's body: a navigation bar below `sm`, a collapsed rail that opens as a modal to `lg`, an expanded rail the visitor can collapse from `lg`, the page as `
` behind a skip link, and the snackbar host (do not add another ``). It needs `` in ``. + +```blade + + SealShare + + + + + + + + + {{-- the page's app bar; its menu button opens the modal rail on a phone --}} + + + + {{ $slot }} + +``` + +- `destinations`: `title`, `icon`, `url`; optional `active` (default: the URL is the current one), `badge` (`true` for a dot, or a count), `section` (a heading in the rail, shown only while it is expanded; consecutive destinations with the same section are grouped), `bar` (default `true`; `false` keeps it out of the bottom bar — M3 wants three to five there), `navigate` (`false` for a full page load instead of `wire:navigate`). +- Slots, each rendered once: `brand` (beside the rail's menu button, expanded only), `rail-header` (a FAB), `rail-footer` (pinned to the foot of the rail), `actions` (a row of icon buttons at the very foot, stacked when collapsed), `top` (the app bar, above the page at every width), and the page. `label` names the landmarks ("Main"); `rail-width` is the expanded width (`16rem`). +- The rail is one element at every width: what is in it is also what a phone sees in the modal rail. Below `sm` nothing opens it but `$store.rail.show()`, so a page whose destinations are not all in the bar needs a menu button in its app bar (hidden from `sm`). +- Below `sm` the shell sets `--material-bottom-bar`, so the snackbar and a `fab` button clear the bar; pad anything else you pin to the bottom with it. +- The content region is `max-lg:overflow-x-clip`. Never make a page wrapper `overflow-x-hidden`: it turns the region into a scroll container and breaks every `sticky` inside. + +### ``, `` + +M3 Expressive's flexible navigation bar, for three to five destinations. It does not position itself; wrap it (`` does): + +```blade +
+ + + + +
+``` + +64px in surface-container with the bottom safe area under it. Narrower than 600px the icon sits in a 56×32 indicator over the label; from 600px (the bar's own width) icon and label share a 40px pill and the items gather in the middle. ``: `label` ("Main"). ``: `label` / slot, `icon`, `link` (with `wire:navigate` unless `external` or `no-wire-navigate`; without a link it is a button), `active` (`aria-current="page"`, filled icon, secondary-container indicator), `badge` (`true` for a dot, a number for a count, 999+ at most), `badge-label` (what a screen reader hears instead of ", 3"). + +### ``, ``, `` + +M3 Expressive's navigation rail: collapsed (96px, icon over label) or expanded (a 56px full-width pill, icon beside label, count at the end). + +```blade +
+ + SealShare + + + + + + + + + + + + +
+
+``` + +- `mode`: `collapsed`, `expanded`, `collapsible` (default: expanded until its menu button collapses it; the choice is `$store.rail`, remembered and applied before the first paint), `modal` (collapsed in the layout; the menu button or `$store.rail.show()` opens it expanded over a scrim, focus held until Escape, the scrim or leaving the page), `adaptive` (``'s: hidden and opened as a modal below `sm`, collapsed and opened as a modal to `lg`, collapsible from `lg`). +- Props: `label` ("Main"), `width` (expanded width, `16rem`, held between 220 and 360px), `menu` (the menu button; on by default for `collapsible`, `modal`, `adaptive`). Slots: `brand` (beside the menu button, expanded only), `header` (a FAB), the destinations (the only part that scrolls), `footer`. In a flex row the rail sticks to the top of the viewport. +- Anything inside a rail takes both shapes with the `rail-collapsed:` variant, true while that rail is drawn collapsed for whatever reason: `…expanded only…`, ``. Put the variant on a wrapper, never on a component. Nothing that shows while collapsed may be wider than 96px. +- ``: the same props as ``. ``: a group with a heading that shows only while the rail is expanded; it names the group for screen readers either way. +- `$store.rail`: `collapsed`, `toggle()`, `collapse()`, `expand()` (the remembered choice), `open`, `show()`, `hide()` (the modal rail; closed on every `wire:navigate`). `config/livewire-material.php` → `rail.default` (`expanded` or `collapsed`) and `rail.storage_key` (`material-rail`). + ### `` M3 Expressive top app bar, sticky by default (`:sticky="false"` to scroll away), turning surface-container once content scrolls under it. `variant`: `small` (default), `center`, `medium` and `large` (a big title that collapses into the row as the page scrolls — CSS sticky, no layout shift), `search` (put an `` in the slot). Props: `title`, `subtitle`, `heading` (`h1` default). Slots: `navigation` (leading icon button), `actions` (trailing icon buttons, avatar). diff --git a/resources/css/components/navigation.css b/resources/css/components/navigation.css new file mode 100644 index 00000000..3c83990c --- /dev/null +++ b/resources/css/components/navigation.css @@ -0,0 +1,584 @@ +/* + * M3 Expressive navigation: the flexible navigation bar and the navigation rail — collapsed, + * expanded and modal. + * + * Values from androidx Compose Material 3 (Apache-2.0) at androidx-main + * 27cf9a7d5788aa0f5f2d8b6699ce279560daf326: tokens/NavigationBarTokens.kt, + * NavigationBarVerticalItemTokens.kt, NavigationBarHorizontalItemTokens.kt, + * NavigationRailCollapsedTokens.kt, NavigationRailExpandedTokens.kt, + * NavigationRailBaselineItemTokens.kt, NavigationRailVerticalItemTokens.kt, + * NavigationRailHorizontalItemTokens.kt, NavigationRailColorTokens.kt, and the layout in + * ShortNavigationBar.kt, WideNavigationRail.kt and NavigationItem.kt. + * + * [data-navigation-bar] surface-container, 64px, the bottom safe area under it + * [data-navigation-bar-items] equal widths; centred from a 600px-wide bar + * [data-navigation-bar-item] data-active + * [data-navigation-pill] icon and label; the indicator itself from 600px + * [data-navigation-indicator] the 56×32 indicator around the icon below 600px + * [data-navigation-label] + * + * [data-navigation-rail="collapsed|expanded|collapsible|modal|adaptive"] data-open + * [data-navigation-rail-scrim] modal and adaptive rails + * [data-navigation-rail-panel] the