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
198 lines
11 KiB
PHP
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>
|