Cut duplicated and speculative code across the package

An over-engineering audit of the whole tree, applied in five reviewed
batches. Behaviour stays the same except where UPGRADE.md says otherwise.

PHP: the showcase and error-page stylesheets are prebuilt into
resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import
(first occurrence kept, the order an application's build gives), instead
of Stylesheets::bundle() inlining imports on every request; only the
import walk DesignGuard needs stays. SchemeStylesheet::withProfiles()
replaces three copies of the scheme-plus-profiles loop, material:scheme
leaves spec and contrast checks to the node script that already made
them, and the error page's scheme cache, the hashed view namespace, the
translations path with no lang/ folder and DesignGuard's 1.x-name hints
are gone.

JS: the androidx shape port progress.js and both bin scripts each carried
lives once in resources/js/shapes.js (the generated SVGs are unchanged);
util.js holds ringIndex(), ms(), reopenGuard() and remember(), which
were written out several times; listeners are released through
AbortController; tooltip.js's hoverPopover() serves the rich tooltip too.

CSS: every rule for an element inside the navigation rail queries
`--md-navigation-rail-value` instead of repeating the seven collapsed
conditions under five media branches; badge, alert, progress, slider and
button read one non-inheriting colour-role table (components/color.css);
the dialog chrome, the submenu's popover chrome, the chip's state layer
and touch target, and the visually-hidden inputs use the shared rules
they copied; foundation/tokens.css is folded into foundation.css.

Views: Support\Field and Support\Link replace the error-key, bound-value
and link-attribute blocks copied into the fields and link components;
the timepicker period group, the menu filter and the showcase head are
partials; the datepicker's steppers and entry fields are loops; component
docblocks no longer restate SKILL.md.

Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces
four per-group files, DesignGuardTest and the layout-component tests use
datasets, browser tests share one ready() helper, CSS parsing lives in
ComponentStylesheet alone. docs/audits and the finding IDs citing it are
removed, as are pestphp/pest-plugin-laravel, the unused composer scripts
and check:font; the lint job runs in the feature job, which now installs
node packages so the prebuilt-stylesheet staleness test runs in CI.

Feature suite 1177 passed, Chrome browser suite 299 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-17 19:29:21 +02:00
co-authored by Claude Opus 5
parent 471d927e64
commit 247c596c3a
233 changed files with 16635 additions and 10579 deletions
+85 -158
View File
@@ -1,10 +1,8 @@
<?php
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use NoNameWeb\LivewireMaterial\Support\Layout;
use NoNameWeb\LivewireMaterial\Support\Stylesheets;
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet;
/**
* The package's stylesheets are plain CSS in `material.*` layers, with no Tailwind in them. Every
@@ -18,87 +16,14 @@ use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
* below run over the whole tree `all.css` reaches (foundation, layout and components together)
* rather than the foundation alone, and — one file it does not reach — `showcase.css`, checked on
* its own further down. A component's own values (colours as roles, shadows as elevation levels,
* and so on) are each group's own concern, in tests/Feature/Components/*StylesheetsTest.php;
* `bundle()`, the PHP bundler `all.css` is built for, is tests/Feature/StylesheetsBundleTest.php's.
* and so on) are ComponentStylesheetsTest's own concern.
* The prebuilt bundles in resources/dist/ are tests/Feature/StylesheetsBundleTest.php's.
*/
const MATERIAL_LAYER_STATEMENT = '@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;';
function stylesheetPath(string $path = ''): string
{
return (string) realpath(__DIR__.'/../../resources/css'.($path === '' ? '' : '/'.$path));
}
/**
* A stylesheet without its comments; a quoted string keeps whatever it holds.
*/
function stylesheetWithoutComments(string $css): string
{
return (string) preg_replace('~("(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\')|/\*.*?\*/~s', '$1', $css);
}
/**
* The top-level statements (`@layer …;`, `@import …;`) and blocks of a stylesheet, in order, with
* whitespace collapsed in statements and preludes.
*
* @return list<array{statement: string}|array{prelude: string, body: string}>
*/
function stylesheetItems(string $css): array
{
$css = stylesheetWithoutComments($css);
$items = [];
$start = 0;
$depth = 0;
$quote = null;
$opening = 0;
for ($i = 0, $length = strlen($css); $i < $length; $i++) {
$char = $css[$i];
if ($quote !== null) {
if ($char === '\\') {
$i++;
} elseif ($char === $quote) {
$quote = null;
}
continue;
}
if ($char === '"' || $char === "'") {
$quote = $char;
} elseif ($char === '{') {
if ($depth++ === 0) {
$opening = $i;
}
} elseif ($char === '}' && --$depth === 0) {
$items[] = [
'prelude' => (string) preg_replace('/\s+/', ' ', trim(substr($css, $start, $opening - $start))),
'body' => substr($css, $opening + 1, $i - $opening - 1),
];
$start = $i + 1;
} elseif ($char === ';' && $depth === 0) {
$items[] = ['statement' => preg_replace('/\s+/', ' ', trim(substr($css, $start, $i - $start))).';'];
$start = $i + 1;
}
}
expect(trim(substr($css, $start)))->toBe('', 'The stylesheet ends inside an unclosed rule.');
return $items;
}
/**
* The declarations of a flat block body, by property.
*
* @return array<string, string>
*/
function stylesheetDeclarations(string $body): array
{
return collect(explode(';', $body))
->map(fn (string $declaration): string => trim($declaration))
->filter()
->mapWithKeys(fn (string $declaration): array => [trim(strstr($declaration, ':', true)) => trim(substr(strstr($declaration, ':'), 1))])
->all();
return (string) realpath(ComponentStylesheet::cssPath($path));
}
/**
@@ -106,13 +31,13 @@ function stylesheetDeclarations(string $body): array
*/
function stylesheetBlock(string $css, string $prelude): string
{
foreach (stylesheetItems($css) as $item) {
foreach (ComponentStylesheet::items(ComponentStylesheet::withoutComments($css)) as $item) {
if (($item['prelude'] ?? null) === $prelude) {
return $item['body'];
}
if (str_starts_with($item['prelude'] ?? '', '@layer ')) {
foreach (stylesheetItems($item['body']) as $inner) {
foreach (ComponentStylesheet::items(ComponentStylesheet::withoutComments($item['body'])) as $inner) {
if (($inner['prelude'] ?? null) === $prelude) {
return $inner['body'];
}
@@ -130,8 +55,8 @@ function stylesheetBlock(string $css, string $prelude): string
*/
function stylesheetImports(string $file): array
{
return collect(stylesheetItems(File::get($file)))
->map(fn (array $item): ?string => preg_match('/^@import\s+([\'"])(.+?)\1/', $item['statement'] ?? '', $match) === 1 ? $match[2] : null)
return collect(ComponentStylesheet::items(ComponentStylesheet::withoutComments(File::get($file))))
->map(fn (array $item): ?string => ComponentStylesheet::importPath($item['statement'] ?? ''))
->filter()
->values()
->all();
@@ -205,8 +130,10 @@ it('brings every foundation file and text.css into the foundation, and nothing e
expect($parts->diff($foundation)->values()->all())->toBe([])
->and(array_map(fn (string $import): string => basename($import), stylesheetImports(stylesheetPath('foundation.css'))))
// In the order of their layers, the rules outside every layer beside the reset.
->toBe(['reset.css', 'hidden.css', 'tokens.css', 'base.css', 'interaction.css', 'text.css'])
// In the order of their layers, the rules outside every layer beside the reset — the seven
// token files imported directly (plan step 33: foundation/tokens.css held only these
// imports, and only foundation.css reached it, so it was folded in here).
->toBe(['reset.css', 'hidden.css', 'scheme.css', 'shape.css', 'elevation.css', 'motion.css', 'type.css', 'state.css', 'spacing.css', 'base.css', 'interaction.css', 'text.css'])
->and($foundation)
->toContain('tokens/scheme.css', 'tokens/shape.css', 'tokens/elevation.css', 'tokens/motion.css', 'tokens/type.css', 'tokens/state.css', 'tokens/spacing.css', 'tokens/font.css')
->and(array_values(preg_grep('/^components\//', $foundation)))->toBe([]);
@@ -253,7 +180,7 @@ it('leaves no stylesheet under resources/css/ outside all.css or showcase.css',
it('opens every stylesheet all.css reaches with a header, the layer statement and plain imports', function () {
foreach (allStylesheets() as $file) {
$name = stylesheetName($file);
$items = stylesheetItems(File::get($file));
$items = ComponentStylesheet::items(ComponentStylesheet::withoutComments(File::get($file)));
expect(File::get($file))->toStartWith('/*', "{$name} has no header comment")
->and($items[0]['statement'] ?? null)->toBe(MATERIAL_LAYER_STATEMENT, "{$name} does not open with the layer statement");
@@ -275,12 +202,11 @@ it('opens every stylesheet all.css reaches with a header, the layer statement an
});
it('keeps every foundation rule inside its material layer, but the two that hide', function () {
// Which layer each file writes into; tokens.css only imports.
// Which layer each file writes into.
$layers = [
'foundation.css' => null,
'foundation/reset.css' => 'material.reset',
'foundation/hidden.css' => null,
'foundation/tokens.css' => null,
'foundation/base.css' => 'material.base',
'foundation/interaction.css' => 'material.base',
'text.css' => 'material.text',
@@ -293,15 +219,15 @@ it('keeps every foundation rule inside its material layer, but the two that hide
foreach (foundationStylesheets() as $file) {
$name = stylesheetName($file);
$layer = array_key_exists($name, $layers) ? $layers[$name] : 'material.tokens';
$important += substr_count(stylesheetWithoutComments(File::get($file)), '!important');
$important += substr_count(ComponentStylesheet::withoutComments(File::get($file)), '!important');
foreach (stylesheetItems(File::get($file)) as $item) {
foreach (ComponentStylesheet::items(ComponentStylesheet::withoutComments(File::get($file))) as $item) {
if (! isset($item['prelude'])) {
continue;
}
if (! str_starts_with($item['prelude'], '@layer ')) {
$unlayered["{$name}: {$item['prelude']}"] = stylesheetDeclarations($item['body']);
$unlayered["{$name}: {$item['prelude']}"] = ComponentStylesheet::flatDeclarations($item['body']);
continue;
}
@@ -321,7 +247,7 @@ it('keeps every foundation rule inside its material layer, but the two that hide
it('defines x-cloak once, reached from both the foundation and all.css', function () {
$definitions = collect(File::allFiles(stylesheetPath()))
->filter(fn (SplFileInfo $file): bool => $file->getExtension() === 'css')
->filter(fn (SplFileInfo $file): bool => str_contains(stylesheetWithoutComments($file->getContents()), '[x-cloak]'))
->filter(fn (SplFileInfo $file): bool => str_contains(ComponentStylesheet::withoutComments($file->getContents()), '[x-cloak]'))
->map(fn (SplFileInfo $file): string => stylesheetName((string) $file->getRealPath()))
->values()
->all();
@@ -333,37 +259,21 @@ it('defines x-cloak once, reached from both the foundation and all.css', functio
it('writes no Tailwind directive anywhere all.css reaches', function () {
foreach (allStylesheets() as $file) {
expect(stylesheetWithoutComments(File::get($file)))
expect(ComponentStylesheet::withoutComments(File::get($file)))
->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b/', stylesheetName($file))
->not->toMatch('/--(?:theme|spacing|alpha)\(|\btheme\(|[\'"]tailwindcss[\'"]/', stylesheetName($file));
}
});
it('writes no Tailwind utility class anywhere under resources/ or tests/', function () {
// DesignGuard's own family table (check (i)) is the standing definition of a Tailwind
// utility or variant — this test is that guard turned on the package's own source, so the
// two never drift apart. tests/Feature/DesignGuardTest.php and tests/Fixtures/design-guard/
// carry Tailwind on purpose (they are the guard's own fixtures) and are left out.
$root = __DIR__.'/../..';
$designGuardTest = (string) realpath($root.'/tests/Feature/DesignGuardTest.php');
$paths = collect(['resources/views', 'resources/js', 'src', 'tests/Browser', 'tests/Feature'])
->flatMap(fn (string $directory): Collection => collect(File::allFiles($root.'/'.$directory)))
->filter(fn (SplFileInfo $file): bool => in_array($file->getExtension(), ['php', 'js', 'ts'], true))
->reject(fn (SplFileInfo $file): bool => (string) $file->getRealPath() === $designGuardTest)
->map(fn (SplFileInfo $file): string => (string) $file->getRealPath())
->values()
->all();
expect($paths)->not->toBeEmpty();
expect(DesignGuard::scan($paths)->violations())->toBe([]);
});
// No Tailwind-shaped class anywhere the package ships or tests itself is
// tests/Feature/DesignGuardTest.php's: one of its tests scans resources/views, resources/js, src
// and the Workbench, another scans tests/Browser and tests/Feature, so nothing here repeats
// either.
it('writes a media query, anywhere all.css reaches, only at M3\'s breakpoints, in px', function () {
$queries = collect(allStylesheets())
->flatMap(function (string $file): array {
preg_match_all('/@media\b([^{]*)\{/', stylesheetWithoutComments(File::get($file)), $matches);
preg_match_all('/@media\b([^{]*)\{/', ComponentStylesheet::withoutComments(File::get($file)), $matches);
return array_map(fn (string $query): string => stylesheetName($file).': '.trim($query), $matches[1]);
});
@@ -376,9 +286,9 @@ it('writes a media query, anywhere all.css reaches, only at M3\'s breakpoints, i
$feature = substr($query, strpos($query, ': ') + 2);
// The one named exception (docs/reference/m3/components-navigation-selection-inputs.md §
// Time pickers/Behaviour, and tests/Feature/Components/InputStylesheetsTest.php's
// assertBreakpointsInPx()): a viewport *height* in an `orientation` query is not a
// breakpoint, since M3 does not make one of it. A width, in any query, still is.
// Time pickers/Behaviour, and tests/Feature/Components/ComponentStylesheetsTest.php's
// token-values test): a viewport *height* in an `orientation` query is not a breakpoint,
// since M3 does not make one of it. A width, in any query, still is.
$orientationHeight = preg_match('/\(\s*orientation\s*:/', $feature) === 1
&& preg_match('/(?:^|[\s:<>=(])(?:min-|max-)?height\b/', $feature) === 1
&& ! str_contains($feature, 'width');
@@ -408,7 +318,7 @@ it('declares M3\'s spacing scale as the reference gives it', function () {
);
$reference = collect($rows)->mapWithKeys(fn (array $row): array => ["--md-sys-measurement-space{$row[1]}" => "{$row[3]}px"])->all();
$tokens = stylesheetDeclarations(stylesheetBlock(File::get(stylesheetPath('tokens/spacing.css')), ':root'));
$tokens = ComponentStylesheet::flatDeclarations(stylesheetBlock(File::get(stylesheetPath('tokens/spacing.css')), ':root'));
expect($reference)->toHaveCount(13)
->and($tokens)->toBe($reference);
@@ -495,11 +405,11 @@ it('gives every component view a stylesheet, or lists it on a short, documented
});
/**
* Every component view the four group stylesheet tests (Action/Input/Containment/Navigation) used
* to check one at a time, in one dataset instead: every `resources/views/components/*.blade.php`
* backed by a `components/*.css` file of its own, `tabs` and `tab` folded into the one entry
* `imports…()` below already special-cases (they share tabs.css, N-16), `theme-script` (no
* stylesheet at all) and every layout component (the test above covers those) left out.
* Every component view ComponentStylesheetsTest used to check one at a time, in one dataset
* instead: every `resources/views/components/*.blade.php` backed by a `components/*.css` file of
* its own, `tabs` and `tab` folded into the one entry `imports…()` below already special-cases
* (they share tabs.css), `theme-script` (no stylesheet at all) and every layout component (the
* test above covers those) left out.
*
* @return list<string>
*/
@@ -517,17 +427,34 @@ function componentStylesheetViews(): array
dataset('component stylesheet views', componentStylesheetViews());
/**
* The `<x-livewire-material::…>` tags a view renders, including any inside a
* `@include('livewire-material::partials.<name>', …)` it pulls in — markup a view moved out to
* resources/views/partials/<name>.blade.php (menu's filter field, the timepicker's period picker)
* still renders the same tags, just not inline where a plain regex over the view alone would see
* them.
*
* @return list<string>
*/
function renderedComponentTags(string $view): array
{
$content = File::get($view);
preg_match_all('/<x-livewire-material::([a-z-]+)/', $content, $tags);
preg_match_all('/@include\(\'livewire-material::partials\.([\w.-]+)\'/', $content, $partials);
return collect($tags[1])
->merge(collect($partials[1])->flatMap(fn (string $partial): array => renderedComponentTags(__DIR__."/../../resources/views/partials/{$partial}.blade.php")))
->all();
}
it('imports, from every component stylesheet, the stylesheet of each component its view renders', function (string $name) {
// tabs.css is shared by two views, tabs.blade.php and tab.blade.php (N-16); every other
// tabs.css is shared by two views, tabs.blade.php and tab.blade.php; every other
// component stylesheet in this dataset has exactly one view of its own name.
$views = $name === 'tabs' ? ['tabs', 'tab'] : [$name];
$rendered = collect($views)
->flatMap(function (string $view): array {
preg_match_all('/<x-livewire-material::([a-z-]+)/', File::get(__DIR__."/../../resources/views/components/{$view}.blade.php"), $tags);
return $tags[1];
})
->flatMap(fn (string $view): array => renderedComponentTags(__DIR__."/../../resources/views/components/{$view}.blade.php"))
->unique()
// A rendered tag that is not a components/ stylesheet at all (a layout component's, e.g.
// <x-pane>) has no import of its own to check here.
@@ -607,7 +534,7 @@ it('imports, from every layout stylesheet, the stylesheet of each component its
it('keeps every layout rule in material.layout, and the visibility props in material.visibility', function () {
// layoutStylesheets() follows every @import, so it now reaches component stylesheets a layout
// file draws on (pane.css: app-bar.css and button.css, for its own top app bar and back
// button) — each already checked, in material.components, by its own group's stylesheet test.
// button) — each already checked, in material.components, by ComponentStylesheetsTest.
foreach (layoutStylesheets() as $file) {
$name = stylesheetName($file);
@@ -621,9 +548,9 @@ it('keeps every layout rule in material.layout, and the visibility props in mate
default => 'material.layout',
};
expect(stylesheetWithoutComments($css))->not->toContain('!important', $name);
expect(ComponentStylesheet::withoutComments($css))->not->toContain('!important', $name);
foreach (stylesheetItems($css) as $item) {
foreach (ComponentStylesheet::items(ComponentStylesheet::withoutComments($css)) as $item) {
if (! isset($item['prelude'])) {
continue;
}
@@ -637,21 +564,21 @@ it('keeps every layout rule in material.layout, and the visibility props in mate
it('gives every spacing token a gap and a padding, and the layout components read only those', function () {
$tokens = array_map(
fn (string $property): string => substr($property, strlen('--md-sys-measurement-')),
array_keys(stylesheetDeclarations(stylesheetBlock(File::get(stylesheetPath('tokens/spacing.css')), ':root'))),
array_keys(ComponentStylesheet::flatDeclarations(stylesheetBlock(File::get(stylesheetPath('tokens/spacing.css')), ':root'))),
);
expect(Layout::SPACING)->toBe($tokens);
$spacing = File::get(stylesheetPath('layout/spacing.css'));
expect(stylesheetDeclarations(stylesheetBlock($spacing, "[data-md-gap='none']")))->toBe(['--md-gap' => '0px']);
expect(ComponentStylesheet::flatDeclarations(stylesheetBlock($spacing, "[data-md-gap='none']")))->toBe(['--md-gap' => '0px']);
foreach ($tokens as $token) {
expect(stylesheetDeclarations(stylesheetBlock($spacing, "[data-md-gap='{$token}']")))->toBe(['--md-gap' => "var(--md-sys-measurement-{$token})"])
->and(stylesheetDeclarations(stylesheetBlock($spacing, "[data-md-padding='{$token}']")))->toBe(['padding' => "var(--md-sys-measurement-{$token})"]);
expect(ComponentStylesheet::flatDeclarations(stylesheetBlock($spacing, "[data-md-gap='{$token}']")))->toBe(['--md-gap' => "var(--md-sys-measurement-{$token})"])
->and(ComponentStylesheet::flatDeclarations(stylesheetBlock($spacing, "[data-md-padding='{$token}']")))->toBe(['padding' => "var(--md-sys-measurement-{$token})"]);
}
expect(collect(stylesheetItems(stylesheetBlock($spacing, '@layer material.layout')))->pluck('prelude')->all())
expect(collect(ComponentStylesheet::items(ComponentStylesheet::withoutComments(stylesheetBlock($spacing, '@layer material.layout'))))->pluck('prelude')->all())
->toHaveCount(1 + 2 * count($tokens));
});
@@ -668,10 +595,10 @@ it('hides an element below and from every breakpoint but compact, and nothing el
$expected["@media (width >= {$width}px)"] = "[data-md-hide-from='{$breakpoint}']";
}
$actual = collect(stylesheetItems($visibility))->mapWithKeys(function (array $item): array {
$rule = stylesheetItems($item['body'])[0];
$actual = collect(ComponentStylesheet::items(ComponentStylesheet::withoutComments($visibility)))->mapWithKeys(function (array $item): array {
$rule = ComponentStylesheet::items(ComponentStylesheet::withoutComments($item['body']))[0];
expect(stylesheetDeclarations($rule['body']))->toBe(['display' => 'none']);
expect(ComponentStylesheet::flatDeclarations($rule['body']))->toBe(['display' => 'none']);
return [$item['prelude'] => $rule['prelude']];
})->all();
@@ -686,7 +613,7 @@ it('gives every md-type class exactly font, letter-spacing and font-variation-se
// asserted directly rather than cross-checked against a second copy.
preg_match_all('/\.md-(type-[\w-]+) \{\n(.*?)\n {4}\}/s', File::get(stylesheetPath('text.css')), $matches, PREG_SET_ORDER);
$classes = collect($matches)->mapWithKeys(fn (array $match): array => ["md-{$match[1]}" => stylesheetDeclarations($match[2])]);
$classes = collect($matches)->mapWithKeys(fn (array $match): array => ["md-{$match[1]}" => ComponentStylesheet::flatDeclarations($match[2])]);
expect($classes)->toHaveCount(30);
@@ -730,18 +657,18 @@ it('ships exactly the documented text classes, each ink an M3 role', function ()
],
];
$classes = collect(stylesheetItems(stylesheetBlock($text, '@layer material.text')))
$classes = collect(ComponentStylesheet::items(ComponentStylesheet::withoutComments(stylesheetBlock($text, '@layer material.text'))))
->map(fn (array $item): string => $item['prelude'] ?? $item['statement'])
->all();
expect($classes)->toBe($styles->merge(array_keys($inks))->merge(array_keys($rest))->map(fn (string $class): string => ".{$class}")->all());
foreach ($inks as $class => $role) {
expect(stylesheetDeclarations(stylesheetBlock($text, ".{$class}")))->toBe(['color' => "var(--md-sys-color-{$role})"]);
expect(ComponentStylesheet::flatDeclarations(stylesheetBlock($text, ".{$class}")))->toBe(['color' => "var(--md-sys-color-{$role})"]);
}
foreach ($rest as $class => $declarations) {
expect(stylesheetDeclarations(stylesheetBlock($text, ".{$class}")))->toBe($declarations);
expect(ComponentStylesheet::flatDeclarations(stylesheetBlock($text, ".{$class}")))->toBe($declarations);
}
});
@@ -754,7 +681,7 @@ it('ships exactly the documented text classes, each ink an M3 role', function ()
// file keeps to.
it('builds the package in the Workbench\'s one CSS entry, with no Tailwind left to sit between', function () {
$app = stylesheetWithoutComments(File::get(__DIR__.'/../../workbench/resources/css/app.css'));
$app = ComponentStylesheet::withoutComments(File::get(__DIR__.'/../../workbench/resources/css/app.css'));
// Opens by ordering the layers, so the order holds whichever the page links first — an
// application with Tailwind of its own still opens its own entry the same way (its header).
@@ -775,11 +702,11 @@ it('builds the package in the Workbench\'s one CSS entry, with no Tailwind left
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);
$items = ComponentStylesheet::items(ComponentStylesheet::withoutComments($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))
->and(ComponentStylesheet::withoutComments($css))
->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b/')
->not->toMatch('/--(?:theme|spacing|alpha)\(|\btheme\(|[\'"]tailwindcss[\'"]/');
@@ -814,7 +741,7 @@ it('shapes showcase.css like a package stylesheet, with its documented unlayered
// 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);
preg_match_all('/([^{};]+)\{/', preg_replace('/@(?:layer|media)\b[^{]*\{/', '', ComponentStylesheet::withoutComments($css)), $preludes);
$views = collect(File::allFiles(__DIR__.'/../../resources/views/showcase'))->map(fn ($file): string => $file->getContents())->implode("\n");
foreach ($preludes[1] as $prelude) {
@@ -823,13 +750,13 @@ it('shapes showcase.css like a package stylesheet, with its documented unlayered
}
}
preg_match_all('/data-md-showcase(?:-\w+)*|\.showcase(?:-\w+)+/', stylesheetWithoutComments($css), $hooks);
preg_match_all('/data-md-showcase(?:-\w+)*|\.showcase(?:-\w+)+/', ComponentStylesheet::withoutComments($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);
preg_match_all('/@media\b([^{]*)\{/', ComponentStylesheet::withoutComments($css), $matches);
foreach ($matches[1] as $query) {
$feature = trim($query);
@@ -845,17 +772,17 @@ it('shapes showcase.css like a package stylesheet, with its documented unlayered
}
});
it('keeps all.css under a gzip size budget, measured through Stylesheets::bundle()', function () {
// 106,838 bytes gzipped today (2026-09-15), rounded up by about 10% to a number CI can hold
// to without Node: PHP's own gzencode() on the same bytes Stylesheets::bundle() serves the
// showcase and the error page's fallback. A little organic growth should not retrigger this
// on every commit; a real jump means look at what grew since the budget was last raised.
$budget = 120_000;
it('keeps the showcase bundle, all.css with showcase.css, under a gzip size budget', function () {
// 40,167 bytes gzipped today (2026-09-17), measured with PHP's own gzencode() on
// resources/dist/showcase.css, the prebuilt bundle the showcase serves, comments stripped. A
// little organic growth should not retrigger this on every commit; a real jump means look at
// what grew since the budget was last raised.
$budget = 44_000;
$gzipped = strlen((string) gzencode(Stylesheets::bundle([stylesheetPath('all.css')]), 9));
$gzipped = strlen((string) gzencode(File::get(__DIR__.'/../../resources/dist/showcase.css'), 9));
expect($gzipped)->toBeLessThanOrEqual($budget, sprintf(
'all.css now gzips to %s bytes, over the %s-byte budget — see what grew, and raise the budget deliberately if it should have.',
'The showcase bundle now gzips to %s bytes, over the %s-byte budget — see what grew, and raise the budget deliberately if it should have.',
number_format($gzipped),
number_format($budget),
));