Leave a Markdown mail component's classes to the mail theme in the design guard
A mail component is drawn by the mail theme, not by the application's CSS entry, and the theme's classes share Tailwind's names: ReStride's `x-mail::sessions` wraps its table in `<div class="table">`, the class the package's own mail theme styles, and the guard reported it as Tailwind's `display: table`. A view under a path `mail.markdown.paths` names (Laravel's `resources/views/vendor/mail` by default) now skips family (i) and the breakpoint, scale, palette and colour-value checks; its icon names, the directives in its component tags, forbidColours() and forbid() are still read. A mail theme stylesheet under that path, which has to write literal values, is left out of check (iii) and of the exempt classes, so its `.table` no longer hides `table` in the application's other views either. The guard's docblock, the README and the skill's "Testing the design" say so; a fixture test reads the same mail view and theme with and without the path configured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7a95449c82
commit
0641071a06
@@ -150,7 +150,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.
|
||||
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.
|
||||
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
it in the layout, `toggle()` opens and closes the modal rail (`show()`, `hide()`) instead of
|
||||
changing a choice nothing draws there. A shortcut that pressed the rail's menu button to get this
|
||||
can call `$store.rail.toggle()` again.
|
||||
- **`DesignGuard`** no longer reads a Markdown mail component's classes as Tailwind: a view under
|
||||
`mail.markdown.paths` (`resources/views/vendor/mail` by default) is drawn by the mail theme, whose
|
||||
`table`, `button` and `panel` share Tailwind's names, so only its icon names, the directives in
|
||||
its component tags and the application's own bans are checked there. A mail theme stylesheet
|
||||
under that path is no longer checked for literal values, and its classes no longer exempt the
|
||||
same names in the application's other views. A filter an application wrote around such a
|
||||
finding can go.
|
||||
- **`DesignGuard`** reports a colour utility on a colour the application's own Tailwind theme
|
||||
named (`bg-brand`, `text-sport-run`, `from-brand`, `border-l-zone-4`, `bg-brand/8`) in a class
|
||||
list, with `var(--…)` of the application's own as its replacement, unless the application's
|
||||
|
||||
@@ -1207,6 +1207,8 @@ It reads every path it is given and fails with `path:line` on unknown symbol nam
|
||||
|
||||
In the `.css` files it is given (`material-scheme.css` skipped) it fails on a literal colour, radius, shadow, font, font size, weight, line height, letter spacing, easing or duration, and on a media query width off 600/840/1200/1600px; a `var()`, or a `calc()`/`min()`/`max()`/`clamp()` built on one, is fine.
|
||||
|
||||
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.
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -47,6 +47,15 @@ use Symfony\Component\Finder\Finder;
|
||||
* at runtime (`'text-'.$tone`) or hidden in a comment stays invisible — the same reason
|
||||
* to write class names out whole.
|
||||
*
|
||||
* A Markdown mail component — a view under a path `mail.markdown.paths` names, Laravel's
|
||||
* `resources/views/vendor/mail` by default — is drawn by the mail theme, not by the application's
|
||||
* CSS entry, and the theme's own classes (`table`, `button`, `panel`, `break-all`) share a
|
||||
* Tailwind utility's name. So family (i) and the breakpoint, scale, palette and colour-value
|
||||
* checks skip those views, while their icon names, the directives in their component tags and
|
||||
* the application's own bans are still read; and a stylesheet under that path, a mail theme that
|
||||
* has to write literal values because mail clients read no custom property, is neither check
|
||||
* (iii)'s nor a source of exempt classes.
|
||||
*
|
||||
* expect(DesignGuard::scan([resource_path('views'), resource_path('js'), resource_path('css'), app_path()])
|
||||
* ->missingStylesheets(resource_path('css/app.css'))
|
||||
* ->forbidColours(['tertiary'])
|
||||
@@ -385,6 +394,8 @@ class DesignGuard
|
||||
}
|
||||
}
|
||||
|
||||
$mailPaths = $this->mailComponentPaths();
|
||||
|
||||
foreach ($this->files() as $file) {
|
||||
if (str_ends_with($file->getFilename(), '.css')) {
|
||||
continue; // A directly-scanned CSS file is check (iii)'s alone; these checks read Blade, PHP and JS.
|
||||
@@ -393,12 +404,13 @@ class DesignGuard
|
||||
$contents = (string) file_get_contents($file->getPathname());
|
||||
$where = $this->relative($file->getPathname());
|
||||
$isBlade = str_ends_with($file->getFilename(), '.blade.php');
|
||||
$readsClasses = ! $this->isUnder($file->getPathname(), $mailPaths);
|
||||
|
||||
if ($isBlade) {
|
||||
$contents = $this->withoutBladeComments($contents);
|
||||
}
|
||||
|
||||
if (str_ends_with($file->getFilename(), '.php')) {
|
||||
if ($readsClasses && str_ends_with($file->getFilename(), '.php')) {
|
||||
foreach ($this->literalClasses($contents) as [$line, $class]) {
|
||||
if (($hint = $this->tailwindFamilyHint($class)) !== null) {
|
||||
$violations[] = "{$where}:{$line} {$hint}";
|
||||
@@ -433,13 +445,13 @@ class DesignGuard
|
||||
foreach (explode("\n", $contents) as $index => $text) {
|
||||
$line = $index + 1;
|
||||
|
||||
if (preg_match_all($this->colourPattern(), $text, $matches)) {
|
||||
if ($readsClasses && preg_match_all($this->colourPattern(), $text, $matches)) {
|
||||
foreach ($matches[0] as $class) {
|
||||
$violations[] = "{$where}:{$line} Tailwind palette colour `{$class}` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS";
|
||||
}
|
||||
}
|
||||
|
||||
foreach ([...$this->offTheTokens($text), ...$this->forbiddenRoles($text)] as $what) {
|
||||
foreach ([...($readsClasses ? $this->offTheTokens($text) : []), ...$this->forbiddenRoles($text)] as $what) {
|
||||
$violations[] = "{$where}:{$line} {$what}";
|
||||
}
|
||||
|
||||
@@ -1546,8 +1558,9 @@ class DesignGuard
|
||||
|
||||
/**
|
||||
* Every `.css` file this guard reads for check (iii): the ones `scan()`'s paths hold, outside
|
||||
* the package, the generated `material-scheme.css` excluded. The CSS entry's imports are not
|
||||
* followed here — a vendor stylesheet it pulls in is not the application's to put on tokens.
|
||||
* the package, the generated `material-scheme.css` and a mail theme under a mail component path
|
||||
* excluded. The CSS entry's imports are not followed here — a vendor stylesheet it pulls in is
|
||||
* not the application's to put on tokens.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
@@ -1571,12 +1584,44 @@ class DesignGuard
|
||||
}
|
||||
}
|
||||
|
||||
$mailPaths = $this->mailComponentPaths();
|
||||
|
||||
return array_values(array_unique(array_filter(
|
||||
$files,
|
||||
fn (string $file): bool => $file !== '' && ! static::isPackageFile($file) && ! $this->isGeneratedScheme($file),
|
||||
fn (string $file): bool => $file !== '' && ! static::isPackageFile($file) && ! $this->isGeneratedScheme($file) && ! $this->isUnder($file, $mailPaths),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* The Markdown mail component paths Laravel renders mail from (`mail.markdown.paths`), as real
|
||||
* paths; one that does not exist is left out. See the class header.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
protected function mailComponentPaths(): array
|
||||
{
|
||||
return array_values(array_filter(array_map(
|
||||
fn (mixed $path): string => is_string($path) ? (string) realpath($path) : '',
|
||||
(array) config('mail.markdown.paths', []),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $directories Real paths.
|
||||
*/
|
||||
protected function isUnder(string $file, array $directories): bool
|
||||
{
|
||||
$file = realpath($file) ?: $file;
|
||||
|
||||
foreach ($directories as $directory) {
|
||||
if (str_starts_with($file, rtrim($directory, '/').'/')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every class an application's own stylesheets select on — the scanned ones and whatever the
|
||||
* CSS entry imports outside the package — check (i)'s exemption list, so a class it defines is
|
||||
|
||||
@@ -336,6 +336,35 @@ it('reports a colour utility on the application\'s own theme colour, and a Tailw
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves a Markdown mail component\'s classes and its theme to the mail theme, but still reads its icons and directives', function () {
|
||||
$mail = realpath(GUARD_FIXTURES.'/mail/views');
|
||||
$table = 'Tailwind display utility `table` compiles to nothing — write the `display` rule in your own CSS';
|
||||
$icon = 'mail/views/vendor/mail/html/sessions.blade.php:7 unknown Material Symbol `not_a_symbol`';
|
||||
$directive = 'mail/views/vendor/mail/html/sessions.blade.php:8 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`';
|
||||
|
||||
config(['mail.markdown.paths' => [$mail.'/vendor/mail']]);
|
||||
|
||||
// The theme's `.table` exempts nothing outside the mail components, and its literals are its own.
|
||||
expect(fixtureRelative(DesignGuard::scan($mail)->violations()))->toBe([
|
||||
"mail/views/page.blade.php:1 {$table}",
|
||||
$icon,
|
||||
$directive,
|
||||
]);
|
||||
|
||||
// Outside a mail component path the same files read as the application's own: the theme's
|
||||
// classes as Tailwind, its literals as check (iii)'s, and its `.table` exempting `table`.
|
||||
config(['mail.markdown.paths' => []]);
|
||||
|
||||
expect(fixtureRelative(DesignGuard::scan($mail)->violations()))->toBe([
|
||||
'mail/views/vendor/mail/html/sessions.blade.php:3 value outside the M3 scale `text-sm` — use one of the `md-type-*` classes (text.css), which set size, line height and tracking together',
|
||||
'mail/views/vendor/mail/html/sessions.blade.php:4 Tailwind text utility `break-all` compiles to nothing — write the rule in your own CSS',
|
||||
$icon,
|
||||
$directive,
|
||||
'mail/views/vendor/mail/html/themes/default.css:2 literal colour `#3d4852` — use `var(--md-sys-color-*)`',
|
||||
'mail/views/vendor/mail/html/themes/default.css:3 literal font size `font-size: 14px` — set the whole style with `font: var(--md-sys-typescale-*)` and its `-tracking`, or an `md-type-*` class',
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports an arbitrary [] value once, whatever utility it modifies, and strips a trailing !', function () {
|
||||
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/tailwind/arbitrary.blade.php'))->violations());
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<div class="table"></div>
|
||||
@@ -0,0 +1,8 @@
|
||||
@props(['sessions', 'url'])
|
||||
<div class="table">
|
||||
<table class="sessions text-sm" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr><td class="break-all">{{ $sessions }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<x-mail::button :url="$url" icon="not_a_symbol">Open the week</x-mail::button>
|
||||
<x-mail::panel @class(['wide'])>A panel</x-mail::panel>
|
||||
@@ -0,0 +1,4 @@
|
||||
.table th {
|
||||
color: #3d4852;
|
||||
font-size: 14px;
|
||||
}
|
||||
Reference in New Issue
Block a user