Rewrite the showcase frame without Tailwind

Plan step 38 (first batch): layout.blade.php, index.blade.php,
section.blade.php, shell.blade.php and the example component now
lay out through the layout components (pane, stack, row, grid,
surface) and their props, text through the md-* classes, and
everything else through showcase.css's data-md-showcase-* hooks — no
Tailwind utility remains in any of them. The head links the new
stylesheet route after @vite(...), not before: that entry opens the
document's first @layer statement, and linking the showcase's own
bundle ahead of it re-anchors the material layer before Tailwind's
own and lets its preflight's `padding: 0` beat the search view's
padding-top (found by a failing browser test). @vite(...) stays, its
Tailwind CSS entry included, only for the section views (still
Tailwind until the next batches) and the JS runtime; the config
comment says so.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-15 03:55:25 +02:00
co-authored by Claude Sonnet 5
parent 62b09c45b5
commit 9c8b39600b
6 changed files with 167 additions and 107 deletions
+8 -3
View File
@@ -191,9 +191,14 @@ return [
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Every component in every variant, rendered in the application's own | Every component in every variant, rendered in the application's own
| scheme. Off unless the application runs locally. 'vite' names the entry | scheme. Off unless the application runs locally. 'vite' names the
| points that import this package's CSS and JavaScript; the error pages | application's own entry points, for its JavaScript (Livewire, Alpine);
| load them too, showcase or not. | the error pages load them too, showcase or not. A CSS entry here is
| temporary — the showcase's own chrome no longer needs one (it serves
| its own bundle, ShowcaseAssetController), but the section views
| (resources/views/showcase/sections/*) are still Tailwind until step
| 38's last batch removes it, and an application without Tailwind of
| its own may still want one here for the error pages' build.
| |
*/ */
@@ -18,17 +18,30 @@
} }
@endphp @endphp
<div @if ($title) id="{{ \NoNameWeb\LivewireMaterial\Showcase\Sections::anchor($title) }}" @endif class="scroll-mt-24 space-y-4 rounded-corner-lg bg-surface-container p-4"> <x-livewire-material::surface
@if ($title) :id="$title ? \NoNameWeb\LivewireMaterial\Showcase\Sections::anchor($title) : null"
<h3 class="type-title-md">{{ $title }}</h3> padding="space200"
@endif corner="lg"
data-md-showcase-example
>
<x-livewire-material::stack gap="space200">
@if ($title)
<h3 class="md-type-title-md">{{ $title }}</h3>
@endif
<div @class(['flex flex-wrap items-center gap-4' => ! $stack, 'space-y-4' => $stack])> @if ($stack)
{!! \Illuminate\Support\Facades\Blade::render($code) !!} <x-livewire-material::stack gap="space200">
</div> {!! \Illuminate\Support\Facades\Blade::render($code) !!}
</x-livewire-material::stack>
@else
<x-livewire-material::row gap="space200" wrap align="center">
{!! \Illuminate\Support\Facades\Blade::render($code) !!}
</x-livewire-material::row>
@endif
<details> <details>
<summary class="w-fit cursor-pointer rounded-corner-xs type-label-md text-on-surface-variant focus-ring">Blade</summary> <summary class="md-type-label-md md-ink-variant md-focus-ring" data-md-showcase-code-summary>Blade</summary>
<pre class="mt-2 overflow-x-auto rounded-corner-sm bg-surface-container-highest p-3 type-body-sm"><code>{{ trim($code) }}</code></pre> <pre class="md-type-body-sm" data-md-showcase-code><code>{{ trim($code) }}</code></pre>
</details> </details>
</div> </x-livewire-material::stack>
</x-livewire-material::surface>
+32 -30
View File
@@ -1,36 +1,38 @@
@extends('livewire-material::showcase.layout') @extends('livewire-material::showcase.layout')
@section('content') @section('content')
<div class="mx-auto w-full max-w-6xl space-y-12 pt-4 pb-16"> <x-livewire-material::pane width="wide" data-md-showcase-page>
<div class="max-w-3xl space-y-3"> <x-livewire-material::stack gap="space600">
<h1 class="type-headline-lg">Livewire Material</h1> <x-livewire-material::stack gap="space150">
<p class="type-title-lg">Material 3 Expressive for Laravel and Livewire.</p> <h1 class="md-type-headline-lg">Livewire Material</h1>
<p class="type-body-lg text-on-surface-variant"> <p class="md-type-title-lg">Material 3 Expressive for Laravel and Livewire.</p>
Every token and component, rendered in this application's own scheme and theme. Pick a section in the <p class="md-type-body-lg md-ink-variant">
navigation, search for one above (or press <kbd class="rounded-corner-xs bg-surface-container-highest px-1.5 type-label-md">/</kbd>), or start below. Every token and component, rendered in this application's own scheme and theme. Pick a section in the
</p> navigation, search for one above (or press <kbd class="md-type-label-md" data-md-showcase-kbd>/</kbd>), or start below.
</div> </p>
</x-livewire-material::stack>
@foreach (collect($sections)->groupBy('group', preserveKeys: true) as $group => $entries) @foreach (collect($sections)->groupBy('group', preserveKeys: true) as $group => $entries)
<section class="space-y-4" aria-labelledby="group-{{ \Illuminate\Support\Str::slug($group) }}"> <x-livewire-material::stack as="section" gap="space200" :aria-labelledby="'group-'.\Illuminate\Support\Str::slug($group)">
<h2 id="group-{{ \Illuminate\Support\Str::slug($group) }}" class="type-title-lg">{{ $group }}</h2> <h2 id="group-{{ \Illuminate\Support\Str::slug($group) }}" class="md-type-title-lg">{{ $group }}</h2>
<div class="grid grid-cols-1 gap-3 medium:grid-cols-2 expanded:grid-cols-3"> <x-livewire-material::grid :columns="['compact' => 1, 'medium' => 2, 'expanded' => 3]" gap="space200">
@foreach ($entries as $key => $entry) @foreach ($entries as $key => $entry)
<x-livewire-material::card variant="outlined" data-md-list-row wire:key="section-{{ $key }}"> <x-livewire-material::card variant="outlined" data-md-list-row wire:key="section-{{ $key }}">
<div class="flex items-start gap-4"> <x-livewire-material::row gap="space200" align="start">
<span class="grid size-12 shrink-0 place-items-center rounded-corner-lg bg-secondary-container text-on-secondary-container"> <span data-md-showcase-section-icon>
<x-livewire-material::icon :name="$entry['icon']" /> <x-livewire-material::icon :name="$entry['icon']" />
</span> </span>
<div class="min-w-0"> <x-livewire-material::stack gap="space50">
<a href="{{ route('livewire-material.section', $key) }}" wire:navigate data-md-list-open class="type-title-md">{{ $entry['title'] }}</a> <a href="{{ route('livewire-material.section', $key) }}" wire:navigate data-md-list-open class="md-type-title-md">{{ $entry['title'] }}</a>
<p class="mt-1 type-body-md text-on-surface-variant">{{ $entry['description'] }}</p> <p class="md-type-body-md md-ink-variant">{{ $entry['description'] }}</p>
</div> </x-livewire-material::stack>
</div> </x-livewire-material::row>
</x-livewire-material::card> </x-livewire-material::card>
@endforeach @endforeach
</div> </x-livewire-material::grid>
</section> </x-livewire-material::stack>
@endforeach @endforeach
</div> </x-livewire-material::stack>
</x-livewire-material::pane>
@endsection @endsection
+41 -23
View File
@@ -38,26 +38,37 @@
<x-livewire-material::theme-script /> <x-livewire-material::theme-script />
{{-- Kept only for the section views (resources/views/showcase/sections/*), still Tailwind
until the next batches of step 38, and for the Livewire/Alpine runtime; its CSS entry
goes with them. It opens the document's first @layer statement (Workbench package.css's
header explains the trick), which the link below must follow, not precede — first would
re-anchor `material` ahead of Tailwind's own layers and let its preflight's `padding: 0`
beat the search view's padding-top. --}}
@vite(config('livewire-material.showcase.vite')) @vite(config('livewire-material.showcase.vite'))
@livewireStyles @livewireStyles
{{-- The showcase's own chrome: does not depend on the build above. --}}
<link rel="stylesheet" href="{{ \NoNameWeb\LivewireMaterial\Http\Controllers\ShowcaseAssetController::url() }}" />
</head> </head>
<body class="bg-surface font-sans text-on-surface antialiased"> <body data-md-showcase>
<x-livewire-material::scaffold :destinations="$destinations" label="Showcase" rail-width="17rem"> <x-livewire-material::scaffold :destinations="$destinations" label="Showcase" rail-width="17rem">
<x-slot:brand> <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> <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:brand>
<x-slot:top> <x-slot:top>
<x-livewire-material::app-bar variant="search"> <x-livewire-material::app-bar variant="search">
<x-slot:navigation> <x-slot:navigation>
<span class="medium:hidden"><x-livewire-material::button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="showcase-menu" /></span> <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> </x-slot:navigation>
{{-- The search looks through every section, example and component: the index is {{-- 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 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. --}} keep clear of <x-search>'s own (`results`, `open`), which the slot also sees. --}}
<div <div
class="w-full" data-md-showcase-search
x-data="{ x-data="{
query: '', query: '',
entries: null, entries: null,
@@ -111,42 +122,49 @@
x-on:focus.once="load()" x-on:focus.once="load()"
x-on:keydown.enter.prevent="go($event, matches[0])" x-on:keydown.enter.prevent="go($event, matches[0])"
> >
<p x-show="query.trim() === ''" class="px-4 py-3 type-body-md text-on-surface-variant"> <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="rounded-corner-xs bg-surface-container-highest px-1.5 type-label-md">/</kbd> to search from anywhere. 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>
<p x-cloak x-show="query.trim() !== '' && entries !== null && matches.length === 0" class="px-4 py-3 type-body-md text-on-surface-variant"> <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>. Nothing matches <span x-text="query.trim()"></span>.
</p> </p>
<template x-for="result in matches" x-bind:key="result.url + result.title"> <template x-for="result in matches" x-bind:key="result.url + result.title">
<a <a
x-bind:href="result.url" x-bind:href="result.url"
x-on:click="go($event, result)" x-on:click="go($event, result)"
class="state-layer focus-ring flex min-h-14 flex-col justify-center px-4 py-2 outline-none" class="md-state-layer md-focus-ring"
data-showcase-result data-showcase-result
data-md-showcase-search-result
> >
<span class="truncate type-body-lg text-on-surface" x-text="result.title"></span> <span class="md-type-body-lg md-ink md-truncate" x-text="result.title"></span>
<span class="truncate type-body-sm text-on-surface-variant" x-text="`${result.kind} · ${result.context}`"></span> <span class="md-type-body-sm md-ink-variant md-truncate" x-text="`${result.kind} · ${result.context}`"></span>
</a> </a>
</template> </template>
</x-livewire-material::search> </x-livewire-material::search>
</div> </div>
<x-slot:actions> <x-slot:actions>
{{-- With colour profiles, a preview of each on this page; nothing is stored. --}} <x-livewire-material::row gap="space100" align="center">
@if (($profiles = \NoNameWeb\LivewireMaterial\Support\Scheme::profiles()) !== []) {{-- With colour profiles, a preview of each on this page; nothing is stored. --}}
<x-livewire-material::menu label="Colour profile" position="bottom-end" data-test="showcase-profiles"> @if (($profiles = \NoNameWeb\LivewireMaterial\Support\Scheme::profiles()) !== [])
<x-slot:trigger> <x-livewire-material::menu label="Colour profile" position="bottom-end" data-test="showcase-profiles">
<x-livewire-material::button icon="palette" tooltip="Colour profile" /> <x-slot:trigger>
</x-slot:trigger> <x-livewire-material::button icon="palette" tooltip="Colour profile" />
</x-slot:trigger>
@foreach ($profiles as $name => $profile) @foreach ($profiles as $name => $profile)
<x-livewire-material::menu-item :label="$profile['label']" x-on:click="$store.theme.previewScheme('{{ $name }}')" data-scheme-preview="{{ $name }}" /> <x-livewire-material::menu-item :label="$profile['label']" x-on:click="$store.theme.previewScheme('{{ $name }}')" data-scheme-preview="{{ $name }}" />
@endforeach @endforeach
</x-livewire-material::menu> </x-livewire-material::menu>
@endif @endif
<span class="ms-2 me-3 max-expanded:hidden"><x-livewire-material::theme-toggle mode="picker" /></span> <x-livewire-material::stack as="span" hide-below="expanded">
<span class="expanded:hidden"><x-livewire-material::theme-toggle mode="cycle" /></span> <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-slot:actions>
</x-livewire-material::app-bar> </x-livewire-material::app-bar>
</x-slot:top> </x-slot:top>
+27 -20
View File
@@ -1,5 +1,6 @@
{{-- One section of the showcase on a page of its own, with the way on to the next. The page's {{-- One section of the showcase on a page of its own, with the way on to the next. The page's
heading names the section, so the section's own heading is only for a screen reader. --}} heading names the section, so the section's own heading is only for a screen reader
(`data-md-showcase-sections`'s own rule in resources/css/showcase.css). --}}
@extends('livewire-material::showcase.layout') @extends('livewire-material::showcase.layout')
@@ -11,26 +12,32 @@
@endphp @endphp
@section('content') @section('content')
<div class="mx-auto w-full max-w-6xl pt-4 pb-16"> <x-livewire-material::pane width="wide" data-md-showcase-page>
<header class="mb-6 space-y-1"> <x-livewire-material::stack gap="space300">
<p class="type-label-lg text-on-surface-variant">{{ $sections[$section]['group'] }}</p> <x-livewire-material::stack as="header" gap="space50">
<h1 class="type-headline-lg">{{ $sections[$section]['title'] }}</h1> <p class="md-type-label-lg md-ink-variant">{{ $sections[$section]['group'] }}</p>
</header> <h1 class="md-type-headline-lg">{{ $sections[$section]['title'] }}</h1>
</x-livewire-material::stack>
<div class="space-y-16 [&>section>h2]:sr-only"> <x-livewire-material::stack gap="space800" data-md-showcase-sections>
@include('livewire-material::showcase.sections.'.$section) @include('livewire-material::showcase.sections.'.$section)
<nav aria-label="Sections" class="flex flex-wrap items-center justify-between gap-4 border-t border-divider pt-6"> <x-livewire-material::stack gap="space300">
@if ($previous) <x-livewire-material::divider decorative />
<x-livewire-material::button variant="text" icon="arrow_back" :label="$sections[$previous]['title']" :link="route('livewire-material.section', $previous)" data-test="previous-section" />
@else
<x-livewire-material::button variant="text" icon="arrow_back" label="Overview" :link="route('livewire-material.showcase')" data-test="previous-section" />
@endif
@if ($next) <x-livewire-material::row as="nav" aria-label="Sections" justify="between" wrap gap="space200">
<x-livewire-material::button variant="tonal" icon-right="arrow_forward" :label="$sections[$next]['title']" :link="route('livewire-material.section', $next)" data-test="next-section" /> @if ($previous)
@endif <x-livewire-material::button variant="text" icon="arrow_back" :label="$sections[$previous]['title']" :link="route('livewire-material.section', $previous)" data-test="previous-section" />
</nav> @else
</div> <x-livewire-material::button variant="text" icon="arrow_back" label="Overview" :link="route('livewire-material.showcase')" data-test="previous-section" />
</div> @endif
@if ($next)
<x-livewire-material::button variant="tonal" icon-right="arrow_forward" :label="$sections[$next]['title']" :link="route('livewire-material.section', $next)" data-test="next-section" />
@endif
</x-livewire-material::row>
</x-livewire-material::stack>
</x-livewire-material::stack>
</x-livewire-material::stack>
</x-livewire-material::pane>
@endsection @endsection
+34 -19
View File
@@ -35,13 +35,20 @@
<x-livewire-material::theme-script /> <x-livewire-material::theme-script />
{{-- Kept only for the Livewire/Alpine runtime here this page has no Tailwind class of its
own. The next batches of step 38 leave it to the sections that still need it. It opens
the document's first @layer statement (Workbench package.css's header explains the
trick), which the link below must follow, not precede see layout.blade.php's comment. --}}
@vite(config('livewire-material.showcase.vite')) @vite(config('livewire-material.showcase.vite'))
@livewireStyles @livewireStyles
{{-- The showcase's own chrome: does not depend on the build above. --}}
<link rel="stylesheet" href="{{ \NoNameWeb\LivewireMaterial\Http\Controllers\ShowcaseAssetController::url() }}" />
</head> </head>
<body class="bg-surface font-sans text-on-surface antialiased"> <body data-md-showcase>
<x-livewire-material::scaffold :destinations="$destinations"> <x-livewire-material::scaffold :destinations="$destinations">
<x-slot:brand> <x-slot:brand>
<a href="{{ route('livewire-material.showcase') }}" class="block truncate rounded-corner-xs type-title-lg focus-ring">Livewire Material</a> <a href="{{ route('livewire-material.showcase') }}" class="md-type-title-lg md-truncate md-focus-ring" data-md-showcase-brand>Livewire Material</a>
</x-slot:brand> </x-slot:brand>
<x-slot:rail-header> <x-slot:rail-header>
@@ -58,27 +65,35 @@
</x-slot:actions> </x-slot:actions>
<x-slot:top> <x-slot:top>
<header class="sticky top-0 z-20 flex h-16 items-center gap-1 bg-surface px-1 pt-[var(--material-safe-top,env(safe-area-inset-top))] medium:px-4"> <x-livewire-material::app-bar :title="$current['title']">
<span class="medium:hidden"><x-livewire-material::button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="shell-menu" /></span> <x-slot:navigation>
<h1 class="min-w-0 flex-1 truncate px-3 type-title-lg medium:px-0">{{ $current['title'] }}</h1> <x-livewire-material::stack as="span" hide-from="medium">
<x-livewire-material::button icon="search" tooltip="Search" /> <x-livewire-material::button icon="menu" tooltip="Open navigation" x-data x-on:click="$store.rail.show()" data-test="shell-menu" />
</header> </x-livewire-material::stack>
</x-slot:navigation>
<x-slot:actions>
<x-livewire-material::button icon="search" tooltip="Search" />
</x-slot:actions>
</x-livewire-material::app-bar>
</x-slot:top> </x-slot:top>
<div class="mx-auto w-full max-w-5xl space-y-4 pb-6"> <x-livewire-material::pane width="medium" data-md-showcase-page>
<p class="type-body-md text-on-surface-variant" data-test="shell-page">This is the {{ strtolower($current['title']) }} page.</p> <x-livewire-material::stack gap="space200">
<p class="md-type-body-md md-ink-variant" data-test="shell-page">This is the {{ strtolower($current['title']) }} page.</p>
<x-livewire-material::list segmented> <x-livewire-material::list segmented>
@foreach (range(1, 14) as $index) @foreach (range(1, 14) as $index)
<x-livewire-material::list-item :title="$current['title'].' item '.$index" description="A line of supporting text for this item" :icon="$current['icon']" trailing="{{ $index }}h" wire:key="item-{{ $index }}" /> <x-livewire-material::list-item :title="$current['title'].' item '.$index" description="A line of supporting text for this item" :icon="$current['icon']" trailing="{{ $index }}h" wire:key="item-{{ $index }}" />
@endforeach @endforeach
</x-livewire-material::list> </x-livewire-material::list>
<div class="flex flex-wrap gap-2"> <x-livewire-material::row gap="space100" wrap>
<x-livewire-material::button label="Show a toast" variant="tonal" x-data x-on:click="materialToast('Moved to archive', { action: { label: 'Undo', handler: () => {} } })" /> <x-livewire-material::button label="Show a toast" variant="tonal" x-data x-on:click="materialToast('Moved to archive', { action: { label: 'Undo', handler: () => {} } })" />
<x-livewire-material::button label="Back to the showcase" link="{{ route('livewire-material.section', 'navigation') }}" no-wire-navigate /> <x-livewire-material::button label="Back to the showcase" link="{{ route('livewire-material.section', 'navigation') }}" no-wire-navigate />
</div> </x-livewire-material::row>
</div> </x-livewire-material::stack>
</x-livewire-material::pane>
</x-livewire-material::scaffold> </x-livewire-material::scaffold>
@livewireScripts @livewireScripts