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
+40 -7
View File
@@ -6,18 +6,51 @@ use NoNameWeb\LivewireMaterial\Tests\TestCase;
pest()->extend(TestCase::class)->in('Feature', 'Browser');
/**
* The element name (under `<`) and the attributes of the first tag in a rendered component, by
* name; a boolean attribute Blade renders as `name="name"`. For the layout components' render
* tests, which assert what their root carries.
* The attributes of a rendered component's tag, by name; a boolean attribute Blade renders as
* `name="name"`. Without `$tag`, it is the first tag anywhere in the rendered Blade (anchored to
* the start), and the element name comes back too, under `<` — the layout components' render
* tests, which assert what their root carries, read it this way. With `$tag` (`|`-separated for
* more than one, e.g. `'button|a'`), it is the first `<$tag>` anywhere in the rendered Blade
* instead, with no `<` key — for a component whose root is not the tag Blade renders first (a
* button or link inside a wrapper, a span), where the anchored match would read the wrong element.
*
* @return array<string, string>
*/
function layoutRoot(string $html): array
function layoutRoot(string $html, ?string $tag = null): array
{
preg_match('/^\s*<([a-z]+)\b([^>]*)>/s', $html, $tag);
preg_match_all('/([\w:.@-]+)(?:="([^"]*)")?/', $tag[2] ?? '', $pairs, PREG_SET_ORDER);
if ($tag === null) {
preg_match('/^\s*<([a-z]+)\b([^>]*)>/s', $html, $match);
$root = ['<' => $match[1] ?? ''];
$attributes = $match[2] ?? '';
} else {
preg_match('/<(?:'.$tag.')\b([^>]*)>/', $html, $match);
$root = [];
$attributes = $match[1] ?? '';
}
return ['<' => $tag[1] ?? ''] + collect($pairs)->mapWithKeys(fn (array $pair): array => [$pair[1] => $pair[2] ?? ''])->all();
preg_match_all('/([\w:.@-]+)(?:="([^"]*)")?/', $attributes, $pairs, PREG_SET_ORDER);
return $root + collect($pairs)->mapWithKeys(fn (array $pair): array => [$pair[1] => $pair[2] ?? ''])->all();
}
/**
* The page is done loading, and Alpine and/or Livewire (whichever the page renders) have booted —
* chained after visit(), before a Browser test probe reads anything either one wires up. Most
* pages need both; one that renders no Livewire component needs Alpine alone, and vice versa.
*/
function ready(mixed $page, bool $alpine = true, bool $livewire = true): mixed
{
$checks = ["document.readyState === 'complete'"];
if ($alpine) {
$checks[] = "typeof window.Alpine !== 'undefined'";
}
if ($livewire) {
$checks[] = "typeof window.Livewire !== 'undefined'";
}
return $page->waitForEvent('networkidle')->assertScript(implode(' && ', $checks));
}
// A shared CI runner is slower than a workstation: an animation or a smooth scroll can take longer