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
+38
-81
@@ -18,10 +18,9 @@ use Symfony\Component\Finder\Finder;
|
||||
* (i) any Tailwind utility or variant in a view, PHP or JS file — a breakpoint prefix, a
|
||||
* cleared scale (radius, shadow, type size/weight/leading/tracking, easing, duration), a
|
||||
* layout, spacing, sizing, position, border, effect, interactivity, text or display
|
||||
* utility, a colour utility on a role, a 1.x ink, white or black (`text-meta`, `bg-white`,
|
||||
* `text-on-surface/60`) or on a colour the application's own Tailwind theme named
|
||||
* (`bg-brand`, `text-sport-run/60`), a 1.x package utility (`type-body-md`, `focus-ring`,
|
||||
* `rounded-corner-lg`), a variant, or an arbitrary `[…]` value or property — each with its
|
||||
* utility, a colour utility on a role, white or black (`bg-white`, `text-on-surface/60`)
|
||||
* or on a colour the application's own Tailwind theme named (`bg-brand`,
|
||||
* `text-sport-run/60`), a variant, or an arbitrary `[…]` value or property — each with its
|
||||
* 2.0.0 replacement: a layout component and prop (`gap-4` → `gap="space200"`), an `md-*`
|
||||
* class, or a token for the application's own CSS. A class the application's own
|
||||
* stylesheets declare is exempt, and so is every `md-*` class.
|
||||
@@ -101,14 +100,6 @@ class DesignGuard
|
||||
'2xl' => 'extra-large',
|
||||
];
|
||||
|
||||
/** The M3 breakpoint each window size class is, in px (foundations.md § Layout). */
|
||||
protected const array WINDOW_PX = [
|
||||
'medium' => 600,
|
||||
'expanded' => 840,
|
||||
'large' => 1200,
|
||||
'extra-large' => 1600,
|
||||
];
|
||||
|
||||
/** Tailwind's radius scale and the M3 corner it replaces each (styles §Shape). */
|
||||
protected const array CORNERS = [
|
||||
'none' => 'none',
|
||||
@@ -142,12 +133,6 @@ class DesignGuard
|
||||
* families are the line-by-line checks'.
|
||||
*/
|
||||
protected const array FAMILIES = [
|
||||
// 1.x's own utilities, which compiled through the package's Tailwind theme until 2.0.0.
|
||||
['/^(?:focus-ring|state-layer|touch-target|link)$/', '1.x utility `%s` compiles to nothing — use `md-%s` (interaction.css)'],
|
||||
['/^rounded(?:-(?:ss|se|ee|es|tl|tr|br|bl|t|r|b|l|s|e))?-corner-(?<corner>[a-z-]+)$/', '1.x utility `%s` compiles to nothing — use `var(--md-sys-shape-corner-{corner})` in your own CSS, or `<x-surface corner="{corner}">`'],
|
||||
['/^shadow-elevation-(?<level>[0-5])$/', '1.x utility `%s` compiles to nothing — use `var(--md-sys-elevation-{level})` in your own CSS'],
|
||||
['/^ease-(?<easing>(?:spatial|effects)-(?:fast|default|slow))$/', '1.x utility `%s` compiles to nothing — pair `var(--md-sys-motion-{easing})` with `var(--md-sys-motion-{easing}-duration)` in your own `transition`'],
|
||||
['/^ease-(?<easing>standard|emphasized)(?<phase>-accelerate|-decelerate)?$/', '1.x utility `%s` compiles to nothing — use `var(--md-sys-motion-easing-{easing}{phase})` in your own `transition`'],
|
||||
// Tailwind's cleared easing and duration scale: matched only here, in a class list a
|
||||
// Blade or PHP file writes literally (never line-by-line across every file, the way
|
||||
// outsideTheScale() still reads its other scale steps) — a plain PHP or JS string such as
|
||||
@@ -205,17 +190,23 @@ class DesignGuard
|
||||
'tabular-nums' => 'md-tabular',
|
||||
];
|
||||
|
||||
/** A colour utility's prefix, up to its dash; `COLOUR_UTILITY` and `THEME_COLOUR_UTILITY` share it. */
|
||||
protected const string COLOUR_PREFIX = '/^(?<utility>bg|text|border(?:-(?:[trblxyse]|bs|be))?|divide|ring(?:-offset)?|outline|fill|stroke|decoration|accent|caret|placeholder|shadow|from|via|to)-';
|
||||
|
||||
/** A colour utility's opacity modifier (`/60`, `/[0.32]`, `/(--alpha)`), to the end of the token. */
|
||||
protected const string COLOUR_OPACITY = '(?:\/(?<opacity>\d{1,3}|\[[^\]]*\]|\(--[\w-]+\)))?$/';
|
||||
|
||||
/**
|
||||
* A colour utility written on an M3 role, a 1.x ink name, or white/black/current, with an
|
||||
* optional opacity modifier (`text-on-surface/60`, `bg-scrim/[0.32]`). The roles are every
|
||||
* `--md-sys-color-*` the default scheme and elevation tokens declare.
|
||||
* A colour utility written on an M3 role, or white/black/current, with an optional opacity
|
||||
* modifier (`text-on-surface/60`, `bg-scrim/[0.32]`). The roles are every `--md-sys-color-*`
|
||||
* the default scheme and elevation tokens declare.
|
||||
*/
|
||||
protected const string COLOUR_UTILITY = '/^(?<utility>bg|text|border(?:-(?:[trblxyse]|bs|be))?|divide|ring(?:-offset)?|outline|fill|stroke|decoration|accent|caret|placeholder|shadow|from|via|to)-'
|
||||
protected const string COLOUR_UTILITY = self::COLOUR_PREFIX
|
||||
.'(?<role>(?:on-)?(?:primary|secondary|tertiary|error|success|warning|info)(?:-container|-dim|-fixed(?:-dim|-variant)?)?'
|
||||
.'|inverse-(?:primary|surface|on-surface|error|success|warning|info)'
|
||||
.'|(?:on-)?background|(?:on-)?surface(?:-variant|-dim|-bright|-container(?:-lowest|-low|-high|-highest)?)?'
|
||||
.'|outline(?:-variant)?|scrim|shadow|body|meta|quiet|structure|chrome|divider|white|black|current|transparent|inherit|initial)'
|
||||
.'(?:\/(?<opacity>\d{1,3}|\[[^\]]*\]|\(--[\w-]+\)))?$/';
|
||||
.'|outline(?:-variant)?|scrim|shadow|white|black|current|transparent|inherit|initial)'
|
||||
.self::COLOUR_OPACITY;
|
||||
|
||||
/**
|
||||
* A colour utility on any other name (`bg-off-plan`, `text-sport-run`, `border-l-zone-4`,
|
||||
@@ -225,8 +216,7 @@ class DesignGuard
|
||||
* `bg-cover`, `shadow-none`) is reported as that family; the name starts with a letter, so a
|
||||
* width or a stop (`border-2`, `from-10%`) never reads as one.
|
||||
*/
|
||||
protected const string THEME_COLOUR_UTILITY = '/^(?<utility>bg|text|border(?:-(?:[trblxyse]|bs|be))?|divide|ring(?:-offset)?|outline|fill|stroke|decoration|accent|caret|placeholder|shadow|from|via|to)-'
|
||||
.'(?<name>[a-z][a-z\d]*(?:-[a-z\d]+)*)(?:\/(?<opacity>\d{1,3}|\[[^\]]*\]|\(--[\w-]+\)))?$/';
|
||||
protected const string THEME_COLOUR_UTILITY = self::COLOUR_PREFIX.'(?<name>[a-z][a-z\d]*(?:-[a-z\d]+)*)'.self::COLOUR_OPACITY;
|
||||
|
||||
/**
|
||||
* The hooks an application writes on markup of its own, not through a component tag, whose
|
||||
@@ -246,16 +236,6 @@ class DesignGuard
|
||||
'data-md-list-row' => ['stylesheet' => 'components/list-item.css', 'except' => ['card']],
|
||||
];
|
||||
|
||||
/** 1.x's semantic ink names, and the M3 role each resolved to (UPGRADE.md § 4). */
|
||||
protected const array INK_ALIASES = [
|
||||
'body' => 'on-surface-variant',
|
||||
'meta' => 'on-surface-variant',
|
||||
'quiet' => 'outline',
|
||||
'structure' => 'outline-variant',
|
||||
'chrome' => 'outline-variant',
|
||||
'divider' => 'outline-variant',
|
||||
];
|
||||
|
||||
/** An ink role, and the `md-ink-*` class (text.css) that sets it on plain text. */
|
||||
protected const array INK_ROLE = [
|
||||
'on-surface' => 'md-ink',
|
||||
@@ -406,7 +386,7 @@ class DesignGuard
|
||||
if ($this->cssEntry !== null && ! is_file($this->cssEntry)) {
|
||||
$violations[] = "{$this->cssEntry}:1 the CSS entry `missingStylesheets()` names does not exist";
|
||||
} elseif ($this->cssEntry !== null) {
|
||||
$foundation = static::packageCssRoot().'/foundation.css';
|
||||
$foundation = static::packagePath('css').'/foundation.css';
|
||||
|
||||
if (! isset($resolved[$foundation])) {
|
||||
$violations[] = sprintf(
|
||||
@@ -428,7 +408,8 @@ class DesignGuard
|
||||
$where = $this->relative($file->getPathname());
|
||||
$isBlade = str_ends_with($file->getFilename(), '.blade.php');
|
||||
$readsClasses = ! $this->isUnder($file->getPathname(), $mailPaths);
|
||||
$readsHooks = ! $this->isUnder($file->getPathname(), array_filter([static::packageViewRoot()]));
|
||||
// A hook written in the package's own views is a tag's own, so hooks are read only outside them.
|
||||
$readsHooks = ! $this->isUnder($file->getPathname(), array_filter([static::packagePath('views')]));
|
||||
|
||||
if ($isBlade) {
|
||||
$contents = $this->withoutBladeComments($contents);
|
||||
@@ -520,7 +501,7 @@ class DesignGuard
|
||||
}
|
||||
|
||||
if ($this->paginationUsages($contents) !== []) {
|
||||
$needed[] = static::packageCssRoot().'/components/pagination.css';
|
||||
$needed[] = static::packagePath('css').'/components/pagination.css';
|
||||
}
|
||||
|
||||
return $needed;
|
||||
@@ -539,7 +520,7 @@ class DesignGuard
|
||||
return ["{$entry}:1 the CSS entry `unusedStylesheets()` names does not exist"];
|
||||
}
|
||||
|
||||
$root = static::packageCssRoot();
|
||||
$root = static::packagePath('css');
|
||||
// Comments out, line count kept; the import strings themselves stay readable.
|
||||
$css = (string) preg_replace_callback('~/\*.*?\*/~s', fn (array $comment): string => str_repeat("\n", substr_count($comment[0], "\n")), (string) file_get_contents($real));
|
||||
$imports = [];
|
||||
@@ -730,7 +711,7 @@ class DesignGuard
|
||||
return array_map(function (array $match): string {
|
||||
$max = $match['max'] ?? '';
|
||||
$name = self::WINDOW_CLASSES[$match['name']];
|
||||
$px = self::WINDOW_PX[$name];
|
||||
$px = Layout::BREAKPOINTS[$name];
|
||||
$comparison = $max !== '' ? "width < {$px}px" : "width >= {$px}px";
|
||||
|
||||
return "Tailwind breakpoint `{$max}{$match['name']}:` compiles to nothing — M3's {$name} ({$px}px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media ({$comparison})` in your own CSS";
|
||||
@@ -825,10 +806,6 @@ class DesignGuard
|
||||
return "Tailwind variant class `{$token}` compiles to nothing — ".$this->variantHint($variant['lead']);
|
||||
}
|
||||
|
||||
if (preg_match('/^(?:type-(?:emphasized-)?(?:display|headline|title|body|label)-(?:lg|md|sm))$/', $token) === 1) {
|
||||
return "1.x utility `{$token}` compiles to nothing — use `md-{$token}` (text.css)";
|
||||
}
|
||||
|
||||
if (preg_match('/^\[(?<property>-{0,2}[a-z][\w-]*):[^\]]+\]$/', $token, $arbitrary) === 1) {
|
||||
return "Tailwind arbitrary property `{$token}` compiles to nothing — write `{$arbitrary['property']}` in your own CSS";
|
||||
}
|
||||
@@ -890,16 +867,14 @@ class DesignGuard
|
||||
|
||||
/**
|
||||
* The 2.0.0 replacement for a Tailwind variant, by its first segment: a state, the theme, a
|
||||
* structural pseudo-class, a direction, an M3 breakpoint variant 1.x compiled, or an
|
||||
* arbitrary selector.
|
||||
* structural pseudo-class, a direction, an M3 breakpoint, or an arbitrary selector.
|
||||
*/
|
||||
protected function variantHint(string $lead): string
|
||||
{
|
||||
$m3 = ['medium' => 600, 'expanded' => 840, 'large' => 1200, 'extra-large' => 1600];
|
||||
$bare = str_starts_with($lead, 'max-') ? substr($lead, 4) : $lead;
|
||||
|
||||
return match (true) {
|
||||
isset($m3[$bare]) => '`@media (width '.($bare === $lead ? '>=' : '<')." {$m3[$bare]}px)` in your own CSS, or a layout component's `hide-below`/`hide-from`/`stack-below` prop",
|
||||
$bare !== 'compact' && isset(Layout::BREAKPOINTS[$bare]) => '`@media (width '.($bare === $lead ? '>=' : '<').' '.Layout::BREAKPOINTS[$bare]."px)` in your own CSS, or a layout component's `hide-below`/`hide-from`/`stack-below` prop",
|
||||
$lead === 'dark' => "the roles already switch with the theme; a dark-only rule is `[data-theme='dark'] …` in your own CSS",
|
||||
$lead === 'rail-collapsed' => "`:root[data-rail='collapsed'] …` in your own CSS",
|
||||
in_array($lead, ['motion-reduce', 'motion-safe'], true) => '`@media (prefers-reduced-motion: reduce)` in your own CSS',
|
||||
@@ -999,7 +974,7 @@ class DesignGuard
|
||||
}
|
||||
|
||||
$utility = $m['utility'];
|
||||
$role = self::INK_ALIASES[$m['role']] ?? $m['role'];
|
||||
$role = $m['role'];
|
||||
$dead = "Tailwind colour utility `{$token}` compiles to nothing";
|
||||
|
||||
return match (true) {
|
||||
@@ -1553,7 +1528,7 @@ class DesignGuard
|
||||
preg_match_all('/(?<![\w`\[-])'.preg_quote($hook, '/').'(?![\w-])/', $markup, $matches, PREG_OFFSET_CAPTURE);
|
||||
|
||||
foreach ($matches[0] as [, $offset]) {
|
||||
$found[] = [substr_count(substr($markup, 0, $offset), "\n") + 1, $hook, static::packageCssRoot().'/'.$stylesheet];
|
||||
$found[] = [substr_count(substr($markup, 0, $offset), "\n") + 1, $hook, static::packagePath('css').'/'.$stylesheet];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1622,7 +1597,7 @@ class DesignGuard
|
||||
}
|
||||
|
||||
foreach ($this->paginationUsages($contents) as $line) {
|
||||
$needs[] = [$line, '`->links()`', static::packageCssRoot().'/components/pagination.css'];
|
||||
$needs[] = [$line, '`->links()`', static::packagePath('css').'/components/pagination.css'];
|
||||
}
|
||||
|
||||
usort($needs, fn (array $a, array $b): int => $a[0] <=> $b[0]);
|
||||
@@ -1762,42 +1737,24 @@ class DesignGuard
|
||||
*/
|
||||
protected static function isPackageFile(string $file): bool
|
||||
{
|
||||
$root = static::packageCssRoot();
|
||||
$root = static::packagePath('css');
|
||||
|
||||
return $root !== '' && ($file === $root || str_starts_with($file, $root.'/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* This package's own `resources/css`, from this file's own location — works whether the class
|
||||
* loads from `vendor/nonameweb/livewire-material` (a consuming application) or from this
|
||||
* repository itself, the same trick `Support\Stylesheets::name()` uses.
|
||||
* A folder of this package's own `resources/` (`css`, `views`, `views/components`), from this
|
||||
* file's own location — works whether the class loads from `vendor/nonameweb/livewire-material`
|
||||
* (a consuming application) or from this repository itself. Empty when it does not exist.
|
||||
*/
|
||||
protected static function packageCssRoot(): string
|
||||
protected static function packagePath(string $folder): string
|
||||
{
|
||||
return (string) realpath(dirname(__DIR__, 2).'/resources/css');
|
||||
}
|
||||
|
||||
/**
|
||||
* This package's own `resources/views/components` — every component and layout component
|
||||
* lives here, so a tag's own view existing there is what "is this a package tag" checks.
|
||||
*/
|
||||
protected static function packageComponentViewRoot(): string
|
||||
{
|
||||
return (string) realpath(dirname(__DIR__, 2).'/resources/views/components');
|
||||
}
|
||||
|
||||
/**
|
||||
* This package's own `resources/views`: its components, showcase, pagination and error pages.
|
||||
* A hook written there is a tag's own, so check (ii) reads hooks only outside it.
|
||||
*/
|
||||
protected static function packageViewRoot(): string
|
||||
{
|
||||
return (string) realpath(dirname(__DIR__, 2).'/resources/views');
|
||||
return (string) realpath(dirname(__DIR__, 2)."/resources/{$folder}");
|
||||
}
|
||||
|
||||
protected static function isPackageTag(string $name): bool
|
||||
{
|
||||
$root = static::packageComponentViewRoot();
|
||||
$root = static::packagePath('views/components');
|
||||
|
||||
return $root !== '' && is_file("{$root}/{$name}.blade.php");
|
||||
}
|
||||
@@ -1815,7 +1772,7 @@ class DesignGuard
|
||||
}
|
||||
|
||||
foreach (['components', 'layout'] as $group) {
|
||||
$file = static::packageCssRoot()."/{$group}/{$name}.css";
|
||||
$file = static::packagePath('css')."/{$group}/{$name}.css";
|
||||
|
||||
if (is_file($file)) {
|
||||
return $file;
|
||||
@@ -1827,7 +1784,7 @@ class DesignGuard
|
||||
|
||||
protected static function packageRelativeName(string $file): string
|
||||
{
|
||||
$root = static::packageCssRoot().'/';
|
||||
$root = static::packagePath('css').'/';
|
||||
|
||||
return str_starts_with($file, $root) ? substr($file, strlen($root)) : $file;
|
||||
}
|
||||
@@ -1849,8 +1806,8 @@ class DesignGuard
|
||||
$vendor = base_path('vendor/nonameweb/livewire-material/resources/css');
|
||||
|
||||
// A Composer path repository may symlink the package: the import still goes through vendor/.
|
||||
if (realpath($vendor) === static::packageCssRoot() && str_starts_with($file, static::packageCssRoot().'/')) {
|
||||
$file = $vendor.substr($file, strlen(static::packageCssRoot()));
|
||||
if (realpath($vendor) === static::packagePath('css') && str_starts_with($file, static::packagePath('css').'/')) {
|
||||
$file = $vendor.substr($file, strlen(static::packagePath('css')));
|
||||
}
|
||||
|
||||
return static::relativeImportPath($entryDir, $file);
|
||||
|
||||
Reference in New Issue
Block a user