diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 073fa8de..ab44d696 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -1159,7 +1159,7 @@ In the `.css` files it is given (`material-scheme.css` skipped) it fails on a li ## Livewire traps -- Blade directives do not compile inside a component tag's attributes: `` reaches the browser as literal text. On a component tag use `{{ }}` and `:prop` bindings, or put the Alpine on a plain element inside the slot. +- Blade directives do not compile inside a component tag's attributes: `` reaches the browser as literal text. On a component tag use `{{ }}` and `:prop` bindings — `@js($value)` is `{{ \Illuminate\Support\Js::from($value) }}`, `@class([...])` is `:class="\Illuminate\Support\Arr::toCssClasses([...])"` — or put the Alpine on a plain element inside the slot. `DesignGuard` names the form for each directive. - Never pass `hidden`, a display utility or a position (`absolute`, `relative`) to a component: it is merged beside the component's own and whichever Tailwind emits last wins. Wrap the component in an element that carries it. A variant that only hides (`max-medium:hidden`) is safe. - `$attributes->wire('model')->value()` is `false`, not `null`, when there is no `wire:model`, and `filled(false)` is true. Normalise with `?: null`. - End every statement in a multi-line Alpine attribute with `;`: an inline `@if … @endif` inside it swallows the newline after it. diff --git a/src/Testing/DesignGuard.php b/src/Testing/DesignGuard.php index 58aab999..c67f9cbd 100644 --- a/src/Testing/DesignGuard.php +++ b/src/Testing/DesignGuard.php @@ -376,7 +376,7 @@ class DesignGuard } foreach ($this->directivesInComponentTags($contents) as [$line, $directive]) { - $violations[] = "{$where}:{$line} Blade directive `{$directive}` inside a component tag, where it does not compile"; + $violations[] = "{$where}:{$line} Blade directive `{$directive}` inside a component tag, where it does not compile — ".$this->directiveInTag($directive); } foreach ($this->forbiddenRoleProps($contents) as [$line, $what]) { @@ -1208,6 +1208,23 @@ class DesignGuard return $unknown; } + /** + * What a component tag takes in place of a directive: an attribute expression, which Blade + * compiles inside the tag. + */ + protected function directiveInTag(string $directive): string + { + return match ($directive) { + '@js' => 'use `{{ \\Illuminate\\Support\\Js::from(…) }}`', + '@json' => 'use `{{ json_encode(…) }}`', + '@class' => 'use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`', + '@style' => 'use `:style="\\Illuminate\\Support\\Arr::toCssStyles([…])"`', + '@entangle' => "use `\$wire.entangle('…')` in the Alpine expression", + '@disabled', '@checked', '@selected', '@readonly', '@required' => 'use `:'.substr($directive, 1).'="…"`', + default => 'use a `:prop` binding or `{{ }}`, or move it to a plain element inside the slot', + }; + } + /** * Blade compiles a component tag before its directives, so `` or * `x-show="ok(@js($v))"` on a component reaches the browser as literal text. Use `:class` diff --git a/tests/Feature/DesignGuardTest.php b/tests/Feature/DesignGuardTest.php index 9097bf30..26562d2a 100644 --- a/tests/Feature/DesignGuardTest.php +++ b/tests/Feature/DesignGuardTest.php @@ -29,11 +29,23 @@ it('finds what compiles to nothing', function () { 'views/page.blade.php:5 Tailwind palette colour `text-red-600` compiles to nothing — M3 paints with roles: an `md-ink-*` class, or `var(--md-sys-color-*)` in your own CSS', 'views/page.blade.php:6 1.x utility `focus-ring` compiles to nothing — use `md-focus-ring` (interaction.css)', 'views/page.blade.php:6 Tailwind colour utility `text-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS', - 'views/page.blade.php:8 Blade directive `@class` inside a component tag, where it does not compile', + 'views/page.blade.php:8 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`', "views/page.blade.php:8 Tailwind sizing utility `size-4` compiles to nothing — M3 keeps no size scale: `` sets a content column's measure, `` an icon's; anything else is a length in your own CSS", ]); }); +it('names what a component tag takes in place of each directive', function () { + expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/directives'))->violations()))->toBe([ + 'directives/tags.blade.php:1 Blade directive `@js` inside a component tag, where it does not compile — use `{{ \\Illuminate\\Support\\Js::from(…) }}`', + 'directives/tags.blade.php:2 Blade directive `@json` inside a component tag, where it does not compile — use `{{ json_encode(…) }}`', + 'directives/tags.blade.php:3 Blade directive `@class` inside a component tag, where it does not compile — use `:class="\\Illuminate\\Support\\Arr::toCssClasses([…])"`', + 'directives/tags.blade.php:4 Blade directive `@style` inside a component tag, where it does not compile — use `:style="\\Illuminate\\Support\\Arr::toCssStyles([…])"`', + "directives/tags.blade.php:5 Blade directive `@entangle` inside a component tag, where it does not compile — use `\$wire.entangle('…')` in the Alpine expression", + 'directives/tags.blade.php:6 Blade directive `@disabled` inside a component tag, where it does not compile — use `:disabled="…"`', + 'directives/tags.blade.php:7 Blade directive `@if` inside a component tag, where it does not compile — use a `:prop` binding or `{{ }}`, or move it to a plain element inside the slot', + ]); +}); + it('names the M3 breakpoint for a Tailwind prefix, with its 2.0.0 replacement', function () { expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/breakpoints'))->violations()))->toBe([ "breakpoints/layout.blade.php:1 Tailwind breakpoint `2xl:` compiles to nothing — M3's extra-large (1600px) is a layout component's `hide-below`/`hide-from`/`stack-below` prop, or `@media (width >= 1600px)` in your own CSS", diff --git a/tests/Fixtures/design-guard/directives/tags.blade.php b/tests/Fixtures/design-guard/directives/tags.blade.php new file mode 100644 index 00000000..671c35ad --- /dev/null +++ b/tests/Fixtures/design-guard/directives/tags.blade.php @@ -0,0 +1,7 @@ + + + $open]) /> + $open]) /> + + +