Pare down the showcase's unlayered sizing classes

Plan step 38 (last batch): only two of the previous batch's three
call sites actually need a plain unlayered class for their demo width
— <x-input> forwards `style` to the raw <input>, not the field wrapper
that is sized, and <x-datepicker> forwards neither, only `class`,
`wire:key` and `x-model` — so .showcase-w-sm and .showcase-w-xs stay.
<x-slider> does forward `style` to its root, so its vertical demo's
shared height is one now instead of the .showcase-slider-vertical
class. .showcase-w-narrow stays a class on purpose rather than an
inline style: fields.blade.php's "Narrower" input demonstrates an
application's own unlayered rule beating the package's layered
default, and an inline style would win regardless of layers, proving
nothing. The header explains all three.

StylesheetsTest gains showcase.css's own shape check — the same rule
every package stylesheet passes (header, layer statement, plain
imports, no Tailwind directive, breakpoints only at M3's four) — with
its three documented unlayered exceptions as the one allowed gap.

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 05:34:06 +02:00
co-authored by Claude Sonnet 5
parent 42009f3b0c
commit 067626c52e
3 changed files with 79 additions and 19 deletions
+18 -14
View File
@@ -27,11 +27,7 @@
* indicator, a bounded box for the FAB menu, a narrow chip row that forces its own scroll * 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, * 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` * 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>`, * needs beyond the fixed `md-ink-*` set.
* `<x-slider>`) forward only `class` and `style` to the element that needs sizing, never a data
* 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.
* *
* Batch 4 (the last: containment, carousel, bars, navigation, pages) adds the standard side * 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` * sheet demo's own breakpoint (a `<x-row>` beats its own `stack-below` alignment back to `stretch`
@@ -42,6 +38,18 @@
* own horizontal-scroll wrapper, `rail`, `rail-wide`) with their filler "page" and content panes. * 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 * Every per-item carousel colour stays the caller's own inline `style`, the same mechanism as the
* colour swatch above. * 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.
*/ */
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; @layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@@ -553,9 +561,8 @@
} }
} }
/* A handful of call sites (<x-input>, <x-datepicker>, <x-slider>) forward only `class` and /* Two call sites still cannot take a `style` for the width their demo needs — see the file
`style` to the element that needs sizing, so these few widths are the caller's own classes header — so these stay plain unlayered classes. */
rather than data-md-showcase-* hooks — see the file header. */
.showcase-w-sm { .showcase-w-sm {
width: 192px; width: 192px;
} }
@@ -564,12 +571,9 @@
width: 144px; width: 144px;
} }
/* Deliberately unlayered, to make the point fields.blade.php's hint names: an application's own
rule beats the package's layered default even though both set the same property. An inline
`style` would win regardless of layers, which would not demonstrate that. */
.showcase-w-narrow { .showcase-w-narrow {
max-width: 320px; max-width: 320px;
} }
/* <x-slider> forwards only class, style and wire:key to its wrapper, which the label and hint
share with the track — so the vertical demo's shared height is a class too. */
.showcase-slider-vertical {
height: 256px;
}
@@ -25,11 +25,11 @@
'Vertical' => <<<'BLADE' 'Vertical' => <<<'BLADE'
{{-- M3 Expressive's second orientation. A vertical slider needs a height: it is on the wrapper, which the label and hint share. --}} {{-- M3 Expressive's second orientation. A vertical slider needs a height: it is on the wrapper, which the label and hint share. --}}
<x-livewire-material::row gap="space500" wrap align="end"> <x-livewire-material::row gap="space500" wrap align="end">
<x-slider label="Volume" orientation="vertical" value="40" class="showcase-slider-vertical" hint="Up and Down" /> <x-slider label="Volume" orientation="vertical" value="40" style="height: 256px;" hint="Up and Down" />
<x-slider label="Warmth" orientation="vertical" value="6" :max="10" ticks class="showcase-slider-vertical" /> <x-slider label="Warmth" orientation="vertical" value="6" :max="10" ticks style="height: 256px;" />
<x-slider label="Brightness" orientation="vertical" value="70" size="md" icon="light_mode" class="showcase-slider-vertical" /> <x-slider label="Brightness" orientation="vertical" value="70" size="md" icon="light_mode" style="height: 256px;" />
<x-slider label="Always labelled" orientation="vertical" value="55" value-label="always" color="tertiary" class="showcase-slider-vertical" /> <x-slider label="Always labelled" orientation="vertical" value="55" value-label="always" color="tertiary" style="height: 256px;" />
<x-slider label="Locked" orientation="vertical" value="25" disabled class="showcase-slider-vertical" /> <x-slider label="Locked" orientation="vertical" value="25" disabled style="height: 256px;" />
</x-livewire-material::row> </x-livewire-material::row>
BLADE, BLADE,
'Colours and value labels' => <<<'BLADE' 'Colours and value labels' => <<<'BLADE'
+56
View File
@@ -666,3 +666,59 @@ it('builds the package in a Workbench entry of its own, with the material layers
'workbench/resources/js/app.js', 'workbench/resources/js/app.js',
]); ]);
}); });
it('shapes showcase.css like a package stylesheet, with its documented unlayered exceptions', function () {
$path = __DIR__.'/../../resources/css/showcase.css';
$css = File::get($path);
$items = stylesheetItems($css);
expect($css)->toStartWith('/*', 'showcase.css has no header comment')
->and($items[0]['statement'] ?? null)->toBe(MATERIAL_LAYER_STATEMENT, 'showcase.css does not open with the layer statement')
->and(stylesheetWithoutComments($css))
->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b/')
->not->toMatch('/--(?:theme|spacing|alpha)\(|\btheme\(|[\'"]tailwindcss[\'"]/');
$blocks = false;
$unlayered = [];
foreach (array_slice($items, 1) as $item) {
if (isset($item['prelude'])) {
$blocks = true;
if ($item['prelude'] === '@layer material.components') {
expect($item['body'])->not->toContain('@layer');
} else {
$unlayered[$item['prelude']] = true;
}
continue;
}
expect($blocks)->toBeFalse("`{$item['statement']}` comes after a block")
->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
// 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,
'.showcase-w-narrow' => true,
]);
preg_match_all('/@media\b([^{]*)\{/', stylesheetWithoutComments($css), $matches);
foreach ($matches[1] as $query) {
$feature = trim($query);
expect($feature)->not->toMatch('/\b(?:min|max)-(?:width|height)\b/', "{$feature} is not written as a range");
preg_match_all('/(\d*\.?\d+)([a-z%]*)/i', $feature, $lengths, PREG_SET_ORDER);
foreach ($lengths as [, $number, $unit]) {
expect($unit)->toBe('px', $feature)
->and(in_array($number, ['600', '840', '1200', '1600'], true))->toBeTrue("{$feature} is not at an M3 breakpoint");
}
}
});