Name what a component tag takes in place of each Blade directive

The guard said a directive does not compile inside a component tag but
not what to write instead, and the skill's note said only "use {{ }}".
SealShare's two-factor challenge had @js in an x-data on <x-stack>, and
its implementer had to work out {{ Js::from() }} from Laravel's source.
Each message now names the form: Js::from for @js, json_encode for
@json, Arr::toCssClasses and toCssStyles for @class and @style,
$wire.entangle for @entangle, a bound attribute for @disabled and its
kin, and a prop, an echo or a plain element for control structures
(plan step 46).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-15 11:13:49 +02:00
co-authored by Claude Opus 5
parent d0bed0da69
commit a5ad6af698
4 changed files with 39 additions and 3 deletions
@@ -1159,7 +1159,7 @@ In the `.css` files it is given (`material-scheme.css` skipped) it fails on a li
## Livewire traps ## Livewire traps
- Blade directives do not compile inside a component tag's attributes: `<x-foo x-show="ok(@js($value))">` 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: `<x-foo x-show="ok(@js($value))">` 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. - 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`. - `$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. - End every statement in a multi-line Alpine attribute with `;`: an inline `@if … @endif` inside it swallows the newline after it.
+18 -1
View File
@@ -376,7 +376,7 @@ class DesignGuard
} }
foreach ($this->directivesInComponentTags($contents) as [$line, $directive]) { 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]) { foreach ($this->forbiddenRoleProps($contents) as [$line, $what]) {
@@ -1208,6 +1208,23 @@ class DesignGuard
return $unknown; 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 `<x-icon @class([...])>` or * Blade compiles a component tag before its directives, so `<x-icon @class([...])>` or
* `x-show="ok(@js($v))"` on a component reaches the browser as literal text. Use `:class` * `x-show="ok(@js($v))"` on a component reaches the browser as literal text. Use `:class`
+13 -1
View File
@@ -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: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 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: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: `<x-pane width>` sets a content column's measure, `<x-icon size>` an icon's; anything else is a length in your own CSS", "views/page.blade.php:8 Tailwind sizing utility `size-4` compiles to nothing — M3 keeps no size scale: `<x-pane width>` sets a content column's measure, `<x-icon size>` 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 () { 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([ 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", "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",
@@ -0,0 +1,7 @@
<x-button x-data="{ open: @js($open) }" />
<x-button x-data="{ items: @json($items) }" />
<x-icon name="home" @class(['is-open' => $open]) />
<x-icon name="home" @style(['inline-size: 2rem' => $open]) />
<x-input x-data="{ value: @entangle('value') }" />
<x-button @disabled($locked) />
<x-card @if ($open) outlined @endif />