From 2f530cf2033d0e4d47346205f714c548bd579a7e Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Tue, 12 Aug 2025 14:31:24 +0100 Subject: [PATCH 1/5] Improve Livewire guidelines From e3c485e546fbf457d4dd79fa72f455754ec4d842 Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Tue, 12 Aug 2025 19:11:32 +0100 Subject: [PATCH 2/5] guidelines: livewire: wip --- .ai/{tailwindcss/3 => livewire/2}/.gitkeep | 0 .ai/livewire/3/core.blade.php | 40 ++++------------------ .ai/{tailwindcss => livewire}/4/.gitkeep | 0 .ai/livewire/core.blade.php | 16 +++++++++ .ai/tailwindcss/3/core.blade.php | 1 - .ai/tailwindcss/4/core.blade.php | 1 - .ai/tailwindcss/core.blade.php | 1 + .ai/volt/core.blade.php | 5 +-- src/Mcp/Tools/SearchDocs.php | 20 ++++++----- 9 files changed, 36 insertions(+), 48 deletions(-) rename .ai/{tailwindcss/3 => livewire/2}/.gitkeep (100%) rename .ai/{tailwindcss => livewire}/4/.gitkeep (100%) diff --git a/.ai/tailwindcss/3/.gitkeep b/.ai/livewire/2/.gitkeep similarity index 100% rename from .ai/tailwindcss/3/.gitkeep rename to .ai/livewire/2/.gitkeep diff --git a/.ai/livewire/3/core.blade.php b/.ai/livewire/3/core.blade.php index 2fde0f8..cfd58ac 100644 --- a/.ai/livewire/3/core.blade.php +++ b/.ai/livewire/3/core.blade.php @@ -1,36 +1,8 @@ - #### Key Changes from Livewire 2 +- These changed in Livewire 2, but may not have in this project. Verify this project's setup to ensure you conform with conventions. -- **Namespace**: Components now use `App\Livewire` (not `App\Http\Livewire`) -- **Events**: Use `$this->dispatch()` (not `emit` or `dispatchBrowserEvent`) -- **Layout path**: `components.layouts.app` (not `layouts.app`) -- **Deferred by default**: Use `wire:model.live` for real-time updates -- **Alpine included**: Don't manually include Alpine.js - -#### Livewire Best Practices - -- **Single root element** in Blade components -- **Add wire:key** in loops: - -@verbatim -```blade -@foreach ($items as $item) -
- {{ $item->name }} -
-@endforeach -``` -@endverbatim - -- **Use attributes** for event listeners: - -```php -#[On('todo-created')] -public function refreshList() -{ -// ... -} -``` - -- **Loading states**: Use `wire:loading` and `wire:dirty` -- **Confirmations**: Use `wire:confirm="Are you sure?"` +- **Namespace**: Components now use `App\Livewire` (not `App\Http\Livewire`). +- **Events**: Use `$this->dispatch()` (not `emit` or `dispatchBrowserEvent`). +- **Layout path**: `components.layouts.app` (not `layouts.app`). +- **Deferred by default**: Use `wire:model.live` for real-time updates. +- **Alpine included**: Don't manually include Alpine.js. diff --git a/.ai/tailwindcss/4/.gitkeep b/.ai/livewire/4/.gitkeep similarity index 100% rename from .ai/tailwindcss/4/.gitkeep rename to .ai/livewire/4/.gitkeep diff --git a/.ai/livewire/core.blade.php b/.ai/livewire/core.blade.php index a7eda0a..af08ecc 100644 --- a/.ai/livewire/core.blade.php +++ b/.ai/livewire/core.blade.php @@ -1 +1,17 @@ ## Livewire Core +- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire. + + +## Livewire Best Practices +- **Single root element** in Blade components +- **Loading states**: Use `wire:loading` and `wire:dirty`. +- **Add wire:key** in loops: +@verbatim + ```blade + @foreach ($items as $item) +
+ {{ $item->name }} +
+ @endforeach + ``` +@endverbatim diff --git a/.ai/tailwindcss/3/core.blade.php b/.ai/tailwindcss/3/core.blade.php index fd61e7a..b825a89 100644 --- a/.ai/tailwindcss/3/core.blade.php +++ b/.ai/tailwindcss/3/core.blade.php @@ -1,2 +1 @@ - Always use Tailwind CSS v3, verify you're using only supported classes. -- Use the `search-docs` tool to find exactly what's supported in this project's Tailwind setup. diff --git a/.ai/tailwindcss/4/core.blade.php b/.ai/tailwindcss/4/core.blade.php index 5187aae..88645e0 100644 --- a/.ai/tailwindcss/4/core.blade.php +++ b/.ai/tailwindcss/4/core.blade.php @@ -1,5 +1,4 @@ - Always use Tailwind CSS v4, do not use the deprecated utilities. -- Use the `search-docs` tool to find exactly what's supported in this project's Tailwind setup. - In Tailwind v4 you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: @verbatim $product->delete(); ?> - + @endvolt @endverbatim - @verbatim @endverbatim - - @verbatim diff --git a/src/Mcp/Tools/SearchDocs.php b/src/Mcp/Tools/SearchDocs.php index 487f6fe..64522df 100644 --- a/src/Mcp/Tools/SearchDocs.php +++ b/src/Mcp/Tools/SearchDocs.php @@ -16,21 +16,24 @@ class SearchDocs extends Tool { use MakesHttpRequests; - public function __construct(protected Roster $roster) - { - } + public function __construct(protected Roster $roster) {} public function description(): string { - return 'Search for up-to-date version-specific documentation related to this project and its packages. This tool will search Laravel hosted documentation based on the packages installed and is perfect for all Laravel related packages. Laravel, inertia, pest, livewire, filament, nova, nightwatch, and more.'.PHP_EOL.'You must use this tool to search for Laravel-ecosystem docs before using other approaches.'; + return 'Search for up-to-date version-specific documentation related to this project and its packages. This tool will search Laravel hosted documentation based on the packages installed and is perfect for all Laravel related packages. Laravel, inertia, pest, livewire, filament, nova, nightwatch, and more.'.PHP_EOL.'You must use this tool to search for Laravel-ecosystem docs before using other approaches. The results provided are for this project\'s package version and does not cover all versions of the package.'; } public function schema(ToolInputSchema $schema): ToolInputSchema { return $schema - ->string('queries') - ->description('### separated list of queries to perform. Useful to pass multiple if you aren\'t sure if it is "toggle" or "switch", or "infinite scroll" or "infinite load", for example.')->required() - + ->raw('queries', [ + 'description' => 'List of queries to perform, pass multiple if you aren\'t sure if it is "toggle" or "switch", for example', + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'description' => 'Search query', + ], + ])->required() ->raw('packages', [ 'description' => 'Package names to limit searching to from application-info. Useful if you know the package(s) you need. i.e. laravel/framework, inertiajs/inertia-laravel, @inertiajs/react', 'type' => 'array', @@ -54,7 +57,7 @@ public function handle(array $arguments): ToolResult|Generator $packagesFilter = array_key_exists('packages', $arguments) ? $arguments['packages'] : null; $queries = array_filter( - array_map('trim', explode('###', $arguments['queries'])), + array_map('trim', $arguments['queries']), fn ($query) => $query !== '' && $query !== '*' ); @@ -90,6 +93,7 @@ public function handle(array $arguments): ToolResult|Generator 'token_limit' => $tokenLimit, 'format' => 'markdown', ]; + try { $response = $this->client()->asJson()->post($apiUrl, $payload); From 94e97a103e946c22a80c62ee3b8b8a88bc5f0839 Mon Sep 17 00:00:00 2001 From: ashleyhindle <454975+ashleyhindle@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:11:57 +0000 Subject: [PATCH 3/5] Fix code styling --- src/Mcp/Tools/SearchDocs.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Mcp/Tools/SearchDocs.php b/src/Mcp/Tools/SearchDocs.php index 64522df..ef2c132 100644 --- a/src/Mcp/Tools/SearchDocs.php +++ b/src/Mcp/Tools/SearchDocs.php @@ -16,7 +16,9 @@ class SearchDocs extends Tool { use MakesHttpRequests; - public function __construct(protected Roster $roster) {} + public function __construct(protected Roster $roster) + { + } public function description(): string { From 6d0c0c0ad44ad5e259714c58ade32a9a3491e617 Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Tue, 12 Aug 2025 19:55:40 +0100 Subject: [PATCH 4/5] tests: update search docs tests to new queries type --- tests/Feature/Mcp/Tools/SearchDocsTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Feature/Mcp/Tools/SearchDocsTest.php b/tests/Feature/Mcp/Tools/SearchDocsTest.php index 623ce98..2e0f7fd 100644 --- a/tests/Feature/Mcp/Tools/SearchDocsTest.php +++ b/tests/Feature/Mcp/Tools/SearchDocsTest.php @@ -24,7 +24,7 @@ ]); $tool = new SearchDocs($roster); - $result = $tool->handle(['queries' => 'authentication###testing']); + $result = $tool->handle(['queries' => ['authentication', 'testing']]); expect($result)->toBeInstanceOf(ToolResult::class); @@ -57,7 +57,7 @@ ]); $tool = new SearchDocs($roster); - $result = $tool->handle(['queries' => 'authentication']); + $result = $tool->handle(['queries' => ['authentication']]); expect($result)->toBeInstanceOf(ToolResult::class); @@ -77,7 +77,7 @@ ]); $tool = new SearchDocs($roster); - $result = $tool->handle(['queries' => 'test### ###*### ']); + $result = $tool->handle(['queries' => ['test', ' ', '*', ' ']]); expect($result)->toBeInstanceOf(ToolResult::class); @@ -106,7 +106,7 @@ ]); $tool = new SearchDocs($roster); - $result = $tool->handle(['queries' => 'test']); + $result = $tool->handle(['queries' => ['test']]); expect($result)->toBeInstanceOf(ToolResult::class); @@ -129,7 +129,7 @@ ]); $tool = new SearchDocs($roster); - $result = $tool->handle(['queries' => 'nonexistent']); + $result = $tool->handle(['queries' => ['nonexistent']]); expect($result)->toBeInstanceOf(ToolResult::class); @@ -149,7 +149,7 @@ ]); $tool = new SearchDocs($roster); - $result = $tool->handle(['queries' => 'test', 'token_limit' => 5000]); + $result = $tool->handle(['queries' => ['test'], 'token_limit' => 5000]); expect($result)->toBeInstanceOf(ToolResult::class); @@ -169,7 +169,7 @@ ]); $tool = new SearchDocs($roster); - $result = $tool->handle(['queries' => 'test', 'token_limit' => 2000000]); + $result = $tool->handle(['queries' => ['test'], 'token_limit' => 2000000]); expect($result)->toBeInstanceOf(ToolResult::class); From fc193a25b28127a2b00341a6f73d5f8a419c295f Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Tue, 12 Aug 2025 20:43:08 +0100 Subject: [PATCH 5/5] guidelines: livewire: improve --- .ai/livewire/2/.gitkeep | 0 .ai/livewire/2/core.blade.php | 16 +++++++++++++++ .ai/livewire/3/core.blade.php | 33 ++++++++++++++++++++++++++----- .ai/livewire/core.blade.php | 37 ++++++++++++++++++++++++++++++----- .ai/volt/core.blade.php | 3 ++- 5 files changed, 78 insertions(+), 11 deletions(-) delete mode 100644 .ai/livewire/2/.gitkeep create mode 100644 .ai/livewire/2/core.blade.php diff --git a/.ai/livewire/2/.gitkeep b/.ai/livewire/2/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.ai/livewire/2/core.blade.php b/.ai/livewire/2/core.blade.php new file mode 100644 index 0000000..ac380de --- /dev/null +++ b/.ai/livewire/2/core.blade.php @@ -0,0 +1,16 @@ +- `wire:model` is live by default. +- **Namespace**: Components typically exist in `App\Http\Livewire`. +- **Events**: Use `emit()`, `emitTo()`, `emitSelf()` and `dispatchBrowserEvent()` for events. +- Alpine is included separately to Livewire. +- You can listen for `livewire:load` to hook into Livewire initialization, and `Livewire.onPageExpired` for when the page expires: +@verbatim + +document.addEventListener('livewire:load', function () { + Livewire.onPageExpired(() => { + alert('Your session expired'); + }); + + Livewire.onError(status => console.error(status)); +}); + +@endverbatim diff --git a/.ai/livewire/3/core.blade.php b/.ai/livewire/3/core.blade.php index cfd58ac..97a5269 100644 --- a/.ai/livewire/3/core.blade.php +++ b/.ai/livewire/3/core.blade.php @@ -1,8 +1,31 @@ -#### Key Changes from Livewire 2 -- These changed in Livewire 2, but may not have in this project. Verify this project's setup to ensure you conform with conventions. - +## Key Changes from Livewire 2 +- These changed in Livewire 2, but may not have been updated in this project. Verify this project's setup to ensure you conform with project conventions. +- **Wire:model**: Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default. - **Namespace**: Components now use `App\Livewire` (not `App\Http\Livewire`). - **Events**: Use `$this->dispatch()` (not `emit` or `dispatchBrowserEvent`). - **Layout path**: `components.layouts.app` (not `layouts.app`). -- **Deferred by default**: Use `wire:model.live` for real-time updates. -- **Alpine included**: Don't manually include Alpine.js. + +## New directives +- `wire:show`, `wire:transition`, `wire:cloak, `wire:offline`, `wire:target` are available for use. Use the docs to find usages. + +## Alpine +- Alpine is now included with Livewire, don't manually include Alpine.js. +- Plugins built in to Alpine: persist, intersect, collapse, and focus. + +## Lifecycle hooks +- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring: +@verbatim + +document.addEventListener('livewire:init', function () { + Livewire.hook('request', ({ fail }) => { + if (fail && fail.status === 419) { + alert('Your session expired'); + } + }); + + Livewire.hook('message.failed', (message, component) => { + console.error(message); + }); +}); + +@endverbatim diff --git a/.ai/livewire/core.blade.php b/.ai/livewire/core.blade.php index af08ecc..4c78e0a 100644 --- a/.ai/livewire/core.blade.php +++ b/.ai/livewire/core.blade.php @@ -1,11 +1,13 @@ ## Livewire Core -- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire. - +- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. +- Use the `php artisan make:livewire [Posts\\CreatePost]` artisan command to create new components +- State should live on the server, with the UI reflecting it. +- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions. ## Livewire Best Practices -- **Single root element** in Blade components -- **Loading states**: Use `wire:loading` and `wire:dirty`. -- **Add wire:key** in loops: +- Livewire components require a single root element. +- Use `wire:loading` and `wire:dirty` for delightful loading states. +- Add `wire:key` in loops: @verbatim ```blade @foreach ($items as $item) @@ -15,3 +17,28 @@ @endforeach ``` @endverbatim +- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects: +@verbatim + + public function mount(User $user) { $this->user = $user; } + public function updatedSearch() { $this->resetPage(); } + +@endverbatim + +## Testing Livewire +@verbatim + + Livewire::test(Counter::class) + ->assertSet('count', 0) + ->call('increment') + ->assertSet('count', 1) + ->assertSee(1) + ->assertStatus(200); + +@endverbatim +@verbatim + + $this->get('/posts/create') + ->assertSeeLivewire(CreatePost::class); + +@endverbatim diff --git a/.ai/volt/core.blade.php b/.ai/volt/core.blade.php index 50cce07..7ce929a 100644 --- a/.ai/volt/core.blade.php +++ b/.ai/volt/core.blade.php @@ -1,5 +1,6 @@ - This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. There is documentation available for it. -- Volt is an elegantly crafted **functional** API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to coexist in the same file +- Make new Volt components using `php artisan make:volt [name] [--test] [--pest]` +- Volt is a **functional** API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to coexist in the same file - **Single-File Components**: Livewire Volt allows PHP logic and Blade templates in one file. Components use the `@volt` directive. - You must check existing Volt components to find out if they're functional or class based. If you can't detect that, ask the user which they prefer before writing a Volt component.