Add a search to the showcase
tests / feature (8.5) (push) Successful in 1m14s
tests / lint (push) Successful in 1m1s
tests / feature (8.4) (push) Successful in 1m9s
tests / browser (chrome, chromium) (push) Successful in 3m22s
tests / browser (firefox, firefox) (push) Successful in 4m37s
tests / browser (safari, webkit) (push) Successful in 5m4s

A search app bar looks through every section, example and component,
opens the section, or the example at its anchor on its page, and is
reached from anywhere with / or Ctrl+K. Section pages now carry their
own heading.

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:56:00 +02:00
co-authored by Claude Opus 5
parent 7ebcb0565f
commit 51bf034b74
13 changed files with 272 additions and 27 deletions
@@ -3,6 +3,7 @@
namespace NoNameWeb\LivewireMaterial\Http\Controllers;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use NoNameWeb\LivewireMaterial\Showcase\Sections;
/**
@@ -22,4 +23,12 @@ class ShowcaseController
'section' => $section,
]);
}
/**
* The search's index, fetched when the search is first focused rather than written into every page.
*/
public function search(): JsonResponse
{
return response()->json(Sections::index());
}
}
+69
View File
@@ -2,11 +2,80 @@
namespace NoNameWeb\LivewireMaterial\Showcase;
use Illuminate\Support\Str;
use NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider;
/**
* The showcase's pages: one per section, grouped as the rail groups them.
*/
class Sections
{
/**
* What the showcase's search looks through: every section, every example in it (linked to the
* example's anchor), and every component, linked to the first section that introduces it.
*
* @return list<array{title: string, kind: string, context: string, url: string, keywords: string}>
*/
public static function index(): array
{
$sections = static::all();
$components = collect(glob(LivewireMaterialServiceProvider::componentPath().'/*.blade.php'))
->map(fn (string $path): string => basename($path, '.blade.php'))
->all();
$sources = collect($sections)->map(fn (array $section, string $key): string => (string) file_get_contents(static::path($key)));
$entries = [];
$homes = [];
foreach ($sections as $key => $section) {
$url = route('livewire-material.section', $key, false);
$entries[] = ['title' => $section['title'], 'kind' => 'Section', 'context' => $section['group'], 'url' => $url, 'keywords' => $section['description']];
preg_match_all("/^\\s*'((?:[^'\\\\]|\\\\.)*)'\\s*=>\\s*<<<'BLADE'/m", $sources[$key], $examples);
foreach ($examples[1] as $title) {
$title = stripslashes($title);
$entries[] = ['title' => $title, 'kind' => 'Example', 'context' => $section['title'], 'url' => $url.'#'.static::anchor($title), 'keywords' => ''];
}
}
// A section that names a component in its introduction is its home; otherwise the first that uses it.
foreach (['/&lt;x-([a-z0-9-]+)&gt;/', '/<x-(?:livewire-material::)?([a-z0-9-]+)(?=[\\s\\/>])/'] as $pattern) {
foreach ($sources as $key => $source) {
preg_match_all($pattern, $source, $tags);
foreach ($tags[1] as $tag) {
if (in_array($tag, $components, true)) {
$homes[$tag] ??= $key;
}
}
}
}
foreach ($homes as $tag => $key) {
$entries[] = ['title' => "<x-{$tag}>", 'kind' => 'Component', 'context' => $sections[$key]['title'], 'url' => route('livewire-material.section', $key, false), 'keywords' => str_replace('-', ' ', $tag)];
}
return $entries;
}
/**
* The anchor of an example on its section's page.
*/
public static function anchor(string $title): string
{
return 'example-'.Str::slug($title);
}
/**
* A section's view file.
*/
public static function path(string $key): string
{
return dirname(__DIR__, 2).'/resources/views/showcase/sections/'.$key.'.blade.php';
}
/**
* @return array<string, array{title: string, icon: string, group: string, description: string}>
*/