ui-ux-consultant-cli 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/ui-ux-consultant/SKILL.md +844 -0
- package/assets/ui-ux-consultant/references/accessibility.md +175 -0
- package/assets/ui-ux-consultant/references/alt-libraries.md +90 -0
- package/assets/ui-ux-consultant/references/animations.md +448 -0
- package/assets/ui-ux-consultant/references/catalog/colors.md +91 -0
- package/assets/ui-ux-consultant/references/catalog/fonts.md +363 -0
- package/assets/ui-ux-consultant/references/catalog/products.md +340 -0
- package/assets/ui-ux-consultant/references/catalog/styles.md +165 -0
- package/assets/ui-ux-consultant/references/components.md +1116 -0
- package/assets/ui-ux-consultant/references/patterns.md +600 -0
- package/assets/ui-ux-consultant/references/performance.md +198 -0
- package/assets/ui-ux-consultant/references/stacks/astro.md +382 -0
- package/assets/ui-ux-consultant/references/stacks/flutter.md +308 -0
- package/assets/ui-ux-consultant/references/stacks/html-tailwind.md +415 -0
- package/assets/ui-ux-consultant/references/stacks/jetpack-compose.md +333 -0
- package/assets/ui-ux-consultant/references/stacks/laravel.md +521 -0
- package/assets/ui-ux-consultant/references/stacks/nextjs.md +275 -0
- package/assets/ui-ux-consultant/references/stacks/nuxt-ui.md +384 -0
- package/assets/ui-ux-consultant/references/stacks/nuxtjs.md +264 -0
- package/assets/ui-ux-consultant/references/stacks/react-native.md +346 -0
- package/assets/ui-ux-consultant/references/stacks/react.md +268 -0
- package/assets/ui-ux-consultant/references/stacks/shadcn.md +485 -0
- package/assets/ui-ux-consultant/references/stacks/svelte.md +429 -0
- package/assets/ui-ux-consultant/references/stacks/swiftui.md +336 -0
- package/assets/ui-ux-consultant/references/stacks/threejs.md +366 -0
- package/assets/ui-ux-consultant/references/stacks/vue.md +272 -0
- package/assets/ui-ux-consultant/references/theming.md +701 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +130 -0
- package/package.json +51 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# Laravel — UI/UX Reference
|
|
2
|
+
|
|
3
|
+
## When to Read
|
|
4
|
+
Use this file when building UI with Laravel. Covers three approaches: Blade + Livewire (reactive SSR), Inertia.js + Vue/React (SPA feel), and Blade + Alpine.js (simple interactivity). Includes Filament for admin panels.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Three UI Approaches
|
|
9
|
+
|
|
10
|
+
| Approach | When to Use | Stack |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| **Blade + Livewire 3** | Reactive UI without SPA overhead | Livewire 3, Alpine.js, Tailwind CSS |
|
|
13
|
+
| **Inertia.js + Vue 3** | SPA feel with Laravel backend | Inertia.js, Vue 3, Vite |
|
|
14
|
+
| **Inertia.js + React** | SPA feel, React ecosystem | Inertia.js, React 18, shadcn/ui |
|
|
15
|
+
| **Blade + Alpine.js** | Simple interactivity, no reactive backend | Alpine.js, Tailwind CSS |
|
|
16
|
+
| **Filament** | Admin panels and dashboards | Filament 3 (built on Livewire) |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Recommended Packages
|
|
21
|
+
|
|
22
|
+
| Package | Purpose | Install |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| Livewire 3 | Reactive server-side components | `composer require livewire/livewire` |
|
|
25
|
+
| Inertia.js | SPA with Laravel backend | `composer require inertiajs/inertia-laravel` |
|
|
26
|
+
| Laravel Breeze | Auth scaffolding (Blade/Inertia/API) | `php artisan breeze:install` |
|
|
27
|
+
| Laravel Jetstream | Full auth + teams (Livewire or Inertia) | `composer require laravel/jetstream` |
|
|
28
|
+
| Filament | Admin panels, CRUD, resources | `composer require filament/filament` |
|
|
29
|
+
| Wire Elements | Pre-built Livewire modal component | `composer require wire-elements/modal` |
|
|
30
|
+
| Spatie Permission | Roles and permissions | `composer require spatie/laravel-permission` |
|
|
31
|
+
| Laravel Horizon | Queue monitoring UI | `composer require laravel/horizon` |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Style Recommendations
|
|
36
|
+
|
|
37
|
+
- **Admin / dashboard:** Filament (built-in design system) or custom Tailwind grid
|
|
38
|
+
- **Marketing / landing:** Tailwind CSS + Minimalism or Aurora UI aesthetic
|
|
39
|
+
- **SaaS application:** Livewire + Tailwind + Flat Design
|
|
40
|
+
- **Content-heavy:** Blade + Tailwind Typography plugin
|
|
41
|
+
- **Internal tools:** Filament or Breeze + Tailwind
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Approach 1: Livewire 3
|
|
46
|
+
|
|
47
|
+
### Basic Component
|
|
48
|
+
|
|
49
|
+
```php
|
|
50
|
+
// app/Livewire/UserSearch.php
|
|
51
|
+
namespace App\Livewire;
|
|
52
|
+
|
|
53
|
+
use App\Models\User;
|
|
54
|
+
use Livewire\Component;
|
|
55
|
+
use Livewire\Attributes\Computed;
|
|
56
|
+
use Livewire\Attributes\Url;
|
|
57
|
+
|
|
58
|
+
class UserSearch extends Component
|
|
59
|
+
{
|
|
60
|
+
#[Url] // Sync with URL query string
|
|
61
|
+
public string $query = '';
|
|
62
|
+
public string $sortBy = 'name';
|
|
63
|
+
|
|
64
|
+
#[Computed]
|
|
65
|
+
public function users()
|
|
66
|
+
{
|
|
67
|
+
return User::query()
|
|
68
|
+
->when($this->query, fn($q) => $q->where('name', 'like', "%{$this->query}%"))
|
|
69
|
+
->orderBy($this->sortBy)
|
|
70
|
+
->paginate(15);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public function render()
|
|
74
|
+
{
|
|
75
|
+
return view('livewire.user-search');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
{{-- resources/views/livewire/user-search.blade.php --}}
|
|
82
|
+
<div>
|
|
83
|
+
{{-- Search input with debounce --}}
|
|
84
|
+
<input
|
|
85
|
+
wire:model.live.debounce.300ms="query"
|
|
86
|
+
type="search"
|
|
87
|
+
placeholder="Search users..."
|
|
88
|
+
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
{{-- Results --}}
|
|
92
|
+
<div class="mt-4 space-y-2">
|
|
93
|
+
@foreach($this->users as $user)
|
|
94
|
+
<div class="flex items-center justify-between p-4 bg-white border rounded-lg">
|
|
95
|
+
<div>
|
|
96
|
+
<p class="font-medium">{{ $user->name }}</p>
|
|
97
|
+
<p class="text-sm text-gray-500">{{ $user->email }}</p>
|
|
98
|
+
</div>
|
|
99
|
+
<button wire:click="impersonate({{ $user->id }})" class="text-sm text-blue-600 hover:underline">
|
|
100
|
+
Impersonate
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
@endforeach
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{{ $this->users->links() }}
|
|
107
|
+
</div>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Livewire Loading States
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
{{-- Loading spinner on button --}}
|
|
114
|
+
<button wire:click="save" wire:loading.attr="disabled" class="btn-primary">
|
|
115
|
+
<span wire:loading.remove>Save changes</span>
|
|
116
|
+
<span wire:loading class="flex items-center gap-2">
|
|
117
|
+
<svg class="animate-spin h-4 w-4" ...></svg>
|
|
118
|
+
Saving...
|
|
119
|
+
</span>
|
|
120
|
+
</button>
|
|
121
|
+
|
|
122
|
+
{{-- Loading overlay on a section --}}
|
|
123
|
+
<div class="relative">
|
|
124
|
+
<div wire:loading.flex class="absolute inset-0 bg-white/70 items-center justify-center z-10">
|
|
125
|
+
<svg class="animate-spin h-6 w-6 text-blue-600" ...></svg>
|
|
126
|
+
</div>
|
|
127
|
+
<div wire:loading.class="opacity-50">
|
|
128
|
+
{{-- Content --}}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{{-- Target specific actions --}}
|
|
133
|
+
<button wire:click="delete({{ $id }})" wire:loading.attr="disabled" wire:target="delete({{ $id }})">
|
|
134
|
+
Delete
|
|
135
|
+
</button>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Livewire Form with Validation
|
|
139
|
+
|
|
140
|
+
```php
|
|
141
|
+
// app/Livewire/CreatePost.php
|
|
142
|
+
use Livewire\Attributes\Rule;
|
|
143
|
+
|
|
144
|
+
class CreatePost extends Component
|
|
145
|
+
{
|
|
146
|
+
#[Rule('required|min:3|max:100')]
|
|
147
|
+
public string $title = '';
|
|
148
|
+
|
|
149
|
+
#[Rule('required|min:10')]
|
|
150
|
+
public string $body = '';
|
|
151
|
+
|
|
152
|
+
#[Rule('required|exists:categories,id')]
|
|
153
|
+
public ?int $categoryId = null;
|
|
154
|
+
|
|
155
|
+
public function save()
|
|
156
|
+
{
|
|
157
|
+
$validated = $this->validate();
|
|
158
|
+
|
|
159
|
+
Post::create([
|
|
160
|
+
...$validated,
|
|
161
|
+
'user_id' => auth()->id(),
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
$this->reset();
|
|
165
|
+
$this->dispatch('post-created');
|
|
166
|
+
session()->flash('message', 'Post created successfully.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```html
|
|
172
|
+
<form wire:submit="save" class="space-y-4">
|
|
173
|
+
<div>
|
|
174
|
+
<label class="block text-sm font-medium text-gray-700">Title</label>
|
|
175
|
+
<input wire:model="title" type="text"
|
|
176
|
+
class="mt-1 w-full border rounded-lg px-3 py-2 @error('title') border-red-500 @enderror" />
|
|
177
|
+
@error('title')
|
|
178
|
+
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
|
|
179
|
+
@enderror
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
@if(session('message'))
|
|
183
|
+
<div class="p-3 bg-green-50 text-green-700 rounded-lg">{{ session('message') }}</div>
|
|
184
|
+
@endif
|
|
185
|
+
|
|
186
|
+
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
|
187
|
+
Create post
|
|
188
|
+
</button>
|
|
189
|
+
</form>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Livewire Lazy Loading
|
|
193
|
+
|
|
194
|
+
```php
|
|
195
|
+
// Defer rendering until user scrolls to it
|
|
196
|
+
use Livewire\Attributes\Lazy;
|
|
197
|
+
|
|
198
|
+
#[Lazy]
|
|
199
|
+
class ExpensiveReport extends Component
|
|
200
|
+
{
|
|
201
|
+
public function placeholder()
|
|
202
|
+
{
|
|
203
|
+
return <<<HTML
|
|
204
|
+
<div class="animate-pulse h-48 bg-gray-100 rounded-xl"></div>
|
|
205
|
+
HTML;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public function render()
|
|
209
|
+
{
|
|
210
|
+
return view('livewire.expensive-report', [
|
|
211
|
+
'data' => $this->generateReport(),
|
|
212
|
+
]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Event System
|
|
218
|
+
|
|
219
|
+
```php
|
|
220
|
+
// Dispatch from child component
|
|
221
|
+
$this->dispatch('user-updated', id: $user->id);
|
|
222
|
+
|
|
223
|
+
// Listen in parent component
|
|
224
|
+
use Livewire\Attributes\On;
|
|
225
|
+
|
|
226
|
+
#[On('user-updated')]
|
|
227
|
+
public function refreshUser(int $id): void
|
|
228
|
+
{
|
|
229
|
+
$this->user = User::find($id);
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Approach 2: Inertia.js + Vue 3
|
|
236
|
+
|
|
237
|
+
### Controller
|
|
238
|
+
|
|
239
|
+
```php
|
|
240
|
+
// app/Http/Controllers/UsersController.php
|
|
241
|
+
use Inertia\Inertia;
|
|
242
|
+
|
|
243
|
+
class UsersController extends Controller
|
|
244
|
+
{
|
|
245
|
+
public function index(Request $request)
|
|
246
|
+
{
|
|
247
|
+
return Inertia::render('Users/Index', [
|
|
248
|
+
'users' => User::query()
|
|
249
|
+
->when($request->search, fn($q, $s) => $q->where('name', 'like', "%{$s}%"))
|
|
250
|
+
->paginate(15)
|
|
251
|
+
->withQueryString(),
|
|
252
|
+
'filters' => $request->only('search'),
|
|
253
|
+
]);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
public function store(StoreUserRequest $request)
|
|
257
|
+
{
|
|
258
|
+
User::create($request->validated());
|
|
259
|
+
return redirect()->route('users.index')->with('success', 'User created.');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Vue 3 Page Component
|
|
265
|
+
|
|
266
|
+
```vue
|
|
267
|
+
<!-- resources/js/Pages/Users/Index.vue -->
|
|
268
|
+
<script setup lang="ts">
|
|
269
|
+
import { ref, watch } from 'vue';
|
|
270
|
+
import { router, Link } from '@inertiajs/vue3';
|
|
271
|
+
import AppLayout from '@/Layouts/AppLayout.vue';
|
|
272
|
+
import Pagination from '@/Components/Pagination.vue';
|
|
273
|
+
|
|
274
|
+
interface User { id: number; name: string; email: string; }
|
|
275
|
+
interface Props {
|
|
276
|
+
users: { data: User[]; links: object[] };
|
|
277
|
+
filters: { search?: string };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const props = defineProps<Props>();
|
|
281
|
+
const search = ref(props.filters.search ?? '');
|
|
282
|
+
|
|
283
|
+
// Debounced search — updates URL without full reload
|
|
284
|
+
watch(search, (value) => {
|
|
285
|
+
router.get(route('users.index'), { search: value }, {
|
|
286
|
+
preserveState: true,
|
|
287
|
+
replace: true,
|
|
288
|
+
debounce: 300,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
</script>
|
|
292
|
+
|
|
293
|
+
<template>
|
|
294
|
+
<AppLayout title="Users">
|
|
295
|
+
<div class="max-w-4xl mx-auto p-6 space-y-6">
|
|
296
|
+
<input v-model="search" type="search" placeholder="Search users..."
|
|
297
|
+
class="w-full px-4 py-2 border rounded-lg" />
|
|
298
|
+
|
|
299
|
+
<div class="bg-white rounded-xl border overflow-hidden">
|
|
300
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
301
|
+
<thead class="bg-gray-50">
|
|
302
|
+
<tr>
|
|
303
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
|
304
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
|
|
305
|
+
<th class="relative px-6 py-3"><span class="sr-only">Actions</span></th>
|
|
306
|
+
</tr>
|
|
307
|
+
</thead>
|
|
308
|
+
<tbody class="divide-y divide-gray-200">
|
|
309
|
+
<tr v-for="user in users.data" :key="user.id" class="hover:bg-gray-50">
|
|
310
|
+
<td class="px-6 py-4 text-sm font-medium text-gray-900">{{ user.name }}</td>
|
|
311
|
+
<td class="px-6 py-4 text-sm text-gray-500">{{ user.email }}</td>
|
|
312
|
+
<td class="px-6 py-4 text-right">
|
|
313
|
+
<Link :href="route('users.edit', user.id)"
|
|
314
|
+
class="text-sm text-blue-600 hover:underline">Edit</Link>
|
|
315
|
+
</td>
|
|
316
|
+
</tr>
|
|
317
|
+
</tbody>
|
|
318
|
+
</table>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<Pagination :links="users.links" />
|
|
322
|
+
</div>
|
|
323
|
+
</AppLayout>
|
|
324
|
+
</template>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Inertia Form Handling
|
|
328
|
+
|
|
329
|
+
```vue
|
|
330
|
+
<script setup lang="ts">
|
|
331
|
+
import { useForm } from '@inertiajs/vue3';
|
|
332
|
+
|
|
333
|
+
const form = useForm({
|
|
334
|
+
name: '',
|
|
335
|
+
email: '',
|
|
336
|
+
role: 'user',
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
function submit() {
|
|
340
|
+
form.post(route('users.store'), {
|
|
341
|
+
onSuccess: () => form.reset(),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
</script>
|
|
345
|
+
|
|
346
|
+
<template>
|
|
347
|
+
<form @submit.prevent="submit" class="space-y-4">
|
|
348
|
+
<div>
|
|
349
|
+
<label class="block text-sm font-medium text-gray-700">Name</label>
|
|
350
|
+
<input v-model="form.name" type="text"
|
|
351
|
+
:class="form.errors.name && 'border-red-500'"
|
|
352
|
+
class="mt-1 w-full border rounded-lg px-3 py-2" />
|
|
353
|
+
<p v-if="form.errors.name" class="mt-1 text-sm text-red-500">{{ form.errors.name }}</p>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<button type="submit" :disabled="form.processing"
|
|
357
|
+
class="px-4 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50">
|
|
358
|
+
{{ form.processing ? 'Creating...' : 'Create user' }}
|
|
359
|
+
</button>
|
|
360
|
+
</form>
|
|
361
|
+
</template>
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Approach 3: Blade + Alpine.js
|
|
367
|
+
|
|
368
|
+
```html
|
|
369
|
+
{{-- Simple dropdown --}}
|
|
370
|
+
<div x-data="{ open: false }" class="relative">
|
|
371
|
+
<button @click="open = !open" @keydown.escape="open = false"
|
|
372
|
+
class="flex items-center gap-2 px-4 py-2 border rounded-lg">
|
|
373
|
+
Options
|
|
374
|
+
<svg class="w-4 h-4" :class="open && 'rotate-180 transition-transform'" ...></svg>
|
|
375
|
+
</button>
|
|
376
|
+
<div x-show="open" x-transition @click.outside="open = false"
|
|
377
|
+
class="absolute top-full mt-1 w-48 bg-white border rounded-lg shadow-lg py-1 z-10">
|
|
378
|
+
<a href="{{ route('settings') }}" class="block px-4 py-2 text-sm hover:bg-gray-50">Settings</a>
|
|
379
|
+
<form method="POST" action="{{ route('logout') }}">
|
|
380
|
+
@csrf
|
|
381
|
+
<button type="submit" class="w-full text-left px-4 py-2 text-sm hover:bg-gray-50">
|
|
382
|
+
Log out
|
|
383
|
+
</button>
|
|
384
|
+
</form>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{{-- Tabs --}}
|
|
389
|
+
<div x-data="{ activeTab: 'overview' }">
|
|
390
|
+
<nav class="flex border-b">
|
|
391
|
+
<button @click="activeTab = 'overview'"
|
|
392
|
+
:class="activeTab === 'overview' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-500'"
|
|
393
|
+
class="px-4 py-2 text-sm font-medium">Overview</button>
|
|
394
|
+
<button @click="activeTab = 'activity'"
|
|
395
|
+
:class="activeTab === 'activity' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-500'"
|
|
396
|
+
class="px-4 py-2 text-sm font-medium">Activity</button>
|
|
397
|
+
</nav>
|
|
398
|
+
<div x-show="activeTab === 'overview'" class="py-4">
|
|
399
|
+
@include('partials.overview')
|
|
400
|
+
</div>
|
|
401
|
+
<div x-show="activeTab === 'activity'" class="py-4">
|
|
402
|
+
@include('partials.activity')
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Filament Admin Panels
|
|
410
|
+
|
|
411
|
+
```php
|
|
412
|
+
// app/Filament/Resources/UserResource.php
|
|
413
|
+
use Filament\Resources\Resource;
|
|
414
|
+
use Filament\Tables\Columns\TextColumn;
|
|
415
|
+
use Filament\Forms\Components\TextInput;
|
|
416
|
+
use Filament\Forms\Components\Select;
|
|
417
|
+
|
|
418
|
+
class UserResource extends Resource
|
|
419
|
+
{
|
|
420
|
+
protected static ?string $model = User::class;
|
|
421
|
+
protected static ?string $navigationIcon = 'heroicon-o-users';
|
|
422
|
+
|
|
423
|
+
public static function form(Form $form): Form
|
|
424
|
+
{
|
|
425
|
+
return $form->schema([
|
|
426
|
+
TextInput::make('name')->required()->maxLength(255),
|
|
427
|
+
TextInput::make('email')->email()->required()->unique(ignoreRecord: true),
|
|
428
|
+
Select::make('role')
|
|
429
|
+
->options(['admin' => 'Admin', 'user' => 'User'])
|
|
430
|
+
->required(),
|
|
431
|
+
]);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
public static function table(Table $table): Table
|
|
435
|
+
{
|
|
436
|
+
return $table
|
|
437
|
+
->columns([
|
|
438
|
+
TextColumn::make('name')->searchable()->sortable(),
|
|
439
|
+
TextColumn::make('email')->searchable(),
|
|
440
|
+
TextColumn::make('role')->badge()->color(fn($state) => match($state) {
|
|
441
|
+
'admin' => 'danger',
|
|
442
|
+
default => 'gray',
|
|
443
|
+
}),
|
|
444
|
+
TextColumn::make('created_at')->dateTime()->sortable()->toggleable(),
|
|
445
|
+
])
|
|
446
|
+
->filters([TrashedFilter::make()])
|
|
447
|
+
->actions([EditAction::make(), DeleteAction::make()])
|
|
448
|
+
->bulkActions([BulkActionGroup::make([DeleteBulkAction::make()])]);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Best Practices by Category
|
|
456
|
+
|
|
457
|
+
### Livewire
|
|
458
|
+
- Use `#[Computed]` for derived data — cached per request, not re-queried on every re-render
|
|
459
|
+
- Use `#[Url]` to sync component state with URL — enables bookmarkable filtered views
|
|
460
|
+
- Use `wire:model.live.debounce.300ms` for search inputs — never `.live` alone
|
|
461
|
+
- Use `#[Lazy]` for below-fold components and heavy data processing
|
|
462
|
+
- Break large Livewire components into smaller ones with event communication
|
|
463
|
+
- Use `$this->dispatch()` instead of the old `$this->emit()` (deprecated in v3)
|
|
464
|
+
|
|
465
|
+
### Inertia.js
|
|
466
|
+
- Use `useForm` from `@inertiajs/vue3` — provides `errors`, `processing`, `reset()` automatically
|
|
467
|
+
- `preserveState: true` on router visits to keep scroll position and form state
|
|
468
|
+
- Use `<Link preserve-scroll>` for pagination links to avoid scroll-to-top
|
|
469
|
+
- Share global data via `HandleInertiaRequests` middleware (auth user, flash messages)
|
|
470
|
+
- Type `defineProps` with TypeScript interfaces for IDE support on all page props
|
|
471
|
+
|
|
472
|
+
### Database / Eloquent
|
|
473
|
+
- Always eager load relationships: `User::with(['posts', 'roles'])->get()`
|
|
474
|
+
- Use Eloquent scopes for reusable query logic: `User::active()->verified()->paginate()`
|
|
475
|
+
- Cache expensive queries: `Cache::remember('stats', 3600, fn() => computeStats())`
|
|
476
|
+
- Use `paginate()` not `get()` for lists — never load unbounded result sets
|
|
477
|
+
|
|
478
|
+
### Security
|
|
479
|
+
- Always use `@csrf` in Blade forms
|
|
480
|
+
- Validate all input in Form Requests, not controllers
|
|
481
|
+
- Use Laravel's authorization (`$this->authorize()`, policies) not manual checks
|
|
482
|
+
- Never expose Eloquent models directly — use API Resources or explicit arrays
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Common Anti-Patterns
|
|
487
|
+
|
|
488
|
+
1. **N+1 queries in Livewire** — Livewire re-renders on every state change. A query inside `@foreach` with a relationship access creates N+1. Always eager load: `User::with('posts')->get()`.
|
|
489
|
+
|
|
490
|
+
2. **`wire:model` without `.debounce` on search inputs** — `.live` fires on every keystroke. Use `wire:model.live.debounce.300ms` for text inputs.
|
|
491
|
+
|
|
492
|
+
3. **Mixing Livewire and Inertia on the same page** — they are fundamentally different rendering paradigms. Pick one per page. Inertia pages cannot contain Livewire components.
|
|
493
|
+
|
|
494
|
+
4. **Large Blade components (> 150 lines)** — extract repeated HTML into Blade components (`x-card`, `x-modal`) or Livewire components. Monolithic Blade templates are hard to test.
|
|
495
|
+
|
|
496
|
+
5. **Client-side routing in Blade without Inertia** — manually managing SPA navigation in Blade + Fetch is fragile. Use Inertia.js for SPA behavior or accept full-page navigation in Blade.
|
|
497
|
+
|
|
498
|
+
6. **Returning data directly from controllers without authorization** — check policies before returning resources. `$this->authorize('view', $user)` before `return Inertia::render(...)`.
|
|
499
|
+
|
|
500
|
+
7. **Using `session()->flash()` with Inertia** — flash messages in Inertia require sharing them in `HandleInertiaRequests::share()`. Raw session flashes are not auto-passed to Vue/React.
|
|
501
|
+
|
|
502
|
+
8. **No `withQueryString()` on paginator** — without it, Eloquent pagination links lose current search/filter parameters.
|
|
503
|
+
|
|
504
|
+
9. **`wire:click` on non-button elements** — use `<button>` elements for actions, not `<div wire:click>`. Screen readers and keyboard users expect buttons for interactive elements.
|
|
505
|
+
|
|
506
|
+
10. **Filament without proper authorization** — Filament resources are accessible to all authenticated users by default. Implement `canAccess()`, `canCreate()`, `canEdit()`, `canDelete()` methods.
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## Performance Checklist
|
|
511
|
+
|
|
512
|
+
- [ ] `wire:model.live.debounce.300ms` for all search/filter inputs
|
|
513
|
+
- [ ] Eager load Eloquent relationships (`with()`) in all Livewire computed properties
|
|
514
|
+
- [ ] `#[Lazy]` attribute for below-fold or expensive Livewire components
|
|
515
|
+
- [ ] `Cache::remember()` for queries that don't change frequently
|
|
516
|
+
- [ ] Inertia `<Link preserve-scroll>` to avoid scroll-to-top on pagination
|
|
517
|
+
- [ ] `withQueryString()` on all paginators
|
|
518
|
+
- [ ] Laravel Horizon for queue monitoring (async jobs for emails, reports)
|
|
519
|
+
- [ ] Database indexes on all filtered/sorted columns
|
|
520
|
+
- [ ] `php artisan optimize` in production (caches config, routes, views)
|
|
521
|
+
- [ ] Vite asset bundling with `npm run build` — not `mix` (deprecated)
|