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:
co-authored by
Claude Opus 5
parent
471d927e64
commit
247c596c3a
+18
-125
@@ -23,31 +23,6 @@ use Throwable;
|
||||
*/
|
||||
class ErrorPage
|
||||
{
|
||||
/**
|
||||
* The error layout's own stylesheet, which imports button.css and shape.css (and through
|
||||
* button.css, icon.css, loading.css and tooltip.css) for what the layout renders.
|
||||
*/
|
||||
protected const string LAYOUT = 'components/error-page.css';
|
||||
|
||||
/**
|
||||
* The package stylesheets the fallback bundles: the foundation (every application needs it)
|
||||
* and the error layout's own stylesheet — the same rules a Vite build would serve the page,
|
||||
* `Stylesheets::bundle()` inlining them in PHP instead. Not `all.css`: that would bundle every
|
||||
* component's rules for a page that draws three of them.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected const array FILES = ['foundation.css', self::LAYOUT];
|
||||
|
||||
/**
|
||||
* The scheme half of the fallback, cached per worker like `Stylesheets::bundle()`'s own cache:
|
||||
* by the scheme file's path and mtime, so a request that never regenerates it never rebuilds
|
||||
* this either, and a fresh `material:scheme` run is picked up the moment its mtime changes.
|
||||
*
|
||||
* @var array<string, array{css: string, mtime: int|false}>
|
||||
*/
|
||||
protected static array $schemeCache = [];
|
||||
|
||||
/**
|
||||
* The application's Vite tags, or null when they cannot be made. They bring the foundation,
|
||||
* the tokens, the scheme and the font; the layout's own rules come from `layoutStyles()`
|
||||
@@ -88,10 +63,10 @@ class ErrorPage
|
||||
}
|
||||
|
||||
/**
|
||||
* The error layout's own rules for a page with its build: `Stylesheets::bundle()` of
|
||||
* `components/error-page.css` and the stylesheets it imports, inlined beside the application's
|
||||
* Vite tags, which bring everything else. `bundle()` caches it for the worker by its files'
|
||||
* mtimes, as it does the fallback's half.
|
||||
* The error layout's own rules for a page with its build: `components/error-page.css` and the
|
||||
* stylesheets it imports, prebuilt into resources/dist/error-page.css (`npm run
|
||||
* build:stylesheets`) and inlined beside the application's Vite tags, which bring everything
|
||||
* else.
|
||||
*
|
||||
* Nothing is dropped from it, because nothing in it needs a build: the package's only
|
||||
* `@font-face` is `tokens/font.css`'s, which only the foundation reaches and the application's
|
||||
@@ -103,115 +78,33 @@ class ErrorPage
|
||||
*/
|
||||
public static function layoutStyles(): HtmlString
|
||||
{
|
||||
return new HtmlString(Stylesheets::bundle([self::path(self::LAYOUT)]));
|
||||
return new HtmlString(self::dist('error-page.css'));
|
||||
}
|
||||
|
||||
/**
|
||||
* A stylesheet for a page without its build: the foundation and the error layout's own rules —
|
||||
* `Stylesheets::bundle()` of `self::FILES`, the same inlining a Vite build does, so the fallback
|
||||
* can never drift from `resources/css/components/error-page.css` the way a hand-written copy
|
||||
* did — with every `@font-face` dropped (`withoutFontFace()`: there is no build here to serve
|
||||
* the font file, and a relative `url()` a request 404s on is worse than none) and the
|
||||
* application's colours appended in `material-scheme.css`'s own shape (`schemeStylesheet()`):
|
||||
* the standard level, the medium and high contrast levels under `[data-contrast]`, and a block
|
||||
* per colour profile under `[data-scheme]`. `<x-theme-script>` has already written
|
||||
* A stylesheet for a page without its build: the foundation and the error layout's own rules,
|
||||
* prebuilt into resources/dist/error-page-fallback.css — the same inlining a Vite build does,
|
||||
* so the fallback can never drift from `resources/css/components/error-page.css` the way a
|
||||
* hand-written copy did, and without the `@font-face` (there is no build here to serve the font
|
||||
* file; the brand typeface's stack in tokens/type.css falls back to the system fonts) — with
|
||||
* the application's colours appended in `material-scheme.css`'s own shape
|
||||
* (`SchemeStylesheet::withProfiles()`): the standard level, the medium and high contrast levels
|
||||
* under `[data-contrast]`, and a block per colour profile under `[data-scheme]`, built from
|
||||
* `Scheme::forStylesheet()` on every call. `<x-theme-script>` has already written
|
||||
* `data-theme`, `data-contrast` and `data-scheme` onto `<html>` by the time this tag is parsed
|
||||
* (it renders first), so the browser resolves the right block on its own — nothing here decides
|
||||
* an active profile in PHP.
|
||||
*/
|
||||
public static function fallbackStyles(): HtmlString
|
||||
{
|
||||
$files = array_map(self::path(...), self::FILES);
|
||||
|
||||
return new HtmlString(self::withoutFontFace(Stylesheets::bundle($files)).self::schemeStylesheet());
|
||||
return new HtmlString(self::dist('error-page-fallback.css').SchemeStylesheet::withProfiles(...Scheme::forStylesheet()));
|
||||
}
|
||||
|
||||
/**
|
||||
* A package stylesheet's absolute path, from its path under `resources/css/`.
|
||||
* A prebuilt stylesheet under resources/dist/.
|
||||
*/
|
||||
protected static function path(string $file): string
|
||||
protected static function dist(string $file): string
|
||||
{
|
||||
return dirname(__DIR__, 2)."/resources/css/{$file}";
|
||||
}
|
||||
|
||||
/**
|
||||
* `$css` with every `@font-face` block dropped, wherever it sits: not a search for the block's
|
||||
* text (fragile the moment a comment or a value is reworded) but a structural read — find
|
||||
* `@font-face` outside comments and strings, then remove the balanced `{ … }` that follows it,
|
||||
* brace for brace, however the block itself is written. Both reads go through
|
||||
* `Stylesheets::mask()`, because the bundle's comments name the rule too (foundation/base.css's
|
||||
* and error-page.css's headers): read as the rule, such a mention cut from inside its comment to
|
||||
* the end of the next balanced block, leaving the comment open over the rules after it —
|
||||
* base.css's `html` rule and icon.css's rules left the fallback that way. `tokens/font.css`'s
|
||||
* block is the only one the bundle carries, and it is also the bundle's only relative `url()`
|
||||
* (button.css, icon.css, shape.css and the rest reference nothing on disk), so this removal is
|
||||
* also what leaves the fallback with no `url()` to a font it cannot serve. The typeface itself
|
||||
* degrades on its own: `--md-ref-typeface-brand` (tokens/type.css) lists `ui-sans-serif`,
|
||||
* `system-ui` and `sans-serif` right after the brand name, so with no `@font-face` to resolve
|
||||
* it the browser skips straight to that system stack — nothing here has to name one.
|
||||
*/
|
||||
protected static function withoutFontFace(string $css): string
|
||||
{
|
||||
$masked = Stylesheets::mask($css);
|
||||
$result = '';
|
||||
$offset = 0;
|
||||
$length = strlen($css);
|
||||
|
||||
while (($start = stripos($masked, '@font-face', $offset)) !== false) {
|
||||
$result .= substr($css, $offset, $start - $offset);
|
||||
$open = strpos($masked, '{', $start);
|
||||
|
||||
if ($open === false) {
|
||||
$offset = $start + strlen('@font-face');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$depth = 1;
|
||||
$i = $open + 1;
|
||||
|
||||
while ($i < $length && $depth > 0) {
|
||||
$depth += match ($masked[$i]) {
|
||||
'{' => 1,
|
||||
'}' => -1,
|
||||
default => 0,
|
||||
};
|
||||
$i++;
|
||||
}
|
||||
|
||||
$offset = $i;
|
||||
}
|
||||
|
||||
return $result.substr($css, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* The application's colours, in `material-scheme.css`'s own selector shape
|
||||
* (`SchemeStylesheet::levels()`), from `Scheme::forStylesheet()` rather than a generated file
|
||||
* that may not exist yet. Cached by the scheme file's path and mtime (`self::$schemeCache`):
|
||||
* `Scheme` itself reads on every call and keeps nothing, so a resolver or a regenerated file
|
||||
* applies at once everywhere else it is asked — this cache is safe only because the CSS built
|
||||
* here never depends on which profile is *active*, only on the file's own content.
|
||||
*/
|
||||
protected static function schemeStylesheet(): string
|
||||
{
|
||||
$path = (string) config('livewire-material.scheme');
|
||||
clearstatcache(true, $path);
|
||||
$mtime = is_file($path) ? filemtime($path) : false;
|
||||
|
||||
if (isset(self::$schemeCache[$path]) && self::$schemeCache[$path]['mtime'] === $mtime) {
|
||||
return self::$schemeCache[$path]['css'];
|
||||
}
|
||||
|
||||
$scheme = Scheme::forStylesheet($path);
|
||||
$css = SchemeStylesheet::levels($scheme['scheme']);
|
||||
|
||||
foreach ($scheme['profiles'] as $name => $profile) {
|
||||
$css .= "\n".SchemeStylesheet::levels($profile, "[data-scheme='{$name}']");
|
||||
}
|
||||
|
||||
self::$schemeCache[$path] = ['css' => $css, 'mtime' => $mtime];
|
||||
|
||||
return $css;
|
||||
return (string) file_get_contents(dirname(__DIR__, 2)."/resources/dist/{$file}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user