Files
livewire-material/resources/views/showcase/layout.blade.php
T
Andreas Reinhold / reiniandClaude Sonnet 5 42009f3b0c Stop loading Tailwind on the showcase page
Plan step 38 (last batch): every showcase view is now rewritten
without Tailwind, so the showcase's layout keeps only the JavaScript
entries of the application's configured Vite list (Livewire, Alpine)
before its own bundle link, instead of the whole array — its own CSS
already comes solely from ShowcaseAssetController's bundle
(Stylesheets::bundle() of all.css, showcase.css and the scheme), and
linking the application's build too meant the package's own CSS
(all.css) loaded twice on every showcase page (once through the
Workbench's package.css entry, once through the showcase's own
bundle), besides pulling in Tailwind's CSS for nothing. ErrorPage::
assets() keeps passing the whole configured list: the error pages have
no bundle of their own to fall back on, and step 40 is where their
fallback is redesigned. The config's own comment says so.

The frame's temporary visually-hidden <h2> rule goes too — it existed
only because the first batch could not reach into the still-Tailwind
sections/*.blade.php partials it @include's; every section now
carries its own real `class="md-type-headline-md"` heading, so the
structural rule that quietly hid all of them is dead weight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-15 05:33:33 +02:00

198 lines
11 KiB
PHP

{{-- The showcase's frame, and the package's own scaffold 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, viewport-fit=cover" />
<meta name="robots" content="noindex" />
<title>{{ $section !== null ? $title.' · Livewire Material' : 'Livewire Material' }}</title>
<x-livewire-material::theme-script />
{{-- The application's own entries, JavaScript only (Livewire, Alpine): no showcase view
needs anything Tailwind builds any more, and linking its CSS here would open the
document's first @layer statement (Workbench package.css's header explains the trick)
ahead of the showcase's own bundle below, re-anchoring `material` before Tailwind's
own layers instead of after. config('livewire-material.showcase.vite')'s own comment
says why ErrorPage::assets() still passes the whole array. --}}
@vite(array_filter(config('livewire-material.showcase.vite'), fn (string $entry): bool => ! str_ends_with($entry, '.css')))
@livewireStyles
{{-- The showcase's own chrome: does not depend on the build above. --}}
<link rel="stylesheet" href="{{ \NoNameWeb\LivewireMaterial\Http\Controllers\ShowcaseAssetController::url() }}" />
</head>
<body data-md-showcase>
<x-livewire-material::scaffold :destinations="$destinations" label="Showcase" rail-width="17rem">
<x-slot:brand>
<a href="{{ route('livewire-material.showcase') }}" wire:navigate class="md-type-title-lg md-truncate md-focus-ring" data-md-showcase-brand>Livewire Material</a>
</x-slot:brand>
<x-slot:top>
<x-livewire-material::app-bar variant="search">
<x-slot:navigation>
<x-livewire-material::stack as="span" hide-from="medium">
<x-livewire-material::button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="showcase-menu" />
</x-livewire-material::stack>
</x-slot:navigation>
{{-- The search looks through every section, example and component: the index is
fetched on first focus, and / or Ctrl+K (⌘K) reaches it from anywhere. Its names
keep clear of <x-search>'s own (`results`, `open`), which the slot also sees. --}}
<div
data-md-showcase-search
x-data="{
query: '',
entries: null,
async load() {
this.entries ??= await (await fetch(@js(route('livewire-material.search', [], false)))).json();
},
get matches() {
const words = this.query.toLowerCase().split(/\s+/).filter(Boolean);
if (! this.entries || words.length === 0) return [];
const phrase = words.join(' ');
return this.entries
.map((entry) => {
const title = entry.title.toLowerCase().replace(/[<>]/g, '');
const text = `${title} ${entry.context} ${entry.kind} ${entry.keywords}`.toLowerCase();
if (! words.every((word) => text.includes(word))) return null;
const rank = title === phrase ? 0 : title.startsWith(phrase) ? 1 : title.includes(phrase) ? 2 : 3;
return { ...entry, rank };
})
.filter(Boolean)
.sort((a, b) => a.rank - b.rank)
.slice(0, 30);
},
go(event, result) {
if (! result || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || (event.button ?? 0) !== 0) return;
event.preventDefault();
const url = new URL(result.url, window.location.href);
const search = this.$root.querySelector('[data-md-search]');
this.query = '';
if (search) window.Alpine.$data(search).close();
if (url.pathname === window.location.pathname) {
window.location.hash = url.hash;
document.getElementById(url.hash.slice(1))?.scrollIntoView({ block: 'start' });
} else {
window.Livewire.navigate(url.pathname + url.hash);
}
},
shortcut(event) {
const typing = event.target.closest('input, textarea, select, [contenteditable]');
if ((event.key === '/' && ! typing) || (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey))) {
event.preventDefault();
document.getElementById('showcase-search').focus();
}
},
}"
x-on:keydown.window="shortcut($event)"
>
<x-livewire-material::search
id="showcase-search"
placeholder="Search components, examples and sections"
x-model="query"
x-on:focus.once="load()"
x-on:keydown.enter.prevent="go($event, matches[0])"
>
<p x-show="query.trim() === ''" class="md-type-body-md md-ink-variant" data-md-showcase-search-hint>
Type a component (<code>datepicker</code>), an example or a section. Press <kbd class="md-type-label-md" data-md-showcase-kbd>/</kbd> to search from anywhere.
</p>
<p x-cloak x-show="query.trim() !== '' && entries !== null && matches.length === 0" class="md-type-body-md md-ink-variant" data-md-showcase-search-hint>
Nothing matches “<span x-text="query.trim()"></span>”.
</p>
<template x-for="result in matches" x-bind:key="result.url + result.title">
<a
x-bind:href="result.url"
x-on:click="go($event, result)"
class="md-state-layer md-focus-ring"
data-showcase-result
data-md-showcase-search-result
>
<span class="md-type-body-lg md-ink md-truncate" x-text="result.title"></span>
<span class="md-type-body-sm md-ink-variant md-truncate" x-text="`${result.kind} · ${result.context}`"></span>
</a>
</template>
</x-livewire-material::search>
</div>
<x-slot:actions>
<x-livewire-material::row gap="space100" align="center">
{{-- With colour profiles, a preview of each on this page; nothing is stored. --}}
@if (($profiles = \NoNameWeb\LivewireMaterial\Support\Scheme::profiles()) !== [])
<x-livewire-material::menu label="Colour profile" position="bottom-end" data-test="showcase-profiles">
<x-slot:trigger>
<x-livewire-material::button icon="palette" tooltip="Colour profile" />
</x-slot:trigger>
@foreach ($profiles as $name => $profile)
<x-livewire-material::menu-item :label="$profile['label']" x-on:click="$store.theme.previewScheme('{{ $name }}')" data-scheme-preview="{{ $name }}" />
@endforeach
</x-livewire-material::menu>
@endif
<x-livewire-material::stack as="span" hide-below="expanded">
<x-livewire-material::theme-toggle mode="picker" />
</x-livewire-material::stack>
<x-livewire-material::stack as="span" hide-from="expanded">
<x-livewire-material::theme-toggle mode="cycle" />
</x-livewire-material::stack>
</x-livewire-material::row>
</x-slot:actions>
</x-livewire-material::app-bar>
</x-slot:top>
@yield('content')
</x-livewire-material::scaffold>
@livewireScripts
{{-- wire:navigate keeps the URL's hash but not the scroll to it: a search result that opens an
example on another page lands on the example. --}}
<script data-navigate-once>
document.addEventListener('livewire:navigated', () => {
const target = () => (window.location.hash.length > 1 ? document.getElementById(decodeURIComponent(window.location.hash.slice(1))) : null);
const land = () => target()?.scrollIntoView({ block: 'start' });
// After the swap's own scroll to the top and the page transition, and once more when
// the page's components have settled their size.
requestAnimationFrame(() => requestAnimationFrame(land));
setTimeout(() => {
const box = target()?.getBoundingClientRect();
if (box && (box.top < 0 || box.top > window.innerHeight / 2)) {
land();
}
}, 400);
});
</script>
</body>
</html>