Write the showcase examples without showcase-only hooks

Plan step 38 review: the component sections' code samples, which an
application copies, carried 60 data-md-showcase-* hooks that only
showcase.css draws (a sized loading indicator, the plain badge's own
colours, a bordered bar frame, the navigation demos' frames, a table's
scroll wrapper...), so a copied sample did nothing in the application.
They now write what an application has: <x-surface outlined corner>
for a frame and a line, <x-row> for the FAB menu's corner, and an
inline style from the tokens for a size, a scroll box or a colour,
as the Containment batch already did. The side sheet demo takes
align="stretch", which holds in both of <x-row stack-below>'s modes,
instead of a hook that undid align="start" once stacked. The menu
icon colours go through icon-class with two unlayered application-like
classes, which the sample's comment names, and the fields' and date
picker's widths are inline styles now that style reaches their roots.
The radio hint said "from sm"; it is medium.

showcase.css keeps only the frame and the foundation specimens; its
header describes the file instead of the batches that wrote it. The
tests reject a showcase hook or a class outside md-* and the named
application classes in any example, a showcase.css selector outside
its own hooks, and a hook no showcase view renders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-15 06:21:18 +02:00
co-authored by Claude Opus 5
parent cd47aa4b0f
commit ebdcdb82b5
15 changed files with 152 additions and 374 deletions
+32 -286
View File
@@ -1,55 +1,28 @@
/*
* The showcase's own chrome: what its frame needs beyond the layout components, the text classes
* and the shared interaction classes (plan step 38) — a brand wordmark, the search box's own
* geometry, a small icon avatar, an example's card and its code sample. Everything else the frame
* draws is a layout component with its props, or a `md-*` text class; this file is only what those
* two do not cover.
* The showcase's own chrome: what its pages draw beyond the layout components, the `md-*` text
* classes and the shared interaction classes (plan step 38) — the rail's wordmark, the search box
* and its results, a keyboard key in running text, an example's card and its code sample, and the
* foundation pages' specimens: a colour swatch, a type-scale row, a corner swatch, an elevation
* tile, a motion track and its dot, an icon search result and the Layout page's breakpoint cards.
*
* In `material.components`: the showcase is one more thing drawn over the foundation and the
* layout components, exactly where `components/error-page.css` sits for the same reason (a whole
* page's own chrome, not a reusable package component, but still something an application's own
* unlayered CSS should outrank without a specificity fight). Keyed on `data-md-showcase-*` hooks,
* never a bare element selector, since this file is always bundled together with `all.css`
* (`ShowcaseAssetController`) and must not restyle anything outside the showcase's own pages.
* Only the showcase's own pages use it. The examples do not: an example's code is what an
* application copies, so it writes only what an application has — the components and their props,
* the layout components, the text classes, and an inline `style` from the `--md-sys-*` tokens where
* a demo needs a size, a frame or a colour of its own.
*
* No imports: batch 1 (the frame — layout, index, section, shell, the example component) uses no
* selector this file needs to draw over, only tokens already in `all.css`, which the bundle always
* carries ahead of this file. A later batch that needs a component's own hook imports its
* stylesheet here, as every other package file does.
* In `material.components`: the showcase is one more page drawn over the foundation and the layout
* components, where `components/error-page.css` sits for the same reason, and an application's
* unlayered CSS still outranks it. Keyed on `data-md-showcase-*` hooks, never a bare element
* selector, since `ShowcaseAssetController` bundles this file with `all.css` and it must restyle
* nothing outside those hooks. It imports nothing: every token and layout rule it builds on is in
* `all.css`, ahead of it in the bundle. Values are tokens, apart from a specimen's own size; its
* media queries are M3's breakpoints, in px.
*
* Batch 2 (the foundation and layout sections) adds the swatches, specimens and demo boxes those
* pages draw beyond a layout component and a `md-*` text class: a colour swatch (its own fill is
* the role it shows, so that one declaration is the caller's inline `style`, never a class), a
* type-scale row, a shape swatch, an elevation tile, a motion track and its moving dot, an icon
* search result and the Layout page's breakpoint cards.
*
* Batch 3 (the component sections) adds each section's own demo geometry: a sized loading
* indicator, a bounded box for the FAB menu, a narrow chip row that forces its own scroll
* affordance, a scrolling table wrapper, the legend above a checkbox grid, a bordered demo card,
* 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.
*
* Batch 4 (the last: containment, carousel, bars, navigation, pages) adds the standard side
* sheet demo's own breakpoint (a `<x-row>` beats its own `stack-below` alignment back to `stretch`
* once collapsed to a column, which an `align` the row mode needs would otherwise leave
* shrink-wrapped), a bordered app-bar/toolbar frame (plain, `visible` for the search bar whose
* docked suggestions must not clip, `rounded` for the large-screen docked toolbar), a narrow
* scrollable-tabs box, and the navigation bar and rail demo frames (`bar`, `bar-wide` inside its
* own horizontal-scroll wrapper, `rail`, `rail-wide`) with their filler "page" and content panes.
* Every per-item carousel colour stays the caller's own inline `style`, the same mechanism as the
* colour swatch above.
*
* Of the three call sites batch 3 found, two still cannot take a `style` for the width their
* demos need: `<x-input>` forwards `style` to the raw `<input>`, not the field wrapper that is
* actually sized, and `<x-datepicker>` forwards neither — its root takes only `class`, `wire:key`
* and `x-model`. `.showcase-w-sm`, `.showcase-w-xs` stay plain unlayered classes for that reason,
* 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. `.showcase-w-narrow` stays
* one too, but to make a point on purpose (fields.blade.php's "Narrower" input): a small unlayered
* rule from an application's own stylesheet beats the package's own layered default, so it has to
* be a real class, not an inline `style` (which would win trivially and prove nothing). `<x-slider>`
* does forward `style` to its root, so its vertical demo's height became one
* (resources/views/showcase/sections/sliders.blade.php), and `.showcase-slider-vertical` is gone.
* Two kinds of rule sit outside the layers on purpose, as an application's own classes would:
* `.showcase-w-narrow` proves the point its example makes (Text fields, "Narrower": an unlayered
* rule from the call site beats the package's layered default, which an inline `style` would win
* regardless of layers, proving nothing), and `.showcase-ink-*` are the colours the Menus example
* passes to `icon-class`, the application's own class that prop takes.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@@ -331,244 +304,6 @@
color: var(--md-sys-color-on-primary-container);
}
}
/* Buttons: the loading indicator sized and coloured beyond its 48px primary default, and the
bounded box a FAB menu opens into (M3 anchors it to a scaffold's own corner; this is only
room enough to show the menu open). */
[data-md-showcase-loading='lg'] {
width: 96px;
height: 96px;
color: var(--md-sys-color-tertiary);
}
[data-md-showcase-loading='sm'] {
width: 32px;
height: 32px;
}
[data-md-showcase-fab-menu-demo] {
display: flex;
align-items: flex-end;
justify-content: flex-end;
height: 288px;
}
/* Menus: an item's icon in a colour beyond the fixed md-ink-* set (icon-class covers the
roles that set already has, md-ink-info among them). */
[data-md-showcase-icon-color='tertiary'] [data-md-icon] {
color: var(--md-sys-color-tertiary);
}
[data-md-showcase-icon-color='secondary'] [data-md-icon] {
color: var(--md-sys-color-secondary);
}
/* Communication: the "plain" badge's own colours (that variant draws none, by design, so the
caller paints it), a focus ring's corner on a plain span standing in for a control, and the
illustration's two decorative colours beyond currentColor. */
[data-md-showcase-badge-plain] {
background-color: var(--md-sys-color-primary-fixed);
color: var(--md-sys-color-on-primary-fixed);
}
[data-md-showcase-focus-demo] {
border-radius: var(--md-sys-shape-corner-xs);
}
[data-md-showcase-illustration='container'] {
fill: var(--md-sys-color-primary-container);
}
[data-md-showcase-illustration='accent'] {
fill: var(--md-sys-color-tertiary);
}
[data-md-showcase-illustration='on-accent'] {
fill: var(--md-sys-color-on-tertiary);
}
/* Progress: one circular indicator drawn larger than its 48px default, to show the ring
scales with its box. */
[data-md-showcase-progress-lg] {
width: 96px;
height: 96px;
}
/* Chips: a row narrow enough that its set has to scroll, to show the fade and the arrow
buttons M3 asks an overflowing row for. */
[data-md-showcase-chip-scroll] {
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;
}
/* Data: the table's own horizontal scroll stays the caller's, as table.css's header says. */
[data-md-showcase-table-scroll] {
width: 100%;
overflow-x: auto;
}
/* Containment: the standard side sheet demo is a row from `expanded` (840px), M3's own
condition for a co-planar sheet, and a column below it — `<x-row>`'s own `stack-below`
stacks it there, but leaves the `align="start"` the row layout needs (so the sheet does not
stretch to the text column's height) in place, which would otherwise shrink-wrap both
columns instead of stacking them full width. */
@media (width < 840px) {
[data-md-showcase-side-sheet-demo] {
align-items: stretch;
}
}
/* Carousel: an item's own colour is the caller's inline `style` (its role pair is chosen per
item), the same mechanism as the colour swatch above; this only sizes and centres it. */
[data-md-showcase-carousel-swatch] {
display: grid;
place-items: center;
width: 100%;
height: 100%;
}
/* Bars: a bordered frame around one app-bar or docked-toolbar demo, matching the rounding a
real scaffold would clip its bar to. `visible` lets the search bar's docked suggestions
overflow it instead of clipping; `rounded` is the large-screen docked toolbar, which has no
edge of its own and only gains its padding from `expanded` (840px), M3's own condition for
the rounded (rather than square) docked toolbar. */
[data-md-showcase-bar-frame] {
width: 100%;
overflow: hidden;
border-radius: var(--md-sys-shape-corner-lg);
border: 1px solid var(--md-sys-color-outline-variant);
}
[data-md-showcase-bar-frame='visible'] {
overflow: visible;
}
[data-md-showcase-bar-frame='rounded'] {
border: 0;
background-color: var(--md-sys-color-surface);
}
@media (width >= 840px) {
[data-md-showcase-bar-frame='rounded'] {
padding: var(--md-sys-measurement-space200);
}
}
/* Bars: the scrollable-tabs demo needs a width narrow enough to force its own scroll. */
[data-md-showcase-tabs-narrow] {
max-width: 384px;
}
/* Navigation: a bounded frame around one navigation-bar or navigation-rail demo. `bar` and
`bar-wide` stack the bar under a "the page" filler the way a scaffold does; `bar-wide` drops
its own edge because its horizontal-scroll wrapper (`data-md-showcase-nav-scroll`) already
draws one, and is wide enough (600px) that a bar showing icon beside label never wraps.
`rail` and `rail-wide` sit the rail beside a content pane the way a scaffold does; `rail-wide`
is full width, for the two examples with real body text beside the rail. */
[data-md-showcase-nav-demo='bar'] {
display: flex;
flex-direction: column;
height: 224px;
overflow: hidden;
border-radius: var(--md-sys-shape-corner-lg);
border: 1px solid var(--md-sys-color-outline-variant);
background-color: var(--md-sys-color-surface);
}
[data-md-showcase-nav-demo='bar-wide'] {
display: flex;
flex-direction: column;
height: 224px;
min-width: 600px;
background-color: var(--md-sys-color-surface);
}
[data-md-showcase-nav-demo='rail'] {
display: flex;
height: 576px;
overflow: hidden;
border-radius: var(--md-sys-shape-corner-lg);
border: 1px solid var(--md-sys-color-outline-variant);
}
[data-md-showcase-nav-demo='rail-wide'] {
display: flex;
width: 100%;
height: 480px;
overflow: hidden;
border-radius: var(--md-sys-shape-corner-lg);
border: 1px solid var(--md-sys-color-outline-variant);
}
[data-md-showcase-nav-scroll] {
overflow-x: auto;
border-radius: var(--md-sys-shape-corner-lg);
border: 1px solid var(--md-sys-color-outline-variant);
}
[data-md-showcase-nav-page] {
flex: 1 1 auto;
padding: var(--md-sys-measurement-space200);
}
[data-md-showcase-nav-content] {
flex: 1 1 auto;
min-width: 0;
background-color: var(--md-sys-color-surface-container-low);
padding: var(--md-sys-measurement-space300);
}
}
/* Two call sites still cannot take a `style` for the width their demo needs — see the file
header — so these stay plain unlayered classes. */
.showcase-w-sm {
width: 192px;
}
.showcase-w-xs {
width: 144px;
}
/* Deliberately unlayered, to make the point fields.blade.php's hint names: an application's own
@@ -577,3 +312,14 @@
.showcase-w-narrow {
max-width: 320px;
}
/* An application's own icon colours, for menus.blade.php's `icon-class` example: unlayered, as the
menu item's header says a caller's class is, so it paints the icon over the item's ink (and a
disabled item's icon still stays disabled). */
.showcase-ink-tertiary {
color: var(--md-sys-color-tertiary);
}
.showcase-ink-secondary {
color: var(--md-sys-color-secondary);
}
@@ -2,7 +2,7 @@
$examples = [
'Top app bars' => <<<'BLADE'
<x-stack gap="space200" style="width: 100%;">
<div data-md-showcase-bar-frame>
<x-surface corner="lg" outlined style="overflow: hidden;">
<x-app-bar title="Shares" :sticky="false">
<x-slot:navigation><x-button icon="menu" tooltip="Open navigation" /></x-slot:navigation>
<x-slot:actions>
@@ -10,10 +10,10 @@
<x-button icon="filter_list" tooltip="Filter" />
</x-slot:actions>
</x-app-bar>
</div>
</x-surface>
{{-- Resize the window: two icon buttons below 600px, four from there, the rest in "More options". --}}
<div data-md-showcase-bar-frame>
<x-surface corner="lg" outlined style="overflow: hidden;">
<x-app-bar title="holiday-photos" :sticky="false" :actions="[
['label' => 'Share', 'icon' => 'share'],
['label' => 'Download', 'icon' => 'download'],
@@ -23,37 +23,37 @@
]">
<x-slot:navigation><x-button icon="arrow_back" tooltip="Back" /></x-slot:navigation>
</x-app-bar>
</div>
</x-surface>
<div data-md-showcase-bar-frame>
<x-surface corner="lg" outlined style="overflow: hidden;">
<x-app-bar variant="center" title="holiday-photos.zip" subtitle="248 MB · expires in 3 days" :sticky="false">
<x-slot:navigation><x-button icon="arrow_back" tooltip="Back" /></x-slot:navigation>
<x-slot:actions><x-button icon="share" tooltip="Share" /></x-slot:actions>
</x-app-bar>
</div>
</x-surface>
<div data-md-showcase-bar-frame>
<x-surface corner="lg" outlined style="overflow: hidden;">
<x-app-bar variant="medium" title="Recipients" subtitle="3 people" :sticky="false">
<x-slot:navigation><x-button icon="arrow_back" tooltip="Back" /></x-slot:navigation>
<x-slot:actions><x-button icon="person_add" tooltip="Add recipient" /></x-slot:actions>
</x-app-bar>
</div>
</x-surface>
<div data-md-showcase-bar-frame>
<x-surface corner="lg" outlined style="overflow: hidden;">
<x-app-bar variant="large" title="Settings" :sticky="false">
<x-slot:navigation><x-button icon="arrow_back" tooltip="Back" /></x-slot:navigation>
</x-app-bar>
</div>
</x-surface>
<div data-md-showcase-bar-frame="visible">
<x-surface corner="lg" outlined>
<x-app-bar variant="search" :sticky="false" style="border-radius: var(--md-sys-shape-corner-lg);">
<x-slot:navigation><x-button icon="menu" tooltip="Open navigation" /></x-slot:navigation>
<x-search placeholder="Search shares" docked />
<x-slot:actions>
<span data-md-showcase-avatar class="md-type-label-lg">AR</span>
<span class="md-type-label-lg" style="display: grid; place-items: center; width: 32px; height: 32px; border-radius: var(--md-sys-shape-corner-full); background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container);">AR</span>
</x-slot:actions>
</x-app-bar>
</div>
</x-surface>
</x-stack>
BLADE,
'Toolbars' => <<<'BLADE'
@@ -78,16 +78,16 @@
<x-button icon="zoom_in" tooltip-right="Zoom in" />
</x-toolbar>
<div data-md-showcase-bar-frame>
<x-surface corner="lg" outlined style="width: 100%; overflow: hidden;">
<x-toolbar variant="docked" label="Selection">
<x-button icon="download" tooltip="Download" />
<x-button icon="share" tooltip="Share" />
<x-button icon="drive_file_move" tooltip="Move" />
<x-button icon="delete" tooltip="Delete" />
</x-toolbar>
</div>
</x-surface>
<div data-md-showcase-bar-frame>
<x-surface corner="lg" outlined style="width: 100%; overflow: hidden;">
<x-toolbar variant="docked" label="Editing">
<x-button icon="check_box" tooltip="Select" />
<x-button icon="brush" tooltip="Draw" />
@@ -95,10 +95,10 @@
<x-button icon="image" tooltip="Add image" />
<x-slot:fab><x-fab icon="add" tooltip="New note" color="secondary" /></x-slot:fab>
</x-toolbar>
</div>
</x-surface>
{{-- M3's docked toolbar on a large screen: rounded from 840px, its groups of controls divided. Narrower, it is the square docked bar. --}}
<div data-md-showcase-bar-frame="rounded">
<x-surface level="surface" corner="lg" padding="space200" style="width: 100%;">
<x-toolbar variant="docked" rounded label="Text formatting">
<x-button icon="undo" tooltip="Undo" />
<x-button icon="redo" tooltip="Redo" />
@@ -108,7 +108,7 @@
<x-divider vertical />
<x-button icon="link" tooltip="Insert link" />
</x-toolbar>
</div>
</x-surface>
</x-row>
BLADE,
'Primary tabs' => <<<'BLADE'
@@ -134,7 +134,7 @@
<x-tab name="billing">Plan and invoices.</x-tab>
</x-tabs>
<div data-md-showcase-tabs-narrow>
<div style="max-width: 384px;">
<x-tabs scrollable :tabs="collect(['January', 'February', 'March', 'April', 'May', 'June', 'July'])->map(fn ($month) => ['name' => strtolower($month), 'label' => $month])->all()">
<x-tab name="january">January's shares.</x-tab>
</x-tabs>
@@ -60,8 +60,8 @@
'Loading indicator' => <<<'BLADE'
<x-loading />
<x-loading contained />
<x-loading data-md-showcase-loading="lg" label="Uploading" />
<x-loading contained data-md-showcase-loading="sm" />
<x-loading label="Uploading" style="width: 96px; height: 96px; color: var(--md-sys-color-tertiary);" />
<x-loading contained style="width: 32px; height: 32px;" />
BLADE,
'Button groups: standard (press a button) and connected' => <<<'BLADE'
<x-button-group label="Standard" size="md">
@@ -161,13 +161,13 @@
<x-fab icon="add" label="New share" size="lg" color="tertiary" collapse-on-scroll />
BLADE,
'FAB menu' => <<<'BLADE'
<div data-md-showcase-fab-menu-demo>
<x-row justify="end" align="end" style="width: 100%; height: 288px;">
<x-fab-menu label="New">
<x-fab-menu-item label="Upload files" icon="upload_file" />
<x-fab-menu-item label="Upload a folder" icon="drive_folder_upload" />
<x-fab-menu-item label="Paste text" icon="content_paste" />
</x-fab-menu>
</div>
</x-row>
BLADE,
];
@endphp
@@ -13,7 +13,7 @@
['Heart', 'heart', 'background-color: var(--md-sys-color-inverse-surface); color: var(--md-sys-color-inverse-primary);'],
] as [$name, $shape, $colours])
<x-carousel-item :label="$name">
<div data-md-showcase-carousel-swatch style="{{ $colours }}">
<div style="display: grid; place-items: center; width: 100%; height: 100%; {{ $colours }}">
<x-shape :name="$shape" size="144" />
</div>
</x-carousel-item>
@@ -30,7 +30,7 @@
['Ghost', 'ghostish', 'background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary);'],
] as [$name, $shape, $colours])
<x-carousel-item :label="$name">
<div data-md-showcase-carousel-swatch style="{{ $colours }}">
<div style="display: grid; place-items: center; width: 100%; height: 100%; {{ $colours }}">
<x-shape :name="$shape" size="192" />
</div>
</x-carousel-item>
@@ -48,7 +48,7 @@
['oval', 'background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary);'],
] as [$shape, $colours])
<x-carousel-item>
<div data-md-showcase-carousel-swatch style="{{ $colours }}">
<div style="display: grid; place-items: center; width: 100%; height: 100%; {{ $colours }}">
<x-shape :name="$shape" size="96" />
</div>
</x-carousel-item>
@@ -66,7 +66,7 @@
['16/9', 'very-sunny', 'background-color: var(--md-sys-color-secondary); color: var(--md-sys-color-on-secondary);'],
] as [$aspect, $shape, $colours])
<x-carousel-item :aspect="$aspect" :label="$aspect">
<div data-md-showcase-carousel-swatch style="{{ $colours }}">
<div style="display: grid; place-items: center; width: 100%; height: 100%; {{ $colours }}">
<x-shape :name="$shape" size="80" />
</div>
</x-carousel-item>
@@ -81,7 +81,7 @@
['Night', 'puffy-diamond', 'background: linear-gradient(to bottom right, var(--md-sys-color-inverse-surface), var(--md-sys-color-primary)); color: var(--md-sys-color-inverse-primary);'],
] as [$name, $shape, $colours])
<x-carousel-item :label="$name">
<div data-md-showcase-carousel-swatch style="{{ $colours }}">
<div style="display: grid; place-items: center; width: 100%; height: 100%; {{ $colours }}">
<x-shape :name="$shape" size="160" />
</div>
</x-carousel-item>
@@ -53,7 +53,7 @@
BLADE,
'A set that scrolls' => <<<'BLADE'
{{-- M3 asks an overflowing chip row for a visible affordance: the edge it can still scroll towards fades, and on a mouse a button sits over it. --}}
<div data-md-showcase-chip-scroll>
<div style="width: 100%; max-width: 384px;">
<x-chip-set label="Sort and filter" hint="Drag, or use the arrow keys" scroll>
<x-chip type="filter" label="Newest" name="sort[]" value="newest" :selected="true" />
<x-chip type="filter" label="Largest" name="sort[]" value="largest" />
@@ -13,7 +13,7 @@
<x-badge value="Archived" color="neutral" outline />
<span style="position: relative; display: inline-flex"><x-icon name="chat" /><x-badge value="2" color="neutral" floating /></span>
<x-badge tonal color="tertiary"><x-icon name="bolt" filled optical="20" size="12" /> Pro</x-badge>
<x-badge value="Beta" tonal color="plain" data-md-showcase-badge-plain />
<x-badge value="Beta" tonal color="plain" style="background-color: var(--md-sys-color-primary-fixed); color: var(--md-sys-color-on-primary-fixed);" />
BLADE,
'Snackbars' => <<<'BLADE'
<x-row gap="space200" wrap align="center" x-data>
@@ -33,7 +33,7 @@
<p class="md-type-body-md md-ink-variant">
A description makes the snackbar 68px, M3's two-line height; below <code>medium</code> the action wraps under the text.
Press <kbd class="md-type-label-md" data-md-showcase-kbd>Alt</kbd>&nbsp;+&nbsp;<kbd class="md-type-label-md" data-md-showcase-kbd>G</kbd> to move the focus to a snackbar that has an action.
Press <kbd class="md-type-label-md" style="border-radius: var(--md-sys-shape-corner-xs); background-color: var(--md-sys-color-surface-container-highest); padding: 0 var(--md-sys-measurement-space75);">Alt</kbd>&nbsp;+&nbsp;<kbd class="md-type-label-md" style="border-radius: var(--md-sys-shape-corner-xs); background-color: var(--md-sys-color-surface-container-highest); padding: 0 var(--md-sys-measurement-space75);">G</kbd> to move the focus to a snackbar that has an action.
</p>
</x-stack>
BLADE,
@@ -41,7 +41,7 @@
<x-button icon="content_copy" tooltip="Copy link" />
<x-button icon="qr_code_2" tooltip-bottom="Show QR code" />
<x-tooltip text="Expires in 3 days" side="right">
<span tabindex="0" class="md-type-body-md md-ink-variant md-focus-ring" data-md-showcase-focus-demo>holiday-photos.zip</span>
<span tabindex="0" class="md-type-body-md md-ink-variant md-focus-ring" style="border-radius: var(--md-sys-shape-corner-xs);">holiday-photos.zip</span>
</x-tooltip>
BLADE,
'Rich tooltips' => <<<'BLADE'
@@ -89,11 +89,11 @@
<x-empty-state title="No routes yet" description="Draw a route on the map, or import one from a GPX file.">
<x-slot:illustration style="color: var(--md-sys-color-primary)">
<svg style="width: 128px; height: 128px" viewBox="0 0 120 120" fill="none" aria-hidden="true">
<circle cx="60" cy="60" r="56" data-md-showcase-illustration="container" />
<circle cx="60" cy="60" r="56" style="fill: var(--md-sys-color-primary-container)" />
<path d="M28 86C40 62 56 92 68 64S86 42 90 46" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-dasharray="1 10" />
<circle cx="28" cy="86" r="7" fill="currentColor" />
<path d="M90 18a12 12 0 0 1 12 12c0 10-12 22-12 22S78 40 78 30a12 12 0 0 1 12-12Z" data-md-showcase-illustration="accent" />
<circle cx="90" cy="30" r="4" data-md-showcase-illustration="on-accent" />
<path d="M90 18a12 12 0 0 1 12 12c0 10-12 22-12 22S78 40 78 30a12 12 0 0 1 12-12Z" style="fill: var(--md-sys-color-tertiary)" />
<circle cx="90" cy="30" r="4" style="fill: var(--md-sys-color-on-tertiary)" />
</svg>
</x-slot:illustration>
<x-slot:actions>
@@ -188,7 +188,7 @@
</div>
BLADE,
'A standard side sheet: co-planar from expanded, modal below' => <<<'BLADE'
<x-row data-md-showcase-side-sheet-demo align="start" gap="space300" stack-below="expanded" x-data="{ open: true }">
<x-row align="stretch" gap="space300" stack-below="expanded" x-data="{ open: true }">
<x-stack gap="space200" style="flex: 1 1 0%; min-width: 0;">
<x-button label="Show or hide the filters" variant="tonal" x-on:click="open = ! open" />
<p class="md-type-body-md md-ink-variant">
@@ -1,7 +1,7 @@
@php
$examples = [
'Data table' => <<<'BLADE'
<div data-md-showcase-table-scroll>
<div style="width: 100%; overflow-x: auto;">
<x-table>
<thead>
<tr>
@@ -35,8 +35,8 @@
<input id="showcase-error-filled" data-md-field-control value="2020-01-01" aria-invalid="true" />
</x-field>
<x-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-input size="sm" icon="search" placeholder="Filter" aria-label="Filter" style="width: 192px;" />
<x-input size="xs" placeholder="Find" aria-label="Find" style="width: 144px;" />
</x-row>
</x-stack>
</x-grid>
@@ -94,9 +94,9 @@
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" data-md-showcase-demo-card data-md-showcase-size="lg">
<x-card variant="outlined" style="width: 100%; max-width: 672px;">
<fieldset>
<legend class="md-type-title-sm md-ink" data-md-showcase-legend>Tell me about</legend>
<legend class="md-type-title-sm md-ink" style="margin-bottom: var(--md-sys-measurement-space200);">Tell me about</legend>
<x-grid :columns="['compact' => 1, 'expanded' => 2]" gap="space200">
<x-checkbox label="Downloads" name="notify[]" value="downloads" checked />
@@ -112,7 +112,7 @@
'Radio buttons' => <<<'BLADE'
<x-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" />
<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 medium" />
</x-grid>
BLADE,
'Switches' => <<<'BLADE'
@@ -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="md-type-label-lg" data-md-showcase-avatar>AR</span>
<span class="md-type-label-lg" style="display: grid; place-items: center; width: 32px; height: 32px; border-radius: var(--md-sys-shape-corner-full); background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container);">AR</span>
</x-slot:trailing>
</x-search>
</div>
@@ -189,9 +189,9 @@
</div>
{{-- M3's search icon button: search as a secondary action, expanding into the full-screen view. --}}
<div data-md-showcase-search-bar>
<x-row gap="space100" align="center" style="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);">
<x-button icon="menu" aria-label="Open the menu" />
<span class="md-type-title-md md-ink" data-md-showcase-search-bar-title>Shares</span>
<span class="md-type-title-md md-ink" style="flex: 1 1 auto;">Shares</span>
<x-search trigger="icon" label="Search shares">
<x-slot:suggestions>
<x-list>
@@ -200,11 +200,11 @@
</x-slot:suggestions>
<x-slot:empty>No shares match.</x-slot:empty>
</x-search>
</div>
</x-row>
</x-grid>
BLADE,
'A form' => <<<'BLADE'
<x-card variant="outlined" data-md-showcase-demo-card data-md-showcase-size="md">
<x-card variant="outlined" style="width: 100%; max-width: 576px;">
<x-form x-on:submit.prevent="materialToast('Share created', { type: 'success' })" separator>
<x-input label="Share name" required />
<x-textarea label="Message for recipients" />
@@ -125,15 +125,16 @@
</x-menu>
BLADE,
'Icons in their own colour' => <<<'BLADE'
{{-- icon-class takes the application's own class, unlayered so it wins over the item's ink: here .showcase-ink-tertiary { color: var(--md-sys-color-tertiary) }. A disabled item's icon stays disabled. --}}
<x-menu label="New plan">
<x-slot:trigger>
<x-button label="New plan" icon="add" variant="filled" />
</x-slot:trigger>
<x-menu-item label="Running" icon="directions_run" data-md-showcase-icon-color="tertiary" />
<x-menu-item label="Cycling" icon="directions_bike" data-md-showcase-icon-color="secondary" />
<x-menu-item label="Running" icon="directions_run" icon-class="showcase-ink-tertiary" />
<x-menu-item label="Cycling" icon="directions_bike" icon-class="showcase-ink-secondary" />
<x-menu-item label="Swimming" icon="pool" icon-class="md-ink-info" />
<x-menu-item label="Rowing" icon="rowing" data-md-showcase-icon-color="tertiary" disabled />
<x-menu-item label="Rowing" icon="rowing" icon-class="showcase-ink-tertiary" disabled />
</x-menu>
BLADE,
];
@@ -4,22 +4,22 @@
<x-stack gap="space300" style="width: 100%;">
<x-stack gap="space100" style="max-width: 400px;">
<p class="md-type-label-lg md-ink-variant">Compact: icon over label</p>
<div data-md-showcase-nav-demo="bar">
<div data-md-showcase-nav-page class="md-type-body-md md-ink-variant">The page</div>
<x-surface level="surface" corner="lg" outlined style="display: flex; flex-direction: column; height: 224px; overflow: hidden;">
<p class="md-type-body-md md-ink-variant" style="flex: 1 1 auto; padding: var(--md-sys-measurement-space200);">The page</p>
<x-navigation-bar>
<x-navigation-bar-item label="Shares" icon="folder_shared" link="#navigation" no-wire-navigate active />
<x-navigation-bar-item label="Upload" icon="upload" link="#navigation" no-wire-navigate />
<x-navigation-bar-item label="Inbox" icon="inbox" link="#navigation" no-wire-navigate badge="3" />
<x-navigation-bar-item label="Account" icon="account_circle" link="#navigation" no-wire-navigate :badge="true" badge-label="Needs attention" />
</x-navigation-bar>
</div>
</x-surface>
</x-stack>
<x-stack gap="space100" style="max-width: 800px;">
<p class="md-type-label-lg md-ink-variant">From 600px wide: icon beside label, centred</p>
<div data-md-showcase-nav-scroll>
<div data-md-showcase-nav-demo="bar-wide">
<div data-md-showcase-nav-page class="md-type-body-md md-ink-variant">The page</div>
<x-surface level="surface" corner="lg" outlined style="overflow-x: auto;">
<div style="display: flex; flex-direction: column; height: 224px; min-width: 600px;">
<p class="md-type-body-md md-ink-variant" style="flex: 1 1 auto; padding: var(--md-sys-measurement-space200);">The page</p>
<x-navigation-bar>
<x-navigation-bar-item label="Shares" icon="folder_shared" link="#navigation" no-wire-navigate active />
<x-navigation-bar-item label="Upload" icon="upload" link="#navigation" no-wire-navigate />
@@ -27,26 +27,26 @@
<x-navigation-bar-item label="Account" icon="account_circle" link="#navigation" no-wire-navigate />
</x-navigation-bar>
</div>
</div>
</x-surface>
</x-stack>
<x-stack gap="space100" style="max-width: 400px;">
<p class="md-type-label-lg md-ink-variant">Hide on scroll: scroll the page down, then up</p>
<div data-md-showcase-nav-demo="bar">
<div data-md-showcase-nav-page class="md-type-body-md md-ink-variant">The bar follows the window's scrolling, not this box's: scroll the showcase down and it leaves, scroll up and it springs back. It stays while a snackbar or a bottom sheet is on screen.</div>
<x-surface level="surface" corner="lg" outlined style="display: flex; flex-direction: column; height: 224px; overflow: hidden;">
<p class="md-type-body-md md-ink-variant" style="flex: 1 1 auto; padding: var(--md-sys-measurement-space200);">The bar follows the window's scrolling, not this box's: scroll the showcase down and it leaves, scroll up and it springs back. It stays while a snackbar or a bottom sheet is on screen.</p>
<x-navigation-bar hide-on-scroll>
<x-navigation-bar-item label="Shares" icon="folder_shared" link="#navigation" no-wire-navigate active />
<x-navigation-bar-item label="Upload" icon="upload" link="#navigation" no-wire-navigate />
<x-navigation-bar-item label="Inbox" icon="inbox" link="#navigation" no-wire-navigate badge="3" />
</x-navigation-bar>
</div>
</x-surface>
</x-stack>
<x-stack gap="space100" style="max-width: 800px;">
<p class="md-type-label-lg md-ink-variant">Tall (80px): icon over label whatever the width</p>
<div data-md-showcase-nav-scroll>
<div data-md-showcase-nav-demo="bar-wide">
<div data-md-showcase-nav-page class="md-type-body-md md-ink-variant">The page</div>
<x-surface level="surface" corner="lg" outlined style="overflow-x: auto;">
<div style="display: flex; flex-direction: column; height: 224px; min-width: 600px;">
<p class="md-type-body-md md-ink-variant" style="flex: 1 1 auto; padding: var(--md-sys-measurement-space200);">The page</p>
<x-navigation-bar tall>
<x-navigation-bar-item label="Shares" icon="folder_shared" link="#navigation" no-wire-navigate active />
<x-navigation-bar-item label="Upload" icon="upload" link="#navigation" no-wire-navigate />
@@ -54,13 +54,13 @@
<x-navigation-bar-item label="Account" icon="account_circle" link="#navigation" no-wire-navigate />
</x-navigation-bar>
</div>
</div>
</x-surface>
</x-stack>
</x-stack>
BLADE,
'Collapsed, narrow (centred) and expanded rails' => <<<'BLADE'
<x-row gap="space300" wrap align="start" style="width: 100%;">
<div data-md-showcase-nav-demo="rail">
<x-surface corner="lg" outlined style="display: flex; height: 576px; overflow: hidden;">
<x-navigation-rail mode="collapsed" label="Collapsed example" divider>
<x-slot:header>
<x-fab label="Compose" icon="edit" />
@@ -78,9 +78,9 @@
<x-navigation-rail-item label="Settings" icon="settings" link="#navigation" no-wire-navigate />
</x-slot:footer>
</x-navigation-rail>
</div>
</x-surface>
<div data-md-showcase-nav-demo="rail">
<x-surface corner="lg" outlined style="display: flex; height: 576px; overflow: hidden;">
<x-navigation-rail mode="collapsed" width="narrow" align="center" label="Narrow example" divider>
<x-slot:header>
<x-fab label="Compose" icon="edit" />
@@ -94,9 +94,9 @@
<x-navigation-rail-item label="Settings" icon="settings" link="#navigation" no-wire-navigate />
</x-slot:footer>
</x-navigation-rail>
</div>
</x-surface>
<div data-md-showcase-nav-demo="rail">
<x-surface corner="lg" outlined style="display: flex; height: 576px; overflow: hidden;">
<x-navigation-rail mode="expanded" label="Expanded example">
<x-slot:header>
<x-fab label="Compose" icon="edit" />
@@ -114,11 +114,11 @@
<x-navigation-rail-item label="Settings" icon="settings" link="#navigation" no-wire-navigate />
</x-slot:footer>
</x-navigation-rail>
</div>
</x-surface>
</x-row>
BLADE,
'Collapsible rail' => <<<'BLADE'
<div data-md-showcase-nav-demo="rail-wide">
<x-surface corner="lg" outlined style="display: flex; width: 100%; height: 480px; overflow: hidden;">
<x-navigation-rail label="Collapsible example">
<x-slot:brand>
<span class="md-type-title-lg md-truncate" style="display: block;">Mail</span>
@@ -133,13 +133,13 @@
<x-navigation-rail-item label="Sent" icon="send" link="#navigation" no-wire-navigate />
</x-navigation-rail>
<div data-md-showcase-nav-content class="md-type-body-md md-ink-variant">
<x-surface level="surface-container-low" padding="space300" class="md-type-body-md md-ink-variant" style="flex: 1 1 auto; min-width: 0;">
The menu button collapses and expands the rail, and the FAB morphs into an extended FAB with it. The choice is remembered and applied before the next page paints every collapsible rail follows it, <code>&lt;x-scaffold&gt;</code>'s from <code>expanded</code> (840px) too.
</div>
</div>
</x-surface>
</x-surface>
BLADE,
'Rail that hides when collapsed' => <<<'BLADE'
<div data-md-showcase-nav-demo="rail-wide">
<x-surface corner="lg" outlined style="display: flex; width: 100%; height: 480px; overflow: hidden;">
<x-navigation-rail label="Immersive example" hide-when-collapsed>
<x-slot:brand>
<span class="md-type-title-lg md-truncate" style="display: block;">Studio</span>
@@ -150,11 +150,13 @@
<x-navigation-rail-item label="Exports" icon="download" link="#navigation" no-wire-navigate />
</x-navigation-rail>
<x-stack gap="space200" data-md-showcase-nav-content class="md-type-body-md md-ink-variant">
<x-surface level="surface-container-low" padding="space300" style="flex: 1 1 auto; min-width: 0;">
<x-stack gap="space200" class="md-type-body-md md-ink-variant">
<p>M3's other expanded behaviour: collapsing this rail takes it out of the layout instead of narrowing it to 96px, so an immersive page gets the whole window. The only way back is a menu button of the application's own — its own button then docks it again.</p>
<x-button icon="menu" label="Open navigation" x-on:click="$store.rail.show()" />
</x-stack>
</div>
</x-surface>
</x-surface>
BLADE,
];
@endphp
@@ -64,7 +64,7 @@
<x-datepicker label="Disabled" value="2026-09-13" disabled />
<x-datepicker label="Read-only" value="2026-09-13" readonly />
<x-row gap="space125" wrap align="center">
<x-datepicker size="sm" aria-label="From" class="showcase-w-sm" />
<x-datepicker size="sm" aria-label="From" style="width: 192px;" />
</x-row>
</x-stack>
</x-grid>
@@ -20,7 +20,7 @@
<x-progress circular wavy label="Circular wavy, indeterminate" />
<x-progress value="65" circular thick label="Circular thick" />
<x-progress value="65" circular wavy thick label="Circular wavy thick" />
<x-progress circular wavy data-md-showcase-progress-lg label="Circular wavy, 96px" />
<x-progress circular wavy label="Circular wavy, 96px" style="width: 96px; height: 96px;" />
BLADE,
'Colours' => <<<'BLADE'
<x-progress value="35" color="secondary" label="Secondary" />
+13 -1
View File
@@ -132,7 +132,19 @@ it('writes every example as an application writes it', function () {
foreach ($examples as [$file, $code]) {
// The example component rewrites unprefixed tags to a configured prefix; a namespaced tag
// would show an application a form it does not write, and escape that rewrite.
expect($code)->not->toContain('x-livewire-material::', "{$file} writes a namespaced tag in an example");
expect(str_contains($code, 'x-livewire-material::'))->toBeFalse("{$file} writes a namespaced tag in an example");
// Nor a hook only showcase.css draws, which would do nothing in the application's page.
expect(str_contains($code, 'data-md-showcase'))->toBeFalse("{$file} writes a showcase hook in an example");
// A class is a text or interaction class, or one of the application's own that
// showcase.css stands in for, each named in its header.
preg_match_all('/class="([^"]*)"/', $code, $classes);
foreach (preg_split('/\s+/', implode(' ', $classes[1]), -1, PREG_SPLIT_NO_EMPTY) as $class) {
expect(str_starts_with($class, 'md-') || in_array($class, ['showcase-w-narrow', 'showcase-ink-tertiary', 'showcase-ink-secondary'], true))
->toBeTrue("{$file} writes the class `{$class}` in an example");
}
}
});
+21 -4
View File
@@ -698,15 +698,32 @@ it('shapes showcase.css like a package stylesheet, with its documented unlayered
->and($item['statement'])->toMatch('/^@import ([\'"])\.{1,2}\/[\w.\/-]+\.css\1;$/', "`{$item['statement']}` is not a plain import");
}
// showcase.css has no imports of its own yet (its header says why); the day it needs one, this
// showcase.css has no imports of its own (its header says why); the day it needs one, this
// still holds because it is a plain @import assertion above, not a fixed empty list here.
expect($unlayered)->toBe([
// Deliberately unlayered — see the file's header and .showcase-w-narrow's own comment.
'.showcase-w-sm' => true,
'.showcase-w-xs' => true,
// Deliberately unlayered, as an application's own classes are — see the file's header.
'.showcase-w-narrow' => true,
'.showcase-ink-tertiary' => true,
'.showcase-ink-secondary' => true,
]);
// Bundled with all.css on every showcase page, so it styles its own hooks and classes only,
// and each of those is still drawn by some showcase view.
preg_match_all('/([^{};]+)\{/', preg_replace('/@(?:layer|media)\b[^{]*\{/', '', stylesheetWithoutComments($css)), $preludes);
$views = collect(File::allFiles(__DIR__.'/../../resources/views/showcase'))->map(fn ($file): string => $file->getContents())->implode("\n");
foreach ($preludes[1] as $prelude) {
foreach (array_map('trim', explode(',', $prelude)) as $selector) {
expect($selector)->toMatch('/^(?:\[data-md-showcase[\w-]*[\]=]|\.showcase-[\w-]+)/', "showcase.css selects `{$selector}`, outside the showcase's own hooks");
}
}
preg_match_all('/data-md-showcase(?:-\w+)*|\.showcase(?:-\w+)+/', stylesheetWithoutComments($css), $hooks);
foreach (array_unique($hooks[0]) as $hook) {
expect(str_contains($views, ltrim($hook, '.')))->toBeTrue("showcase.css draws `{$hook}`, which no showcase view renders");
}
preg_match_all('/@media\b([^{]*)\{/', stylesheetWithoutComments($css), $matches);
foreach ($matches[1] as $query) {