From 487176d34589585226234d1d7bd0f945b90319eb Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Tue, 15 Sep 2026 04:22:38 +0200 Subject: [PATCH] Give each canonical layout a page of its own in the Layout section Plan step 38: the Layout section's Overview keeps its breakpoint table and live "This window" readout (now a shared partial, and the five breakpoint cards a real range-queried data-md-showcase-breakpoint-card instead of Tailwind's max-medium:/medium:max-expanded: variants), but the three canonical layouts move off it onto a page of their own: /material/layout/{list-detail,supporting-pane,feed} (ShowcaseController::layout(), routes/showcase.php), each built on the real component, sharing one Sections:: layoutPages() feeds. The search index gains an entry per page with its own URL, and the component-homes scan now also reads the three pages' own source so , and keep a home. The whole section moves onto the layout components and md-* classes: no bespoke Tailwind grid or table styling remains. LayoutTest's browser test now visits the three dedicated pages instead of one shared /material/layout; a new Feature test asserts each page renders and is linked from the Layout section. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/showcase.css | 43 +++++ .../components/breakpoint-readout.blade.php | 18 ++ .../views/showcase/layout/feed.blade.php | 41 +++++ .../showcase/layout/list-detail.blade.php | 56 +++++++ .../showcase/layout/supporting-pane.blade.php | 54 ++++++ .../views/showcase/sections/layout.blade.php | 155 ++++++------------ routes/showcase.php | 6 + src/Http/Controllers/ShowcaseController.php | 13 ++ src/Showcase/Sections.php | 41 +++++ tests/Browser/LayoutTest.php | 30 ++-- tests/Feature/ShowcaseTest.php | 18 ++ 11 files changed, 355 insertions(+), 120 deletions(-) create mode 100644 resources/views/showcase/components/breakpoint-readout.blade.php create mode 100644 resources/views/showcase/layout/feed.blade.php create mode 100644 resources/views/showcase/layout/list-detail.blade.php create mode 100644 resources/views/showcase/layout/supporting-pane.blade.php diff --git a/resources/css/showcase.css b/resources/css/showcase.css index 37d4e7b1..64752847 100644 --- a/resources/css/showcase.css +++ b/resources/css/showcase.css @@ -276,4 +276,47 @@ width: 20px; height: 20px; } + + /* Layout: the five breakpoint cards, each lit by the same range query the layout components' + own stylesheets use, so the page proves the breakpoints rather than only describing them. */ + [data-md-showcase-breakpoint-card] { + border-radius: var(--md-sys-shape-corner-md); + background-color: var(--md-sys-color-surface-container); + padding: var(--md-sys-measurement-space200); + } + + @media (width < 600px) { + [data-md-showcase-breakpoint-card='compact'] { + background-color: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + } + } + + @media (600px <= width < 840px) { + [data-md-showcase-breakpoint-card='medium'] { + background-color: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + } + } + + @media (840px <= width < 1200px) { + [data-md-showcase-breakpoint-card='expanded'] { + background-color: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + } + } + + @media (1200px <= width < 1600px) { + [data-md-showcase-breakpoint-card='large'] { + background-color: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + } + } + + @media (width >= 1600px) { + [data-md-showcase-breakpoint-card='extra-large'] { + background-color: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + } + } } diff --git a/resources/views/showcase/components/breakpoint-readout.blade.php b/resources/views/showcase/components/breakpoint-readout.blade.php new file mode 100644 index 00000000..4a2a6295 --- /dev/null +++ b/resources/views/showcase/components/breakpoint-readout.blade.php @@ -0,0 +1,18 @@ +{{-- The live breakpoint readout: this window's width, and the M3 breakpoint it falls in, read + from the same numbers resources/js/breakpoints.js compares against. Shared by the Layout + section's Overview and its three canonical layout pages (plan step 38). --}} + + + + This window + + + + diff --git a/resources/views/showcase/layout/feed.blade.php b/resources/views/showcase/layout/feed.blade.php new file mode 100644 index 00000000..5dddf417 --- /dev/null +++ b/resources/views/showcase/layout/feed.blade.php @@ -0,0 +1,41 @@ +{{-- The feed canonical layout on a page of its own (plan step 38): the same working demo the + Layout section's Overview linked from its own navigation, built on the real , with + the live breakpoint readout the Overview page has. --}} + +@extends('livewire-material::showcase.layout') + +@php + $code = <<<'BLADE' + + The boats before the ferry. + Every lane, twice. + Colder than it looks. + Cheese, mostly. + Up to the ridge and back. + Asleep by the second stop. + + BLADE; +@endphp + +@section('content') + + + +

Foundations

+

Layout

+
+ + + + + +

+ M3's feed: cards or items to browse, in columns that multiply as the room grows — news, photos, + posts. One column below 600px, then as many equal columns of at least min-item as the + feed has room for. +

+ + +
+
+@endsection diff --git a/resources/views/showcase/layout/list-detail.blade.php b/resources/views/showcase/layout/list-detail.blade.php new file mode 100644 index 00000000..75814d31 --- /dev/null +++ b/resources/views/showcase/layout/list-detail.blade.php @@ -0,0 +1,56 @@ +{{-- The list-detail canonical layout on a page of its own (plan step 38): the same working demo + the Layout section's Overview linked from its own navigation, built on the real + , with the live breakpoint readout the Overview page has. --}} + +@extends('livewire-material::showcase.layout') + +@php + $code = <<<'BLADE' + + + + + + + + + + + + + + + +

Select a message to read it.

+

+

Below 840px the list gives way to this message, with a back button that returns you — and your focus — to the list. From 840px both panes show, the list 360px wide and 412px from 1200px.

+
+
+
+
+
+ BLADE; +@endphp + +@section('content') + + + +

Foundations

+

Layout

+
+ + + + + +

+ M3's list-detail: a list, and the detail of what is selected in it — an inbox and a message, folders + and a file, settings and a category. Below 840px the list gives way to the detail with a back button; + from 840px both panes show, the list a fixed 360px (412px from 1200px) and the detail flexible. +

+ + +
+
+@endsection diff --git a/resources/views/showcase/layout/supporting-pane.blade.php b/resources/views/showcase/layout/supporting-pane.blade.php new file mode 100644 index 00000000..fe17e7ee --- /dev/null +++ b/resources/views/showcase/layout/supporting-pane.blade.php @@ -0,0 +1,54 @@ +{{-- The supporting-pane canonical layout on a page of its own (plan step 38): the same working + demo the Layout section's Overview linked from its own navigation, built on the real + , with the live breakpoint readout the Overview page has. --}} + +@extends('livewire-material::showcase.layout') + +@php + $code = <<<'BLADE' + + + + + +

Three days by the lake in June: the train on Friday morning, two nights at the harbour, and a walk round the old town on Sunday before the train home.

+

From 840px the comments sit beside the proposal, 360px wide (412px from 1200px). Below it they are a bottom sheet docked to the window: press its handle to read them.

+
+
+
+
+ + + + + + + + + +
+ BLADE; +@endphp + +@section('content') + + + +

Foundations

+

Layout

+
+ + + + + +

+ M3's supporting pane: a focus pane, and beside it what only means something in relation to it — the + comments on a document, the details of a video. Below 840px it moves under the focus pane or into a + bottom sheet; from 840px it sits beside it, a fixed 360px (412px from 1200px). +

+ + +
+
+@endsection diff --git a/resources/views/showcase/sections/layout.blade.php b/resources/views/showcase/sections/layout.blade.php index a9e24f83..7eafe179 100644 --- a/resources/views/showcase/sections/layout.blade.php +++ b/resources/views/showcase/sections/layout.blade.php @@ -1,71 +1,13 @@ -{{-- M3's window size classes, live: the one this window is in, what each one changes, and the - variants that key on them. The five cards light up through the variants themselves, so the - page proves the compiled breakpoints rather than describing them. +{{-- M3's breakpoints, live: the one this window is in, what each one changes, and the cards that + light up through real media queries, so the page proves the breakpoints rather than only + describing them. - Then the layout components, and an example of each canonical layout — list-detail, supporting - pane, feed — written as an application writes them, with the layout components and the md-* - text classes and no Tailwind. Resize the window to watch each one change at its breakpoints. --}} + Then the layout components, and the way on to the three canonical layouts — list-detail, + supporting pane, feed — each on a page of its own (plan step 38), reached through the + section navigation below. --}} @php $examples = [ - 'List-detail' => <<<'BLADE' - - - - - - - - - - - - - - - -

Select a message to read it.

-

-

Below 840px the list gives way to this message, with a back button that returns you — and your focus — to the list. From 840px both panes show, the list 360px wide and 412px from 1200px.

-
-
-
-
-
- BLADE, - 'Supporting pane' => <<<'BLADE' - - - - - -

Three days by the lake in June: the train on Friday morning, two nights at the harbour, and a walk round the old town on Sunday before the train home.

-

From 840px the comments sit beside the proposal, 360px wide (412px from 1200px). Below it they are a bottom sheet docked to the window: press its handle to read them.

-
-
-
-
- - - - - - - - - -
- BLADE, - 'Feed' => <<<'BLADE' - - The boats before the ferry. - Every lane, twice. - Colder than it looks. - Cheese, mostly. - Up to the ridge and back. - Asleep by the second stop. - - BLADE, 'Surfaces, stacks, rows and grids' => <<<'BLADE' @@ -88,59 +30,48 @@ ]; @endphp -
-

Layout

+ +

Layout

-

- Widths are M3's window size classes and nothing else: compact below 600px, then medium: 600, - expanded: 840, large: 1200 and extra-large: 1600, with - max-medium: and friends for "below". Tailwind's sm:2xl: compile to - nothing. Scripts read the same numbers from resources/js/breakpoints.js. +

+ Widths are M3's breakpoints and nothing else: compact below 600px, then medium 600, expanded 840, large 1200 + and extra-large 1600. Scripts read the same numbers from resources/js/breakpoints.js.

-
- This window - - -
+ -
-
-

Compact

-

below 600px

+ +
+

Compact

+

below 600px

-
-

Medium

-

600–839px

+
+

Medium

+

600–839px

-
-

Expanded

-

840–1199px

+
+

Expanded

+

840–1199px

-
-

Large

-

1200–1599px

+
+

Large

+

1200–1599px

-
-

Extra-large

-

from 1600px

+
+

Extra-large

+

from 1600px

-
+ -
- + + - - - - - + + + + + @@ -180,10 +111,10 @@ -
ClassNavigationPanesDialogs and sheetsMarginClassNavigationPanesDialogs and sheetsMargin
24px
-
+ + -

+

<x-scaffold> does all of this: resize this showcase and watch the bar, the rail and the content margin (--material-margin) change at 600, 840 and 1200px. A visitor who has chosen a rail width keeps it; until then the rail follows the class. @@ -209,7 +140,13 @@ + +

Each canonical layout, on a page of its own

+ + + + @foreach ($examples as $title => $code) @endforeach -
+ diff --git a/routes/showcase.php b/routes/showcase.php index f94c5611..fcd64c88 100644 --- a/routes/showcase.php +++ b/routes/showcase.php @@ -32,6 +32,12 @@ Route::get('mail', [ShowcasePageController::class, 'mail'])->name('mail'); Route::get('search.json', [ShowcaseController::class, 'search'])->name('search'); +// The Layout section's three canonical layouts (plan step 38), each a page of its own rather +// than an anchor on the Layout section's Overview (`{section}` below). +Route::get('layout/{page}', [ShowcaseController::class, 'layout']) + ->whereIn('page', ['list-detail', 'supporting-pane', 'feed']) + ->name('layout'); + Route::get('{section}', [ShowcaseController::class, 'section']) ->whereIn('section', array_keys(Sections::all())) ->name('section'); diff --git a/src/Http/Controllers/ShowcaseController.php b/src/Http/Controllers/ShowcaseController.php index 826c84a1..ddc9770e 100644 --- a/src/Http/Controllers/ShowcaseController.php +++ b/src/Http/Controllers/ShowcaseController.php @@ -24,6 +24,19 @@ class ShowcaseController ]); } + /** + * One of the Layout section's own pages (plan step 38): a canonical layout on a page of its + * own, sharing the Layout section's rail entry and title. + */ + public function layout(string $page): View + { + return view('livewire-material::showcase.layout.'.$page, [ + 'sections' => Sections::all(), + 'section' => 'layout', + 'title' => Sections::layoutPages()[$page]['title'].' · Layout', + ]); + } + /** * The search's index, fetched when the search is first focused rather than written into every page. */ diff --git a/src/Showcase/Sections.php b/src/Showcase/Sections.php index d1404b8f..50569d7e 100644 --- a/src/Showcase/Sections.php +++ b/src/Showcase/Sections.php @@ -40,6 +40,19 @@ class Sections } } + // The Layout section's three canonical layouts (plan step 38): each is a page of its own + // rather than an anchor on the Overview, so its search entry carries its own URL; the + // page's own source joins the Layout section's for the component-homes scan below, so + // , and still find a home. + foreach (static::layoutPages() as $key => $page) { + if ($key === 'layout') { + continue; + } + + $entries[] = ['title' => $page['title'], 'kind' => 'Example', 'context' => 'Layout', 'url' => $page['url'].'#'.static::anchor($page['title']), 'keywords' => '']; + $sources['layout'] .= (string) file_get_contents(static::layoutPagePath($key)); + } + // A section that names a component in its introduction is its home; otherwise the first that uses it. foreach (['/<x-([a-z0-9-]+)>/', '/])/'] as $pattern) { foreach ($sources as $key => $source) { @@ -76,6 +89,34 @@ class Sections return dirname(__DIR__, 2).'/resources/views/showcase/sections/'.$key.'.blade.php'; } + /** + * The Layout section's own pages (plan step 38): the overview and the three canonical + * layouts, each on a page of its own, sharing one `` and the live breakpoint + * readout rather than three anchors on the Overview. They are not top-level destinations of + * their own — `Sections::all()` still has one `layout` entry for the rail and the overview + * grid — so this is their own small index instead. + * + * @return array + */ + public static function layoutPages(): array + { + return [ + 'layout' => ['title' => 'Overview', 'icon' => 'devices', 'url' => route('livewire-material.section', 'layout', false)], + 'list-detail' => ['title' => 'List-detail', 'icon' => 'view_sidebar', 'url' => route('livewire-material.layout', 'list-detail', false)], + 'supporting-pane' => ['title' => 'Supporting pane', 'icon' => 'splitscreen', 'url' => route('livewire-material.layout', 'supporting-pane', false)], + 'feed' => ['title' => 'Feed', 'icon' => 'view_agenda', 'url' => route('livewire-material.layout', 'feed', false)], + ]; + } + + /** + * One of the Layout section's own pages' view file (`layoutPages()`'s keys but `layout` + * itself, which is `path('layout')`). + */ + public static function layoutPagePath(string $key): string + { + return dirname(__DIR__, 2).'/resources/views/showcase/layout/'.$key.'.blade.php'; + } + /** * @return array */ diff --git a/tests/Browser/LayoutTest.php b/tests/Browser/LayoutTest.php index 043df15d..8b515c62 100644 --- a/tests/Browser/LayoutTest.php +++ b/tests/Browser/LayoutTest.php @@ -529,25 +529,33 @@ it('draws no second margin for a pane or a canonical layout inside the scaffold\ ->assertScript(layoutStyle('#pane [data-md-pane-body]', 'paddingLeft')." === '0px'"); }); -it('shows each canonical layout working on the showcase\'s Layout page', function () { - $example = fn (string $title): string => "#example-{$title}"; - - $page = layoutReady(visit('/material/layout')->resize(1280, 900)) +it('shows each canonical layout working on its own Layout page', function () { + $listDetail = layoutReady(visit('/material/layout/list-detail')->resize(1280, 900)) ->assertScript("document.querySelectorAll('#example-list-detail [data-md-list-detail-pane]').length === 2") - ->assertScript("[...document.querySelectorAll('#example-list-detail [data-md-list-detail-pane]')].every((pane) => pane.checkVisibility())") - ->assertScript(layoutRect($example('supporting-pane').' [data-md-supporting-pane-supporting]', 'top').' === '.layoutRect($example('supporting-pane').' [data-md-supporting-pane-main]', 'top')) - ->assertScript(layoutColumns($example('feed').' [data-md-feed]').' > 1'); + ->assertScript("[...document.querySelectorAll('#example-list-detail [data-md-list-detail-pane]')].every((pane) => pane.checkVisibility())"); - $page->click('#example-list-detail a:has-text("Ben Keller")') + $listDetail->click('#example-list-detail a:has-text("Ben Keller")') ->assertSeeIn('#example-list-detail [data-md-list-detail-pane="detail"]', 'Contract, second draft') ->assertNoJavaScriptErrors(); - $compact = layoutReady(visit('/material/layout')->resize(599, 900)); + $compactListDetail = layoutReady(visit('/material/layout/list-detail')->resize(599, 900)); - $compact->click('#example-list-detail a:has-text("Chiara Rossi")') + $compactListDetail->click('#example-list-detail a:has-text("Chiara Rossi")') ->assertSeeIn('#example-list-detail [data-md-list-detail-pane="detail"]', 'Train times for Friday') ->assertScript("! document.querySelector('#example-list-detail [data-md-list-detail-pane=\"list\"]').checkVisibility()") - ->assertScript(layoutColumns($example('feed').' [data-md-feed]').' === 1') + ->assertNoJavaScriptErrors(); + + layoutReady(visit('/material/layout/supporting-pane')->resize(1280, 900)) + ->assertScript(layoutRect('#example-supporting-pane [data-md-supporting-pane-supporting]', 'top').' === '.layoutRect('#example-supporting-pane [data-md-supporting-pane-main]', 'top')); + + layoutReady(visit('/material/layout/supporting-pane')->resize(599, 900)) ->assertScript("document.querySelector('#example-supporting-pane [data-md-supporting-pane-handle]').checkVisibility()") ->assertNoJavaScriptErrors(); + + layoutReady(visit('/material/layout/feed')->resize(1280, 900)) + ->assertScript(layoutColumns('#example-feed [data-md-feed]').' > 1'); + + layoutReady(visit('/material/layout/feed')->resize(599, 900)) + ->assertScript(layoutColumns('#example-feed [data-md-feed]').' === 1') + ->assertNoJavaScriptErrors(); }); diff --git a/tests/Feature/ShowcaseTest.php b/tests/Feature/ShowcaseTest.php index 4d1260b5..2e798c79 100644 --- a/tests/Feature/ShowcaseTest.php +++ b/tests/Feature/ShowcaseTest.php @@ -45,6 +45,24 @@ it('gives every section a page of its own, linked from the overview and the rail $this->get('/material/not-a-section')->assertNotFound(); }); +it('gives each canonical layout a page of its own, linked from the Layout section', function () { + foreach (['list-detail', 'supporting-pane', 'feed'] as $page) { + test()->withoutVite()->get("/material/layout/{$page}") + ->assertOk() + ->assertSee('id="example-'.$page.'"', false) + // Its own navigation reaches the other three pages, including itself. + ->assertSee(route('livewire-material.layout', 'list-detail', false), false) + ->assertSee(route('livewire-material.layout', 'supporting-pane', false), false) + ->assertSee(route('livewire-material.layout', 'feed', false), false); + } + + test()->withoutVite()->get('/material/layout') + ->assertOk() + ->assertSee(route('livewire-material.layout', 'list-detail', false), false) + ->assertSee(route('livewire-material.layout', 'supporting-pane', false), false) + ->assertSee(route('livewire-material.layout', 'feed', false), false); +}); + it('indexes every section, example and component for the search, each linking to a place that exists', function () { $index = collect($this->getJson('/material/search.json')->assertOk()->json());