Put the showcase in the app shell, with a page per section
tests / lint (push) Has been cancelled
tests / feature (8.4) (push) Has been cancelled
tests / feature (8.5) (push) Has been cancelled
tests / browser (chrome, chromium) (push) Has been cancelled
tests / browser (firefox, firefox) (push) Has been cancelled
tests / browser (safari, webkit) (push) Has been cancelled

The showcase is now an overview and one page per section behind a
grouped navigation rail, moved between with wire:navigate, instead of
one long page under a scrolling top bar. The switch exposed a WebKit
bug in the menu: inside a focusable region, WebKit moves focus out of
the closing popover before its toggle event, so Escape no longer
returned focus to the trigger. The menu now reads it on beforetoggle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 09:30:26 +02:00
co-authored by Claude Opus 5
parent 23218431bf
commit 889f8a8aa1
20 changed files with 251 additions and 81 deletions
+49 -28
View File
@@ -1,46 +1,67 @@
{{-- The showcase's frame, and the package's own app shell at work: the rail groups every section,
collapses and expands from `lg`, and opens as a modal from the app bar's menu button on a phone.
Pages move with wire:navigate, so the rail keeps its place and the theme stays. --}}
@php
$sections ??= \NoNameWeb\LivewireMaterial\Showcase\Sections::all();
$section ??= null;
$title = $section !== null ? $sections[$section]['title'] : 'Livewire Material';
$destinations = [[
'title' => 'Overview',
'icon' => 'home',
'url' => route('livewire-material.showcase'),
'active' => $section === null,
'bar' => false,
]];
foreach ($sections as $key => $entry) {
$destinations[] = [
'title' => $entry['title'],
'icon' => $entry['icon'],
'url' => route('livewire-material.section', $key),
'active' => $key === $section,
'section' => $entry['group'],
'bar' => false,
];
}
@endphp
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="robots" content="noindex" />
<title>@yield('title', 'Livewire Material')</title>
<title>{{ $section !== null ? $title.' · Livewire Material' : 'Livewire Material' }}</title>
<x-livewire-material::theme-script />
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
</head>
<body class="min-h-screen bg-surface font-sans text-on-surface antialiased">
<header class="sticky top-0 z-10 border-b border-divider bg-surface-container">
<div class="mx-auto flex max-w-6xl items-center gap-x-6 px-4 py-3">
<a href="{{ route('livewire-material.showcase') }}" class="shrink-0 type-title-lg max-sm:hidden">Livewire Material</a>
<body class="bg-surface font-sans text-on-surface antialiased">
<x-livewire-material::app-shell :destinations="$destinations" label="Showcase" rail-width="17rem">
<x-slot:brand>
<a href="{{ route('livewire-material.showcase') }}" wire:navigate class="block truncate rounded-corner-xs type-title-lg focus-ring">Livewire Material</a>
</x-slot:brand>
<nav class="-my-2 flex min-w-0 flex-1 gap-x-4 overflow-x-auto py-2 whitespace-nowrap type-label-lg text-on-surface-variant [scrollbar-width:none]" aria-label="Sections">
@foreach (['colour' => 'Colour', 'type' => 'Type', 'shape' => 'Shape', 'elevation' => 'Elevation', 'motion' => 'Motion', 'icons' => 'Icons', 'buttons' => 'Buttons', 'menus' => 'Menus', 'communication' => 'Communication', 'progress' => 'Progress', 'containment' => 'Containment', 'carousel' => 'Carousel', 'fields' => 'Fields', 'chips' => 'Chips', 'sliders' => 'Sliders', 'pickers' => 'Date pickers', 'timepickers' => 'Time pickers', 'bars' => 'Bars', 'navigation' => 'Navigation', 'data' => 'Data', 'pages' => 'Pages'] as $anchor => $section)
<a href="#{{ $anchor }}" class="rounded-corner-xs hover:text-on-surface focus-ring">{{ $section }}</a>
@endforeach
</nav>
<x-slot:top>
<x-livewire-material::app-bar :title="$title">
<x-slot:navigation>
<span class="sm:hidden"><x-livewire-material::button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="showcase-menu" /></span>
</x-slot:navigation>
<div class="ms-auto flex shrink-0 rounded-corner-full border border-outline" role="group" aria-label="Theme" x-data x-cloak>
@foreach (['light' => 'Light', 'dark' => 'Dark', 'system' => 'System'] as $choice => $label)
<button
type="button"
data-test="theme-{{ $choice }}"
class="state-layer focus-ring px-4 py-1.5 type-label-lg first:rounded-s-corner-full last:rounded-e-corner-full"
x-on:click="$store.theme.set('{{ $choice }}')"
x-bind:class="$store.theme.choice === '{{ $choice }}' && 'bg-secondary-container text-on-secondary-container'"
x-bind:aria-pressed="($store.theme.choice === '{{ $choice }}').toString()"
>{{ $label }}</button>
@endforeach
</div>
</div>
</header>
<x-slot:actions>
<span class="me-3 max-md:hidden"><x-livewire-material::theme-toggle mode="picker" /></span>
<span class="md:hidden"><x-livewire-material::theme-toggle mode="cycle" /></span>
</x-slot:actions>
</x-livewire-material::app-bar>
</x-slot:top>
@yield('content')
<x-livewire-material::toast />
@yield('content')
</x-livewire-material::app-shell>
@livewireScripts
</body>