Rewrite the showcase's Text fields and selection section without Tailwind

Plan step 38: every two-column layout moves onto <x-grid> with
<x-stack> columns; the checkbox group's card, its legend and the two
search demos' bespoke geometry (an avatar chip, the compact
search-icon-button bar) move to data-md-showcase-* hooks. The two
small demo controls and the "Narrower" field's override move to
.showcase-w-sm/-xs/-narrow — plain, unlayered classes, since
<x-input> and <x-datepicker> forward only class and style to the
element that needs sizing, never a data attribute. "Narrower"'s hint
is reworded from "A max-w-* class" to "An unlayered rule from the
call site wins", since that is now the mechanism being shown.

FieldsTest's Chromium selectors run against an isolated fieldProbe
fixture, not this page, so nothing there is at risk; #fields and every
id (showcase-error, showcase-error-filled, showcase-audience,
showcase-radio-theme) is unchanged.

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 04:53:58 +02:00
co-authored by Claude Sonnet 5
parent dfb5997474
commit 0eb0343008
2 changed files with 129 additions and 71 deletions
+60 -2
View File
@@ -29,8 +29,9 @@
* a small circular avatar and a compact search bar, and the two ink colours a menu item's `icon-class`
* needs beyond the fixed `md-ink-*` set. A handful of call sites (`<x-input>`, `<x-datepicker>`,
* `<x-slider>`) forward only `class` and `style` to the element that needs sizing, never a data
* attribute, so those few rules below are `.md-showcase-*` classes rather than
* `data-md-showcase-*` hooks — the same "caller's class" mechanism any application would reach for.
* attribute, so a few widths below are plain `.showcase-*` classes instead of `data-md-showcase-*`
* hooks — the same "caller's class" mechanism any application would reach for, deliberately not
* named `md-*` since they are not part of the fixed set text.css declares.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@@ -397,4 +398,61 @@
width: 100%;
max-width: 384px;
}
/* Text fields: the legend above a checkbox grid, a bordered demo card at two widths (a form,
a checkbox group), a small circular avatar for a search bar's trailing slot, and the
compact search-icon-button bar M3 uses as a secondary entry point. */
[data-md-showcase-legend] {
margin-bottom: var(--md-sys-measurement-space200);
}
[data-md-showcase-demo-card] {
width: 100%;
}
[data-md-showcase-demo-card][data-md-showcase-size='md'] {
max-width: 576px;
}
[data-md-showcase-demo-card][data-md-showcase-size='lg'] {
max-width: 672px;
}
[data-md-showcase-avatar] {
display: grid;
place-items: center;
width: var(--md-sys-measurement-space400);
height: var(--md-sys-measurement-space400);
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
}
[data-md-showcase-search-bar] {
display: flex;
align-items: center;
gap: var(--md-sys-measurement-space100);
border-radius: var(--md-sys-shape-corner-full);
background-color: var(--md-sys-color-surface-container-high);
padding: var(--md-sys-measurement-space50) var(--md-sys-measurement-space100);
}
[data-md-showcase-search-bar-title] {
flex: 1 1 auto;
}
}
/* A handful of call sites (<x-input>, <x-datepicker>, <x-slider>) forward only `class` and
`style` to the element that needs sizing, so these few widths are the caller's own classes
rather than data-md-showcase-* hooks — see the file header. */
.showcase-w-sm {
width: 192px;
}
.showcase-w-xs {
width: 144px;
}
.showcase-w-narrow {
max-width: 320px;
}
@@ -1,153 +1,153 @@
@php
$examples = [
'Outlined and filled text fields' => <<<'BLADE'
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-4">
<x-livewire-material::grid :columns="['compact' => 1, 'medium' => 2]" gap="space300">
<x-livewire-material::stack gap="space200">
<x-input label="Share name" hint="Recipients see this name" />
<x-input label="Recipient" icon="mail" type="email" placeholder="name@example.com" clearable value="anna@example.com" />
<x-input label="Price" prefix="CHF" suffix="per month" type="number" value="12" />
<x-input label="Share link" value="https://sealshare.test/s/8f3k2" readonly copyable />
<x-password label="Password" hint="At least 12 characters" />
<x-input label="Disabled" value="Not editable" disabled />
</div>
</x-livewire-material::stack>
<div class="grid content-start gap-4">
<x-livewire-material::stack gap="space200">
<x-input variant="filled" label="Share name" hint="Recipients see this name" />
<x-input variant="filled" label="Recipient" icon="mail" type="email" placeholder="name@example.com" clearable value="anna@example.com" />
<x-input variant="filled" label="Price" prefix="CHF" suffix="per month" type="number" value="12" />
<x-input variant="filled" label="Search" icon="search" icon-right="mic" />
<x-password variant="filled" label="Password" hint="At least 12 characters" />
<x-input variant="filled" label="Disabled" value="Not editable" disabled />
</div>
</div>
</x-livewire-material::stack>
</x-livewire-material::grid>
BLADE,
'Errors, required and sizes' => <<<'BLADE'
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-4">
<x-livewire-material::grid :columns="['compact' => 1, 'medium' => 2]" gap="space300">
<x-livewire-material::stack gap="space200">
<x-input label="Email" required />
<x-field id="showcase-error" label="Expiry" :messages="['Choose a date in the future.']">
<input id="showcase-error" data-md-field-control value="2020-01-01" aria-invalid="true" />
</x-field>
</div>
</x-livewire-material::stack>
<div class="grid content-start gap-4">
<x-livewire-material::stack gap="space200">
<x-field id="showcase-error-filled" variant="filled" label="Expiry" :messages="['Choose a date in the future.']">
<input id="showcase-error-filled" data-md-field-control value="2020-01-01" aria-invalid="true" />
</x-field>
<div class="flex flex-wrap items-center gap-3">
<x-input size="sm" icon="search" placeholder="Filter" aria-label="Filter" class="w-48" />
<x-input size="xs" placeholder="Find" aria-label="Find" class="w-36" />
</div>
</div>
</div>
<x-livewire-material::row gap="space125" wrap align="center">
<x-input size="sm" icon="search" placeholder="Filter" aria-label="Filter" class="showcase-w-sm" />
<x-input size="xs" placeholder="Find" aria-label="Find" class="showcase-w-xs" />
</x-livewire-material::row>
</x-livewire-material::stack>
</x-livewire-material::grid>
BLADE,
'Width at medium and above' => <<<'BLADE'
<div class="w-full space-y-4">
<x-livewire-material::stack gap="space200">
{{-- M3: never let a text field span the full width of a large screen. From medium it stops at 40rem. --}}
<x-input label="Bounded, the default" hint="No wider than 40rem from medium, however wide the pane is" />
<x-input label="Narrower" class="max-w-xs" hint="A max-w-* class from the call site wins" />
<x-input label="Narrower" class="showcase-w-narrow" hint="An unlayered rule from the call site wins" />
<x-input label="The width of the pane" full icon="search" hint="`full` takes the bound off — for a search row or an editor" />
</div>
</x-livewire-material::stack>
BLADE,
'Character counter' => <<<'BLADE'
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-4">
<x-livewire-material::grid :columns="['compact' => 1, 'medium' => 2]" gap="space300">
<x-livewire-material::stack gap="space200">
<x-input label="Share name" maxlength="40" counter hint="Recipients see this name" />
<x-input variant="filled" label="Subject" maxlength="60" counter />
</div>
</x-livewire-material::stack>
<x-textarea label="Message for recipients" maxlength="180" counter rows="3" hint="Sent with the link" />
</div>
</x-livewire-material::grid>
BLADE,
'Textarea and select' => <<<'BLADE'
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-4">
<x-livewire-material::grid :columns="['compact' => 1, 'medium' => 2]" gap="space300">
<x-livewire-material::stack gap="space200">
<x-textarea label="Message" hint="Grows as you type, up to six lines" rows="2" max-rows="6" />
<x-select label="Expires after" :options="[['id' => 1, 'name' => '1 hour'], ['id' => 24, 'name' => '1 day'], ['id' => 168, 'name' => '7 days'], ['id' => 720, 'name' => '30 days', 'disabled' => true]]" />
<x-file label="Attachments" multiple hint="Up to 2 GB" />
</div>
</x-livewire-material::stack>
<div class="grid content-start gap-4">
<x-livewire-material::stack gap="space200">
<x-textarea variant="filled" label="Message" hint="Grows as you type, up to six lines" rows="2" max-rows="6" />
<x-select variant="filled" label="Expires after" icon="schedule" placeholder="Choose…" :options="[['id' => 1, 'name' => '1 hour'], ['id' => 24, 'name' => '1 day'], ['id' => 168, 'name' => '7 days']]" />
<x-file variant="filled" label="Attachments" multiple />
</div>
</div>
</x-livewire-material::stack>
</x-livewire-material::grid>
BLADE,
'Checkboxes' => <<<'BLADE'
<div class="grid w-full gap-6 expanded:grid-cols-3">
<div class="grid content-start gap-4">
<x-livewire-material::grid :columns="['compact' => 1, 'expanded' => 3]" gap="space300">
<x-livewire-material::stack gap="space200">
<x-checkbox label="Notify me on download" checked />
<x-checkbox label="Select all files" indeterminate />
<x-checkbox label="Burn after reading" hint="The share is deleted after its first download" />
</div>
<div class="grid content-start gap-4">
</x-livewire-material::stack>
<x-livewire-material::stack gap="space200">
<x-checkbox label="Disabled" disabled />
<x-checkbox label="Disabled and ticked" checked disabled />
<x-checkbox label="Disabled, partly ticked" indeterminate disabled />
</div>
<div class="grid content-start gap-4">
</x-livewire-material::stack>
<x-livewire-material::stack gap="space200">
<x-checkbox label="Show in the dashboard" right />
<x-checkbox aria-label="Unlabelled" />
</div>
</div>
</x-livewire-material::stack>
</x-livewire-material::grid>
BLADE,
'A checkbox group at expanded' => <<<'BLADE'
{{-- M3: from expanded (840px), gather related checkboxes into a contained region instead of one long column. --}}
<x-card variant="outlined" class="w-full max-w-2xl">
<x-card variant="outlined" data-md-showcase-demo-card data-md-showcase-size="lg">
<fieldset>
<legend class="mb-4 type-title-sm text-on-surface">Tell me about</legend>
<legend class="md-type-title-sm md-ink" data-md-showcase-legend>Tell me about</legend>
<div class="grid gap-4 expanded:grid-cols-2">
<x-livewire-material::grid :columns="['compact' => 1, 'expanded' => 2]" gap="space200">
<x-checkbox label="Downloads" name="notify[]" value="downloads" checked />
<x-checkbox label="Comments" name="notify[]" value="comments" />
<x-checkbox label="Expiring shares" name="notify[]" value="expiring" checked />
<x-checkbox label="Failed uploads" name="notify[]" value="failed" />
<x-checkbox label="New team members" name="notify[]" value="members" />
<x-checkbox label="Weekly summary" name="notify[]" value="summary" />
</div>
</x-livewire-material::grid>
</fieldset>
</x-card>
BLADE,
'Radio buttons' => <<<'BLADE'
<div class="grid w-full gap-6 medium:grid-cols-2">
<x-livewire-material::grid :columns="['compact' => 1, 'medium' => 2]" gap="space300">
<x-radio label="Who can open the link" name="showcase-audience" value="password" :options="[['id' => 'anyone', 'name' => 'Anyone with the link'], ['id' => 'password', 'name' => 'Anyone with the password', 'hint' => 'Share the password separately'], ['id' => 'team', 'name' => 'My team only', 'disabled' => true]]" />
<x-radio label="Theme" name="showcase-radio-theme" value="system" inline :options="[['id' => 'light', 'name' => 'Light'], ['id' => 'dark', 'name' => 'Dark'], ['id' => 'system', 'name' => 'System']]" hint="Inline from sm" />
</div>
</x-livewire-material::grid>
BLADE,
'Switches' => <<<'BLADE'
<div class="grid w-full gap-6 expanded:grid-cols-3">
<div class="grid content-start gap-4">
<x-livewire-material::grid :columns="['compact' => 1, 'expanded' => 3]" gap="space300">
<x-livewire-material::stack gap="space200">
<x-toggle label="Email notifications" checked />
<x-toggle label="Weekly summary" />
</div>
<div class="grid content-start gap-4">
</x-livewire-material::stack>
<x-livewire-material::stack gap="space200">
<x-toggle label="With icons" icons checked />
<x-toggle label="With icons, off" icons />
<x-toggle label="Only the check" icons="selected" checked />
</div>
<div class="grid content-start gap-4">
</x-livewire-material::stack>
<x-livewire-material::stack gap="space200">
<x-toggle label="Disabled" disabled />
<x-toggle label="Disabled, on" checked disabled icons />
<x-toggle label="Public link" hint="Anyone with the link can open it" right />
</div>
</div>
</x-livewire-material::stack>
</x-livewire-material::grid>
BLADE,
'Choices' => <<<'BLADE'
<div class="grid w-full gap-6 medium:grid-cols-2">
<div class="grid content-start gap-6">
<x-livewire-material::grid :columns="['compact' => 1, 'medium' => 2]" gap="space300">
<x-livewire-material::stack gap="space300">
<x-choices label="Days you are free" hint="Filter chips: any number" :value="[1, 3]" :options="[['id' => 1, 'name' => 'Mon'], ['id' => 2, 'name' => 'Tue'], ['id' => 3, 'name' => 'Wed'], ['id' => 4, 'name' => 'Thu'], ['id' => 5, 'name' => 'Fri'], ['id' => 6, 'name' => 'Sat', 'disabled' => true]]" />
<x-choices label="Expires after" single :value="24" :options="[['id' => 1, 'name' => '1 hour'], ['id' => 24, 'name' => '1 day'], ['id' => 168, 'name' => '7 days']]" />
</div>
</x-livewire-material::stack>
<div class="grid content-start gap-4">
<x-livewire-material::stack gap="space200">
<x-choices label="Time zone" searchable icon="public" value="Europe/Zurich" :options="collect(timezone_identifiers_list())->map(fn ($zone) => ['id' => $zone, 'name' => str_replace('_', ' ', $zone)])->all()" />
<x-choices label="Language" searchable variant="filled" placeholder="Type a language" :options="[['id' => 'de', 'name' => 'Deutsch'], ['id' => 'en', 'name' => 'English'], ['id' => 'fr', 'name' => 'Français'], ['id' => 'it', 'name' => 'Italiano'], ['id' => 'rm', 'name' => 'Rumantsch', 'disabled' => true]]" />
</div>
</div>
</x-livewire-material::stack>
</x-livewire-material::grid>
BLADE,
'Search' => <<<'BLADE'
<div class="grid w-full gap-6 medium:grid-cols-2">
<x-livewire-material::grid :columns="['compact' => 1, 'medium' => 2]" gap="space300">
<div x-data="{ query: '' }">
<x-search x-model="query" placeholder="Search shares">
<x-list>
@@ -156,7 +156,7 @@
<x-list-item title="design-review.mp4" description="3.4 GB · 7 days left" icon="movie" link="#fields" x-show="'design-review.mp4'.includes(query.toLowerCase())" />
</x-list>
<x-slot:trailing>
<span class="grid size-8 place-items-center rounded-corner-full bg-primary-container type-label-lg text-on-primary-container">AR</span>
<span class="md-type-label-lg" data-md-showcase-avatar>AR</span>
</x-slot:trailing>
</x-search>
</div>
@@ -167,10 +167,10 @@
<x-slot:empty>Type to search your settings.</x-slot:empty>
</x-search>
</div>
</div>
</x-livewire-material::grid>
BLADE,
'Search entry points and suggestions' => <<<'BLADE'
<div class="grid w-full gap-6 medium:grid-cols-2">
<x-livewire-material::grid :columns="['compact' => 1, 'medium' => 2]" gap="space300">
<div x-data="{ query: '' }">
<x-search x-model="query" placeholder="Search shares">
<x-slot:suggestions>
@@ -189,9 +189,9 @@
</div>
{{-- M3's search icon button: search as a secondary action, expanding into the full-screen view. --}}
<div class="flex items-center gap-2 rounded-corner-full bg-surface-container-high px-2 py-1">
<div data-md-showcase-search-bar>
<x-button icon="menu" aria-label="Open the menu" />
<span class="grow type-title-md text-on-surface">Shares</span>
<span class="md-type-title-md md-ink" data-md-showcase-search-bar-title>Shares</span>
<x-search trigger="icon" label="Search shares">
<x-slot:suggestions>
<x-list>
@@ -201,10 +201,10 @@
<x-slot:empty>No shares match.</x-slot:empty>
</x-search>
</div>
</div>
</x-livewire-material::grid>
BLADE,
'A form' => <<<'BLADE'
<x-card variant="outlined" class="w-full max-w-xl">
<x-card variant="outlined" data-md-showcase-demo-card data-md-showcase-size="md">
<x-form x-on:submit.prevent="materialToast('Share created', { type: 'success' })" separator>
<x-input label="Share name" required />
<x-textarea label="Message for recipients" />
@@ -221,10 +221,10 @@
];
@endphp
<section id="fields" class="scroll-mt-24 space-y-6">
<h2 class="type-headline-md">Text fields and selection</h2>
<x-livewire-material::stack as="section" id="fields" gap="space300">
<h2 class="md-type-headline-md">Text fields and selection</h2>
<p class="max-w-3xl type-body-md text-on-surface-variant">
<p class="md-type-body-md md-ink-variant">
<code>&lt;x-input&gt;</code>, <code>&lt;x-password&gt;</code>, <code>&lt;x-textarea&gt;</code>, <code>&lt;x-select&gt;</code>, <code>&lt;x-file&gt;</code>
(all on <code>&lt;x-field&gt;</code>, outlined or filled), <code>&lt;x-checkbox&gt;</code>, <code>&lt;x-radio&gt;</code>, <code>&lt;x-toggle&gt;</code>, <code>&lt;x-choices&gt;</code>, <code>&lt;x-search&gt;</code> and <code>&lt;x-form&gt;</code>.
</p>
@@ -232,4 +232,4 @@
@foreach ($examples as $title => $code)
<x-showcase::example :$title :$code />
@endforeach
</section>
</x-livewire-material::stack>