Count a row written by hand in the design guard's stylesheet checks
`data-md-list-row` on an application's own `<li>`, `<div>`, `<tr>` or `<x-row>` draws its hover, focus and press state layer, its cursor and its `data-md-selected` fill from list-item.css, but missingStylesheets() and unusedStylesheets() read only component tags and `->links()`. ReStride had the stylesheet only because table.css imports it: had its last `<x-table>` gone, every hand-made row would have lost its states and its fill without a finding, and importing list-item.css for the rows directly was reported unused. A Blade view outside the package's own `resources/views` that writes a hook in the new `HOOK_STYLESHEETS` now needs that hook's stylesheet in both checks, named at the earliest line that needs it. The one entry is `data-md-list-row` → `components/list-item.css`, except on `<x-card>` (any spelling), whose row card.css draws; the card's opening tag is blanked out before the hook is read, and a selector such as `[data-md-list-row]` or a name in backticks writes no row. Nothing else list-rows.js, the skill or the README has an application write needs a stylesheet of its own: `data-md-list-open` and a row's `data-md-selected` are drawn only through the row, a selected row in `<x-table>` by table.css, `data-md-dragged` by foundation.css's `md-state-layer` or card.css, `data-md-field-control` by field.css inside `<x-field>`. The constant's docblock says so, and the guard's docblocks, the README, the skill and UPGRADE.md describe the check. A fixture test reads a hand-made `<li>` and `<tr>` row and a card-only view (plain and namespaced `<x-card data-md-list-row>`, and a selector in a script) against both checks; it fails without the change. A second test scans the package's list-item and card views and expects no row finding; it fails when package views are read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b62c83f244
commit
471d927e64
@@ -152,7 +152,7 @@ it('uses only what compiles', function () {
|
||||
|
||||
The guard reads every path it is given and fails, with `path:line` and the 2.0.0 replacement, on any Tailwind utility or variant still in a view, PHP or JS file, a colour of the application's own Tailwind theme (`bg-brand`) included — none compiles, since the application carries no Tailwind — and on 1.x's own utilities (`type-body-md`, `text-meta`, `rounded-corner-lg`, `focus-ring`): a layout component and prop (`flex gap-4` → `<x-row gap="space200">`), an `md-*` class (`truncate` → `md-truncate`), or a token for the application's own CSS (`rounded-lg` → `var(--md-sys-shape-corner-lg)`). A class the application's own stylesheets declare is exempt. In the `.css` files it is given (`material-scheme.css` skipped) it fails on a literal colour, radius, shadow, font, easing or duration, and on a media query off M3's 600/840/1200/1600px; a `var()`, or a `calc()`/`min()`/`max()`/`clamp()` built on one, is fine. It also fails on unknown Material Symbol names and Blade directives written inside component tags. Markdown mail components (under `mail.markdown.paths`, `resources/views/vendor/mail` by default) are the mail theme's to style: their classes are not read as Tailwind, and a mail theme stylesheet there is neither checked for literals nor a source of exempt classes.
|
||||
|
||||
`missingStylesheets($cssEntry)` checks the entry's relative `@import` graph, followed through every package file's own imports, against the package tags the views render (unprefixed, under the configured prefix, or `<x-livewire-material::…>`) and `->links()`, and names each missing `@import` line once; a tag the application shadows with its own component is reported instead. It reads imports only, so leave `resource_path('css')` out of `scan()` while the stylesheets still hold literals. `forbidColours([...])` names roles the application leaves out, and fails wherever one (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds any pattern of your own, line by line.
|
||||
`missingStylesheets($cssEntry)` checks the entry's relative `@import` graph, followed through every package file's own imports, against the package tags the views render (unprefixed, under the configured prefix, or `<x-livewire-material::…>`), `->links()` and the rows they write by hand (`data-md-list-row` on anything but `<x-card>` needs `components/list-item.css`), and names each missing `@import` line once; a tag the application shadows with its own component is reported instead. It reads imports only, so leave `resource_path('css')` out of `scan()` while the stylesheets still hold literals. `forbidColours([...])` names roles the application leaves out, and fails wherever one (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds any pattern of your own, line by line.
|
||||
|
||||
## AI agents
|
||||
|
||||
|
||||
@@ -24,6 +24,15 @@
|
||||
family now too: `border-collapse`, `border-spacing-*`, `from-10%`, `via-none`, `fill-none`,
|
||||
`stroke-*`, `text-shadow-*`, `text-inherit`, and Tailwind 4.2's `mauve`, `olive`, `mist` and
|
||||
`taupe` palettes. A test that expects no findings may fail on them.
|
||||
- **`DesignGuard`** counts a row written by hand. `data-md-list-row` on an `<li>`, a `<div>`, a
|
||||
`<tr>` or `<x-row>` takes its hover, focus and press state layer and its selected fill from
|
||||
`components/list-item.css`, but `missingStylesheets()` and `unusedStylesheets()` read only tags
|
||||
and `->links()`: an application had the stylesheet through `table.css` or `<x-list-item>`, lost
|
||||
it without a finding when the last of those left its views, and an `@import` of `list-item.css`
|
||||
for its own rows was reported unused. A view of the application that writes `data-md-list-row`
|
||||
on anything but `<x-card>`, whose row card.css draws, now needs `list-item.css`; a selector such
|
||||
as `[data-md-list-row]` and the package's own views do not count. A test that expects no findings
|
||||
may name the `@import` to add, and a filter written around the unused report can go.
|
||||
- **A selected row written by hand** — `data-md-list-row` with `data-md-selected` on an `<li>`, a
|
||||
`<div>` or a `<tr>`, not `<x-list-item>` or `<x-card>` — is drawn in `secondary-container` with
|
||||
`on-secondary-container` ink again, as 1.x's `data-list-row` with `data-selected` was; 2.0.0
|
||||
|
||||
@@ -514,7 +514,7 @@ The `illustration` slot draws the application's own artwork in place of the shap
|
||||
|
||||
`variant`: `filled` (default, surface-container-highest), `elevated`, `outlined`; medium corner. Props `title`, `subtitle`, `separator`, `heading` (the title's element, `h3` by default: pass `h2` for a card straight under the page's `h1`); slots `figure` (full-bleed media), `menu` (top-end), `actions` (end-aligned). A card holds content and actions about one subject (M3); a table of many rows or a group of settings is a headed section, not a card. Choose its fill with `variant`, not a background of your own.
|
||||
|
||||
A card or list item that opens something is a **row**: `data-md-list-row` on it and `data-md-list-open` on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in `<a>` or use a stretched link. A row answers with the state layer and one step of elevation; its corner does not move. `data-md-selected` on a row that is not a card — your own `<li>`, `<div>` or `<tr>`, the one whose sheet is open — fills it secondary-container with on-secondary-container ink, the state layer over the fill (`<x-list-item selected>` writes it itself); a card takes no selected fill.
|
||||
A card or list item that opens something is a **row**: `data-md-list-row` on it and `data-md-list-open` on its one opener (the title link or a button). A press anywhere else on the row reaches the opener; its other controls keep their own presses. Never wrap a card in `<a>` or use a stretched link. A row answers with the state layer and one step of elevation; its corner does not move. `data-md-selected` on a row that is not a card — your own `<li>`, `<div>` or `<tr>`, the one whose sheet is open — fills it secondary-container with on-secondary-container ink, the state layer over the fill (`<x-list-item selected>` writes it itself); a card takes no selected fill. A row that is not a card draws its state layer and that fill from `components/list-item.css`, so the CSS entry imports it even where no `<x-list-item>` or `<x-table>` renders (the design guard's `missingStylesheets()` asks for it).
|
||||
|
||||
```blade
|
||||
<x-card variant="outlined" data-md-list-row wire:key="share-{{ $share->id }}">
|
||||
@@ -1212,7 +1212,7 @@ In the `.css` files it is given (`material-scheme.css` skipped) it fails on a li
|
||||
|
||||
Markdown mail components — whatever lies under `config('mail.markdown.paths')`, `resources/views/vendor/mail` by default — are drawn by the mail theme, not by your CSS entry, and its classes (`table`, `button`, `panel`, `break-all`) share Tailwind's names. So the guard skips the class checks there (it still reads their icon names, the directives in their component tags and your own bans), and a mail theme stylesheet under that path, which has to write literal values, is neither checked for literals nor a source of exempt classes.
|
||||
|
||||
`missingStylesheets($cssEntry)` follows the entry's relative `@import`s through every package file's own imports (`split-button.css` counts `button.css` and `menu.css` too; `tailwindcss` or a font URL is skipped) and checks them against the package tags a scanned view renders (plain, prefixed or `<x-livewire-material::…>`) and `->links()`; each missing stylesheet names its `@import` line once, and a tag the application shadows with its own component is reported instead — the application's component wins in Blade. It reads imports only: leave `resource_path('css')` out of `scan()` until the stylesheets are on tokens. `unusedStylesheets($cssEntry)` is the other way round: a package stylesheet the entry imports directly that no scanned view needs, even through a needed stylesheet's own imports, named at its `@import` line to remove (an entry importing `all.css` is left alone). `forbidColours([...])` fails wherever a left-out role (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds a pattern of your own.
|
||||
`missingStylesheets($cssEntry)` follows the entry's relative `@import`s through every package file's own imports (`split-button.css` counts `button.css` and `menu.css` too; `tailwindcss` or a font URL is skipped) and checks them against the package tags a scanned view renders (plain, prefixed or `<x-livewire-material::…>`), `->links()`, and a row a view of yours writes by hand (`data-md-list-row` on anything but `<x-card>`, which needs `components/list-item.css`; the package's own views are not read for it); each missing stylesheet names its `@import` line once, and a tag the application shadows with its own component is reported instead — the application's component wins in Blade. It reads imports only: leave `resource_path('css')` out of `scan()` until the stylesheets are on tokens. `unusedStylesheets($cssEntry)` is the other way round: a package stylesheet the entry imports directly that no scanned view needs — no tag, `->links()` or row written by hand — even through a needed stylesheet's own imports, named at its `@import` line to remove (an entry importing `all.css` is left alone). `forbidColours([...])` fails wherever a left-out role (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds a pattern of your own.
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
+140
-44
@@ -28,13 +28,16 @@ use Symfony\Component\Finder\Finder;
|
||||
* (ii) `missingStylesheets($cssEntry)`: a package component tag used in a view — unprefixed,
|
||||
* under the configured prefix, or `<x-livewire-material::…>` — whose stylesheet the
|
||||
* entry's relative `@import` graph does not reach (followed through every package file's
|
||||
* own imports; a package name or URL the entry also imports is skipped, never fatal), and
|
||||
* `->links()` needing `pagination.css`; each names the missing `@import` line once, at its
|
||||
* first use. A tag the application shadows with its own component of the same name is
|
||||
* reported instead — the application's component wins in Blade, so the package's
|
||||
* stylesheet is moot. `unusedStylesheets($cssEntry)` reports the other way: a package
|
||||
* stylesheet the entry imports directly that no scanned view needs, not even through
|
||||
* another needed stylesheet's imports (an entry importing `all.css` is left alone).
|
||||
* own imports; a package name or URL the entry also imports is skipped, never fatal),
|
||||
* `->links()` needing `pagination.css`, and a hook an application's view writes on markup
|
||||
* of its own whose rules a component's stylesheet holds — `data-md-list-row` on anything
|
||||
* but `<x-card>` needing `list-item.css` (`HOOK_STYLESHEETS`); each names the missing
|
||||
* `@import` line once, at its first use. A tag the application shadows with its own
|
||||
* component of the same name is reported instead — the application's component wins in
|
||||
* Blade, so the package's stylesheet is moot. `unusedStylesheets($cssEntry)` reports the
|
||||
* other way: a package stylesheet the entry imports directly that no scanned view needs,
|
||||
* not even through another needed stylesheet's imports (an entry importing `all.css` is
|
||||
* left alone).
|
||||
* (iii) every `.css` file among the scanned paths, outside the package and excluding the
|
||||
* generated `material-scheme.css`: a literal colour, radius, shadow, font size, weight,
|
||||
* line height, letter spacing, easing or duration, and a media query at a width other
|
||||
@@ -225,6 +228,24 @@ class DesignGuard
|
||||
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-]+\)))?$/';
|
||||
|
||||
/**
|
||||
* The hooks an application writes on markup of its own, not through a component tag, whose
|
||||
* rules live in a component's stylesheet, and that stylesheet (check ii). `data-md-list-row`
|
||||
* on an `<li>`, a `<div>`, a `<tr>` or a layout component such as `<x-row>` draws its hover,
|
||||
* focus and press state layer and its `data-md-selected` fill from list-item.css, which no tag
|
||||
* asks for when the page renders no `<x-list-item>` and no `<x-table>`. `except` names the
|
||||
* package tags that draw the hook themselves: `<x-card data-md-list-row>` is card.css's row.
|
||||
*
|
||||
* Nothing else that list-rows.js, the skill or the README has an application write needs a
|
||||
* stylesheet of its own: `data-md-list-open` and a row's `data-md-selected` are drawn only
|
||||
* through the row, a selected row in `<x-table>` by table.css, `data-md-dragged` by
|
||||
* foundation.css's `md-state-layer` (and card.css on `<x-card>`), and `data-md-field-control`
|
||||
* by field.css inside `<x-field>`.
|
||||
*/
|
||||
protected const array HOOK_STYLESHEETS = [
|
||||
'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',
|
||||
@@ -335,15 +356,16 @@ class DesignGuard
|
||||
|
||||
/**
|
||||
* Check (ii): every package component tag used in a scanned view — unprefixed, under the
|
||||
* configured prefix, or `<x-livewire-material::…>` — and every `->links()` call, against
|
||||
* `$cssEntry`'s `@import` graph (followed through each package file's own imports). A missing
|
||||
* one names the `@import` line to add; a tag the application shadows with its own component
|
||||
* of the same name is reported instead, since the package's stylesheet is then moot. Each
|
||||
* missing stylesheet and each shadowed tag is reported once, at its first use. Only the
|
||||
* imports are read here: the literal values in the application's own CSS are check (iii)'s,
|
||||
* which reads the `.css` files `scan()` is given, entry or not — so an application part-way
|
||||
* through its migration can check its imports before its stylesheets are on tokens. The
|
||||
* classes the entry's own imports declare do join check (i)'s exemptions.
|
||||
* configured prefix, or `<x-livewire-material::…>` — every `->links()` call, and every hook in
|
||||
* `HOOK_STYLESHEETS` a view outside the package writes on its own markup (a row's
|
||||
* `data-md-list-row`), against `$cssEntry`'s `@import` graph (followed through each package
|
||||
* file's own imports). A missing one names the `@import` line to add; a tag the application
|
||||
* shadows with its own component of the same name is reported instead, since the package's
|
||||
* stylesheet is then moot. Each missing stylesheet and each shadowed tag is reported once, at
|
||||
* its first use. Only the imports are read here: the literal values in the application's own
|
||||
* CSS are check (iii)'s, which reads the `.css` files `scan()` is given, entry or not — so an
|
||||
* application part-way through its migration can check its imports before its stylesheets
|
||||
* are on tokens. The classes the entry's own imports declare do join check (i)'s exemptions.
|
||||
*/
|
||||
public function missingStylesheets(string $cssEntry): static
|
||||
{
|
||||
@@ -355,10 +377,11 @@ class DesignGuard
|
||||
|
||||
/**
|
||||
* Check (ii) the other way round: each package stylesheet `$cssEntry` imports directly that no
|
||||
* scanned Blade view needs — no package tag it renders, no `->links()`, and not reached through
|
||||
* the imports of a stylesheet that is needed — named at its `@import` line, to remove. A
|
||||
* component whose last use left the views otherwise keeps its CSS in every page. `foundation.css`
|
||||
* is always needed; an entry that imports `all.css` has chosen everything and is not read.
|
||||
* scanned Blade view needs — no package tag it renders, no `->links()`, no hook it writes by
|
||||
* hand (`HOOK_STYLESHEETS`), and not reached through the imports of a stylesheet that is
|
||||
* needed — named at its `@import` line, to remove. A component whose last use left the views
|
||||
* otherwise keeps its CSS in every page. `foundation.css` is always needed; an entry that
|
||||
* imports `all.css` has chosen everything and is not read.
|
||||
*/
|
||||
public function unusedStylesheets(string $cssEntry): static
|
||||
{
|
||||
@@ -405,6 +428,7 @@ 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()]));
|
||||
|
||||
if ($isBlade) {
|
||||
$contents = $this->withoutBladeComments($contents);
|
||||
@@ -432,13 +456,13 @@ class DesignGuard
|
||||
}
|
||||
|
||||
if ($this->cssEntry !== null) {
|
||||
foreach ($this->missingStylesheetViolations($contents, $resolved, $reported) as [$line, $what]) {
|
||||
foreach ($this->missingStylesheetViolations($contents, $resolved, $reported, $readsHooks) as [$line, $what]) {
|
||||
$violations[] = "{$where}:{$line} {$what}";
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->unusedEntry !== null) {
|
||||
$needed = [...$needed, ...$this->neededStylesheets($contents)];
|
||||
$needed = [...$needed, ...$this->neededStylesheets($contents, $readsHooks)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,12 +495,13 @@ class DesignGuard
|
||||
}
|
||||
|
||||
/**
|
||||
* The package stylesheets a Blade view's own tags and `->links()` calls need, before their
|
||||
* imports are followed.
|
||||
* The package stylesheets a Blade view's own tags, `->links()` calls and, unless `$readsHooks`
|
||||
* is false (a package view), the hooks it writes by hand need, before their imports are
|
||||
* followed.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
protected function neededStylesheets(string $contents): array
|
||||
protected function neededStylesheets(string $contents, bool $readsHooks = true): array
|
||||
{
|
||||
$needed = [];
|
||||
|
||||
@@ -490,6 +515,10 @@ class DesignGuard
|
||||
}
|
||||
}
|
||||
|
||||
if ($readsHooks) {
|
||||
$needed = [...$needed, ...array_column($this->hookUsages($contents), 2)];
|
||||
}
|
||||
|
||||
if ($this->paginationUsages($contents) !== []) {
|
||||
$needed[] = static::packageCssRoot().'/components/pagination.css';
|
||||
}
|
||||
@@ -1502,19 +1531,74 @@ class DesignGuard
|
||||
return array_map(fn (array $match): int => substr_count(substr($contents, 0, $match[1]), "\n") + 1, $matches[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every hook in `HOOK_STYLESHEETS` a Blade view writes, each as [line, hook, stylesheet]: an
|
||||
* attribute of a plain element or of a component tag other than those `except` names (blanked
|
||||
* out first), or a name a PHP array or a script sets. A selector (`[data-md-list-row]`), a name
|
||||
* in backticks and a longer name that starts with the hook write no hook.
|
||||
*
|
||||
* @return list<array{0: int, 1: string, 2: string}>
|
||||
*/
|
||||
protected function hookUsages(string $contents): array
|
||||
{
|
||||
$found = [];
|
||||
|
||||
foreach (self::HOOK_STYLESHEETS as $hook => ['stylesheet' => $stylesheet, 'except' => $except]) {
|
||||
if (! str_contains($contents, $hook)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$markup = $this->withoutPackageTags($contents, $except);
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* `$contents` with the opening tag of each package component `$names` names — plain, under the
|
||||
* configured prefix or `<x-livewire-material::…>` — blanked out, line breaks kept. A quoted
|
||||
* attribute value or a `{{ … }}` inside the tag may hold a `>`.
|
||||
*
|
||||
* @param list<string> $names
|
||||
*/
|
||||
protected function withoutPackageTags(string $contents, array $names): string
|
||||
{
|
||||
if ($names === []) {
|
||||
return $contents;
|
||||
}
|
||||
|
||||
$prefix = (string) config('livewire-material.prefix', '');
|
||||
$namespaces = $prefix !== '' ? ['livewire-material', $prefix] : ['livewire-material'];
|
||||
|
||||
return (string) preg_replace_callback(
|
||||
'/<x-(?:(?:'.$this->alternation($namespaces).')::)?(?:'.$this->alternation($names).')(?=[\s\/>])(?:\{\{.*?\}\}|"[^"]*"|\'[^\']*\'|[^>"\'])*+>?/s',
|
||||
fn (array $tag): string => (string) preg_replace('/[^\n]/', ' ', $tag[0]),
|
||||
$contents,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check (ii)'s findings for one file: a package tag whose stylesheet `$resolved` (every file
|
||||
* `Stylesheets::resolvedFiles()` reached from the CSS entry, keyed by real path) does not
|
||||
* contain, a tag the application shadows, and a `->links()` needing `pagination.css` — each
|
||||
* only the first time `$reported` (shared across every file of one `violations()` run) sees it.
|
||||
* contain, a tag the application shadows, a hook written by hand whose stylesheet it does not
|
||||
* contain (unless `$readsHooks` is false, for a package view), and a `->links()` needing
|
||||
* `pagination.css` — each only the first time `$reported` (shared across every file of one
|
||||
* `violations()` run) sees it, at the earliest line in the file that needs it.
|
||||
*
|
||||
* @param array<string, true> $resolved
|
||||
* @param array<string, true> $reported
|
||||
* @return list<array{0: int, 1: string}>
|
||||
*/
|
||||
protected function missingStylesheetViolations(string $contents, array $resolved, array &$reported): array
|
||||
protected function missingStylesheetViolations(string $contents, array $resolved, array &$reported, bool $readsHooks = true): array
|
||||
{
|
||||
$found = [];
|
||||
$needs = [];
|
||||
|
||||
foreach ($this->packageTagUsages($contents) as [$line, $name, $spelling]) {
|
||||
if ($spelling === 'plain' && $this->shadowedByApplication($name)) {
|
||||
@@ -1526,29 +1610,32 @@ class DesignGuard
|
||||
continue;
|
||||
}
|
||||
|
||||
$stylesheet = static::packageStylesheetFor($name);
|
||||
|
||||
if ($stylesheet !== null && ! isset($resolved[$stylesheet]) && ! isset($reported[$stylesheet])) {
|
||||
$reported[$stylesheet] = true;
|
||||
$found[] = [$line, sprintf(
|
||||
"`<x-%s>` needs `%s`, missing from %s — add `@import '%s';`",
|
||||
$name,
|
||||
static::packageRelativeName($stylesheet),
|
||||
$this->relative($this->cssEntry),
|
||||
$this->importLine($stylesheet),
|
||||
)];
|
||||
if (($stylesheet = static::packageStylesheetFor($name)) !== null) {
|
||||
$needs[] = [$line, "`<x-{$name}>`", $stylesheet];
|
||||
}
|
||||
}
|
||||
|
||||
$pagination = static::packageCssRoot().'/components/pagination.css';
|
||||
if ($readsHooks) {
|
||||
foreach ($this->hookUsages($contents) as [$line, $hook, $stylesheet]) {
|
||||
$needs[] = [$line, "`{$hook}`", $stylesheet];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->paginationUsages($contents) as $line) {
|
||||
if (! isset($resolved[$pagination]) && ! isset($reported[$pagination])) {
|
||||
$reported[$pagination] = true;
|
||||
$needs[] = [$line, '`->links()`', static::packageCssRoot().'/components/pagination.css'];
|
||||
}
|
||||
|
||||
usort($needs, fn (array $a, array $b): int => $a[0] <=> $b[0]);
|
||||
|
||||
foreach ($needs as [$line, $what, $stylesheet]) {
|
||||
if (! isset($resolved[$stylesheet]) && ! isset($reported[$stylesheet])) {
|
||||
$reported[$stylesheet] = true;
|
||||
$found[] = [$line, sprintf(
|
||||
"`->links()` needs `components/pagination.css`, missing from %s — add `@import '%s';`",
|
||||
"%s needs `%s`, missing from %s — add `@import '%s';`",
|
||||
$what,
|
||||
static::packageRelativeName($stylesheet),
|
||||
$this->relative($this->cssEntry),
|
||||
$this->importLine($pagination),
|
||||
$this->importLine($stylesheet),
|
||||
)];
|
||||
}
|
||||
}
|
||||
@@ -1699,6 +1786,15 @@ class DesignGuard
|
||||
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');
|
||||
}
|
||||
|
||||
protected static function isPackageTag(string $name): bool
|
||||
{
|
||||
$root = static::packageComponentViewRoot();
|
||||
|
||||
@@ -583,6 +583,39 @@ it('needs pagination.css when a view calls ->links()', function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it('needs list-item.css for a row written by hand, and leaves a card row to card.css', function () {
|
||||
// rows.blade.php writes `data-md-list-row` on an <li> and a <tr> and renders no package tag;
|
||||
// card-rows.blade.php writes it only on <x-card>, whose row card.css draws, and in a selector.
|
||||
$rows = realpath(GUARD_FIXTURES.'/stylesheets/views/rows.blade.php');
|
||||
$cardRows = realpath(GUARD_FIXTURES.'/stylesheets/views/card-rows.blade.php');
|
||||
$foundationOnly = realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css');
|
||||
$entry = realpath(GUARD_FIXTURES.'/stylesheets/rows.css');
|
||||
|
||||
expect(fixtureRelative(DesignGuard::scan($rows)->missingStylesheets($foundationOnly)->violations()))->toBe([
|
||||
"stylesheets/views/rows.blade.php:3 `data-md-list-row` needs `components/list-item.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/list-item.css';`",
|
||||
])
|
||||
->and(fixtureRelative(DesignGuard::scan($rows)->unusedStylesheets($entry)->violations()))->toBe([
|
||||
"stylesheets/rows.css:2 `components/card.css` is imported, but no scanned view renders a component that needs it — remove `@import '../../../../resources/css/components/card.css';`",
|
||||
])
|
||||
->and(fixtureRelative(DesignGuard::scan($cardRows)->missingStylesheets($foundationOnly)->violations()))->toBe([
|
||||
"stylesheets/views/card-rows.blade.php:1 `<x-card>` needs `components/card.css`, missing from stylesheets/foundation-only.css — add `@import '../../../../resources/css/components/card.css';`",
|
||||
])
|
||||
->and(fixtureRelative(DesignGuard::scan($cardRows)->unusedStylesheets($entry)->violations()))->toBe([
|
||||
"stylesheets/rows.css:3 `components/list-item.css` is imported, but no scanned view renders a component that needs it — remove `@import '../../../../resources/css/components/list-item.css';`",
|
||||
]);
|
||||
});
|
||||
|
||||
it('reads a row written by hand in the application\'s views only, not in the package\'s own', function () {
|
||||
// list-item.blade.php and card.blade.php write `data-md-list-row` for the tags that render them.
|
||||
$components = realpath(__DIR__.'/../../resources/views/components');
|
||||
|
||||
$violations = DesignGuard::scan([$components.'/list-item.blade.php', $components.'/card.blade.php'])
|
||||
->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/foundation-only.css'))
|
||||
->violations();
|
||||
|
||||
expect(array_values(array_filter($violations, fn (string $violation): bool => str_contains($violation, '`data-md-list-row`'))))->toBe([]);
|
||||
});
|
||||
|
||||
it('reports a package tag the application shadows with its own anonymous component', function () {
|
||||
$views = sys_get_temp_dir().'/livewire-material-guard-shadow-'.uniqid();
|
||||
File::ensureDirectoryExists($views.'/components');
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
@import '../../../../resources/css/foundation.css';
|
||||
@import '../../../../resources/css/components/card.css';
|
||||
@import '../../../../resources/css/components/list-item.css';
|
||||
@@ -0,0 +1,9 @@
|
||||
<x-card data-md-list-row>
|
||||
<a href="#" data-md-list-open>Report</a>
|
||||
</x-card>
|
||||
<x-livewire-material::card wire:key="report-{{ $report->id }}" :data-md-list-row="$report->opens()">
|
||||
<a href="#" data-md-list-open>Report</a>
|
||||
</x-livewire-material::card>
|
||||
<script>
|
||||
document.querySelectorAll('[data-md-list-row]').forEach((row) => row.focus())
|
||||
</script>
|
||||
@@ -0,0 +1,11 @@
|
||||
{{-- A row written by hand: `data-md-list-row` in a comment writes none. --}}
|
||||
<ul>
|
||||
<li data-md-list-row @if ($open) data-md-selected @endif>
|
||||
<button type="button" wire:click="open" data-md-list-open>Report</button>
|
||||
</li>
|
||||
</ul>
|
||||
<table>
|
||||
<tr data-md-list-row data-md-selected>
|
||||
<td><a href="#" data-md-list-open>Report</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
Reference in New Issue
Block a user