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
+13 -72
View File
@@ -288,24 +288,12 @@ it('sends an expired form back to its page to refresh', function () {
/**
* Plan step 36: `errors::minimal`'s class lists moved into
* resources/css/components/error-page.css, keyed on `data-md-error-*`. Its view lives outside
* resources/views/components/, so it is not in ContainmentStylesheetsTest's dataset (whose
* "imports what its view renders" check reads a fixed resources/views/components/<name>.blade.php
* path); this is its own small stylesheet-shape test instead, checking the same things.
* resources/views/components/, so it is not in ComponentStylesheetsTest's view-scoped datasets
* (whose "imports what its view renders" and "no class list" checks read a fixed
* resources/views/components/<name>.blade.php path) — those two stay here, reading its real view
* path, while the stylesheet-only checks (shape, tokens, block import) run there instead, on the
* `error-page` entry of its dataset.
*/
it('draws the error layout from a stylesheet shaped like every package stylesheet', function () {
$css = ComponentStylesheet::read('error-page');
expect($css->css)->toStartWith('/*')
->and($css->statements()[0] ?? null)->toBe('@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;')
->and(array_slice($css->statements(), 1))->each->toMatch('/^@import \'\.\/[a-z-]+\.css\';$/')
->and($css->blocks())->each->toBe('@layer material.components')
->and($css->css)->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b|--(?:theme|spacing|alpha)\(|\btheme\(/');
foreach ($css->imports() as $import) {
expect(is_file(dirname(ComponentStylesheet::path('error-page')).'/'.$import))->toBeTrue("error-page.css imports {$import}, which does not exist");
}
});
it('imports the stylesheet of every component the error layout renders', function () {
preg_match_all('/<x-livewire-material::([a-z-]+)/', File::get(__DIR__.'/../../resources/views/error-pages/errors/minimal.blade.php'), $tags);
@@ -322,33 +310,6 @@ it('writes no class list into the error layout but the interaction and text clas
expect(ViewClasses::violations(File::get(__DIR__.'/../../resources/views/error-pages/errors/minimal.blade.php')))->toBe([]);
});
it('takes its values from the tokens and its breakpoints in px', function () {
$source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read('error-page')->css);
expect($source)->not->toMatch('/#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\(/i')
->not->toMatch('/\bfont:(?!\s*var\(--md-sys-typescale-)/')
->not->toMatch('/\btransition[a-z-]*:[^;]*(?:\d+m?s\b|\bease\b|ease-in|ease-out|cubic-bezier)/');
preg_match_all('/@media\s*([^{]+)\{/', $source, $queries);
foreach ($queries[1] as $query) {
preg_match_all('/\(([^()]*)\)/', $query, $features);
foreach ($features[1] as $feature) {
preg_match_all('/(\d*\.?\d+)(px|rem|em)\b/', $feature, $lengths, PREG_SET_ORDER);
foreach ($lengths as [, $number, $unit]) {
expect($unit)->toBe('px', $query);
expect(in_array($number, ['600', '840', '1200', '1600'], true))->toBeTrue("{$query} is not at an M3 breakpoint");
}
}
}
});
it('is imported from the containment block of all.css', function () {
expect(allCssBlock('Containment'))->toContain("@import './components/error-page.css';");
});
it('styles the body only when it holds the error layout, and turns the shape on both paths', function () {
$source = (string) preg_replace('~/\*.*?\*/~s', '', ComponentStylesheet::read('error-page')->css);
@@ -361,7 +322,7 @@ it('styles the body only when it holds the error layout, and turns the shape on
});
/**
* Plan step 40: the fallback is `Stylesheets::bundle()` of the foundation and the error layout
* Plan step 40: the fallback is the prebuilt bundle of the foundation and the error layout
* (which pulls in button.css and shape.css) rather than a hand-built stylesheet, so it carries
* their rules verbatim, with no `@import` (bundled away), no relative `url()` (the only one in the
* bundle, `tokens/font.css`'s, leaves with the `@font-face` block it lives in) and no `@font-face`
@@ -382,13 +343,15 @@ it('bundles the foundation and the error layout into the fallback, without an im
->toContain('[data-md-error-shape]')
->toContain('[data-md-button]')
->toContain('[data-md-shape]')
// The foundation's reset and tokens travelled in too, not only the error layout.
->toContain('[data-md-icon]')
// The foundation's reset, tokens and page travelled in too, not only the error layout.
->toContain('box-sizing: border-box')
->toContain('--md-sys-color-surface');
->toContain('--md-sys-color-surface')
->toMatch('/html \{\s*background-color/');
});
/**
* Plan step 46: what the page inlines beside a build is `Stylesheets::bundle()` of the error layout
* Plan step 46: what the page inlines beside a build is the prebuilt bundle of the error layout
* alone. It reaches no `@font-face` — the package's only one is tokens/font.css's, which only the
* foundation imports, and the application's build serves it — and no `url()` a request from the
* page would have to resolve, so nothing needs dropping from it the way the fallback drops the face.
@@ -398,33 +361,11 @@ it('inlines beside a build the error layout\'s bundle, which holds no font face,
$css = (string) ErrorPage::layoutStyles();
$plain = (string) preg_replace('~/\*.*?\*/~s', '', $css);
expect($css)->toBe(Stylesheets::bundle([$layout]))
expect($css)->toBe(File::get(__DIR__.'/../../resources/dist/error-page.css'))
->and(array_map(basename(...), Stylesheets::resolvedFiles([$layout])))
->toEqualCanonicalizing(['error-page.css', 'button.css', 'icon.css', 'loading.css', 'tooltip.css', 'shape.css'])
->toEqualCanonicalizing(['error-page.css', 'button.css', 'icon.css', 'loading.css', 'tooltip.css', 'shape.css', 'color.css'])
->and($plain)->not->toContain('@font-face')
->not->toContain('@import')
->not->toMatch('/\burl\(/i')
->toContain('@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;');
});
/**
* The bundle's comments name `@font-face` too (foundation/base.css's and error-page.css's headers).
* Read as the rule, each mention cut from inside its comment to the end of the next balanced block
* and left the comment open, so base.css's `html` rule and icon.css's rules went missing from the
* fallback. Only the real block may leave: what remains is the bundle, rule for rule, without it.
*/
it('drops only the real font face from the fallback, not the rules after a comment that names one', function () {
$files = [
realpath(__DIR__.'/../../resources/css/foundation.css'),
realpath(__DIR__.'/../../resources/css/components/error-page.css'),
];
$withoutComments = fn (string $css): string => (string) preg_replace('~/\*.*?\*/~s', '', $css);
$bundle = Stylesheets::bundle($files);
expect(substr_count($withoutComments($bundle), '@font-face'))->toBe(1)
->and(substr_count($bundle, '@font-face'))->toBeGreaterThan(1)
->and($withoutComments((string) ErrorPage::fallbackStyles()))
->toStartWith($withoutComments((string) preg_replace('/@font-face\s*\{[^{}]*\}/', '', $bundle)))
->toMatch('/html \{\s*background-color: var\(--md-sys-color-surface\);/')
->toContain('[data-md-icon] {');
});