ux
Otimiza UX com Laravel Precognition, Prompts e Turbo para interações fluidas. Use quando precisar melhorar experiência do usuário, adicionar validação em tempo real, ou implementar Turbo/HTMX.
Files1
View on GitHubSKILL.mdskills/ux/SKILL.md
GitHubux
Resumo
Melhora UX com Precognition, Prompts interativos e HMR para Inertia.js.
Skills Relacionadas
| Skill | Quando usar junto |
|---|---|
ui-ux |
Para identificar melhorias de UX |
i18n |
Para UX com traduções |
mcp |
Para validar UX visualmente |
testing |
Para testar flows de UX |
Quando usar
Use esta skill sempre que:
- Implementar validação preemptiva com Precognition
- Criar Artisan commands interativos com Prompts
- Configurar HMR para Inertia.js
- Criar CLI user experience otimizada
- Melhorar feedback visual em forms
- Configurar Vite para desenvolvimento
Laravel Precognition
Instalação
composer require laravel/precognition
# React puro (não-Inertia)
npm install laravel-precognition-react
# React + Inertia (use este pacote no lugar do anterior ao usar Inertia)
npm install laravel-precognition-react-inertia
Configuração Backend
O Laravel Precognition já fornece o middleware nativo
Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests.
Não é necessário criar um middleware custom — basta aplicá-lo à rota
que deve suportar validação preemptiva (veja a próxima seção).
Configurar Route
// routes/web.php
use App\Http\Controllers\ProductController;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
Route::post('/products', [ProductController::class, 'store'])
->middleware([HandlePrecognitiveRequests::class]);
No FormRequest ou controller, é possível checar se a requisição é
preemptiva com $request->isPrecognitive().
Frontend React
// resources/js/components/ProductForm.tsx
import { useForm } from 'laravel-precognition-react';
interface ProductForm {
name: string;
price: number;
category_id: number;
}
export function ProductForm() {
// Ordem dos argumentos: MÉTODO, URL, dados iniciais
const form = useForm<ProductForm>(
'post',
'/products',
{
name: '',
price: 0,
category_id: 0,
}
);
return (
<form onSubmit={form.submit}>
<input
type="text"
value={form.data.name}
onChange={(e) => form.setData('name', e.target.value)}
onBlur={() => form.validate('name')}
/>
{form.invalid('name') && (
<span>{form.errors.name}</span>
)}
<input
type="number"
value={form.data.price}
onChange={(e) => form.setData('price', parseFloat(e.target.value))}
onBlur={() => form.validate('price')}
/>
{form.invalid('price') && (
<span>{form.errors.price}</span>
)}
<button type="submit" disabled={form.processing}>
Save
</button>
</form>
);
}
Integração com Inertia
// resources/js/pages/Products/Create.tsx
// No modo Inertia, o useForm do Precognition já integra o Inertia
// (não use o useForm do @inertiajs/react junto).
import { useForm } from 'laravel-precognition-react-inertia';
export default function CreateProduct() {
// Ordem dos argumentos: MÉTODO, URL, dados iniciais
const form = useForm('post', '/products', {
name: '',
price: 0,
category_id: 0,
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// No modo Inertia, submit() recebe as visit options do Inertia
form.submit({
preserveScroll: true,
onSuccess: () => form.reset(),
});
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={form.data.name}
onChange={(e) => form.setData('name', e.target.value)}
onBlur={() => form.validate('name')}
/>
{form.invalid('name') && <span className="error">{form.errors.name}</span>}
<button type="submit" disabled={form.processing}>
Save
</button>
</form>
);
}
Laravel Prompts
Instalação
composer require laravel/prompts
Command Básico
php artisan make:command ImportProducts
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Actions\Product\ImportProductsAction;
use Illuminate\Console\Command;
use Laravel\Prompts\Prompt;
final class ImportProducts extends Command
{
protected $signature = 'products:import';
protected $description = 'Import products from CSV';
public function handle(): int
{
// Confirm
if (! $this->confirm('Do you want to import products?')) {
return Command::SUCCESS;
}
// Select
$source = $this->choice(
'Select import source',
['csv', 'api', 'xml'],
'csv'
);
// Text input
$file = $this->ask('Enter file path', 'products.csv');
// Password/Secret
$apiKey = $this->secret('Enter API key');
// Search/Select
$category = $this->anticipate(
'Select category',
['Electronics', 'Clothing', 'Food'],
'Electronics'
);
// Multi-select
$fields = $this->choice(
'Select fields to import',
['name', 'price', 'description', 'category'],
multiple: true
);
// Progress bar
$this->info('Importing products...');
$products = $this->importFromFile($file);
$progress = $this->output->createProgressBar($products->count());
$progress->start();
foreach ($products as $product) {
ImportProductsAction::dispatch($product);
$progress->advance();
}
$progress->finish();
$this->newLine(2);
$this->info("Successfully imported {$products->count()} products!");
return Command::SUCCESS;
}
}
Prompts Avançados
<?php
use Laravel\Prompts\{
confirm,
info,
note,
alert,
warning,
error,
spin,
table,
search,
multisearch,
suggest,
text,
textarea,
password,
select,
multiselect,
};
// Info/Warning/Error
info('Process started successfully');
note('Remember to backup your data');
warning('This action cannot be undone');
alert('Something went wrong');
error('Failed to process');
// Table
table(
['ID', 'Name', 'Price', 'Status'],
[
[1, 'Product 1', 'R$ 100,00', 'Active'],
[2, 'Product 2', 'R$ 200,00', 'Inactive'],
[3, 'Product 3', 'R$ 150,00', 'Active'],
]
);
// Search
$search = search(
label: 'Search for a product',
options: fn (string $value) => $this->searchProducts($value),
placeholder: 'Type to search...',
);
// Multi-search
$categories = multisearch(
label: 'Select categories',
options: fn (string $value) => Category::where('name', 'like', "%{$value}%")
->pluck('name', 'id')
->toArray(),
);
// Suggest
$name = suggest(
label: 'Product name',
options: ['Product A', 'Product B', 'Product C'],
placeholder: 'Start typing...',
);
// Spinner
$result = spin(
fn () => $this->longRunningTask(),
'Processing...'
);
// Textarea
$description = textarea(
label: 'Product description',
placeholder: 'Enter detailed description...',
required: true,
rows: 5,
);
Laravel Turbo (HMR)
Configurar Vite + Inertia HMR
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.ts'],
refresh: true,
}),
],
server: {
hmr: {
host: 'localhost',
},
},
});
Configuração React Fast Refresh
npm install @vitejs/plugin-react
// vite.config.js
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react(),
laravel({
input: ['resources/css/app.css', 'resources/js/app.tsx'],
refresh: true,
}),
],
});
Hot Module Replacement
// resources/js/app.tsx
import { createInertiaApp } from '@inertiajs/react';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
createInertiaApp({
resolve: (name) =>
resolvePageComponent(
`./Pages/${name}.tsx`,
import.meta.glob('./Pages/**/*.tsx')
),
setup({ el, App }) {
// HMR aceito por padrão
createRoot(el).render(<App />);
},
});
Performance Tuning
Vite Configurações
// vite.config.js
export default defineConfig({
build: {
cssCodeSplit: true,
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
'vendor': ['react', 'react-dom', '@inertiajs/react'],
'filament': ['@filament/support'],
},
},
},
},
optimizeDeps: {
include: ['react', 'react-dom', '@inertiajs/react'],
},
});
Lazy Loading de Componentes
// resources/js/components/LazyComponent.tsx
import { lazy } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
export function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
Precognition com Throttle
// Reduzir requests de validação
// Ordem dos argumentos: MÉTODO, URL, dados iniciais
const form = useForm('post', '/products', initialData);
// Configura o debounce da validação preemptiva (em milissegundos)
form.setValidationTimeout(500); // esperar 500ms
Casos de Uso
Form Multi-step com Precognition
interface Step1Data { name: string; }
interface Step2Data { price: number; }
interface Step3Data { description: string; }
export function MultiStepForm() {
const [step, setStep] = useState(1);
// Ordem dos argumentos: MÉTODO, URL, dados iniciais
const step1 = useForm<Step1Data>('post', '/products/step1', {});
const step2 = useForm<Step2Data>('post', '/products/step2', {});
const step3 = useForm<Step3Data>('post', '/products/step3', {});
const nextStep = () => {
// Valida os campos do passo atual (mesmo os que o usuário não tocou)
// e só avança no callback de sucesso.
const steps = [
{ form: step1, only: ['name'] },
{ form: step2, only: ['price'] },
{ form: step3, only: ['description'] },
];
const { form, only } = steps[step - 1];
form.validate({
only,
onSuccess: () => setStep(step + 1),
});
};
return (
<form>
{step === 1 && <Step1Form form={step1} onNext={nextStep} />}
{step === 2 && <Step2Form form={step2} onNext={nextStep} />}
{step === 3 && <Step3Form form={step3} onNext={handleSubmit} />}
</form>
);
}
Command Interativo com Prompts
<?php
final class SetupTenant extends Command
{
protected $signature = 'tenant:setup';
public function handle(): int
{
$this->info('Welcome to Tenant Setup!');
// Step 1: Basic info
$name = text('Tenant name', required: true);
$email = text('Admin email', required: true)
->validate('email');
// Step 2: Plan selection
$plan = select(
'Select plan',
[
'basic' => 'Basic - R$ 99/mês',
'pro' => 'Pro - R$ 199/mês',
'enterprise' => 'Enterprise - Custom',
]
);
// Step 3: Features
$features = multiselect(
'Select features',
['inventory', 'pos', 'reports', 'api'],
);
// Step 4: Confirm
if (! $this->confirm("Create tenant '{$name}' with {$plan} plan?", true)) {
return Command::FAILURE;
}
// Create tenant
$progress = $this->output->createProgressBar(4);
$progress->start();
$progress->advance();
$tenant = CreateTenantAction::run($name, $email, $plan, $features);
$progress->advance();
$this->run('db:seed', ['--class' => 'TenantSeeder']);
$progress->advance();
$this->run('storage:link');
$progress->finish();
$this->newLine();
$this->info("Tenant {$name} created successfully!");
table(
['Setting', 'Value'],
[
['Tenant ID', $tenant->id],
['Name', $tenant->name],
['Plan', $tenant->plan->name],
['Login URL', route('tenant.login', $tenant->id)],
]
);
return Command::SUCCESS;
}
}
Melhores Práticas
✅ FAÇA
- Use Precognition para forms complexos
- Throttle requests de validação
- Forneça feedback visual imediato
- Use Prompts para commands interativos
- Implemente progress bars para tarefas longas
- Configure HMR para desenvolvimento
- Lazy load componentes pesados
- Teste UX em múltiplos dispositivos
- Use table() para dados tabulares
❌ NÃO FAÇA
- Não implemente validação duplicada
- Não ignore feedback visual
- Não crie commands sem feedback
- Não use Prompts para automação (sem interação)
- Não esqueça de tratar erros
- Não sobrecarregue o usuário com opções
- Não use HMR em produção
Checklist de UX
Antes de finalizar feature com UX:
- Precognition configurado para forms
- Throttle de validação implementado
- Feedback visual para todos estados
- Commands com Prompts criados
- Progress bars para tarefas longas
- HMR configurado e funcionando
- Lazy loading de componentes
- Tratamento de erros amigável
- Testes de UX realizados
- Acessibilidade verificada
Referências Cruzadas
- Validation: Integrado com Form Requests de
architecture - i18n: Mensagens de
i18nem Prompts e forms - Actions: Commands usam Actions de
actions - Testing: Testar UX com
testing
Referências
- Laravel Precognition - Documentação oficial
- Laravel Prompts - Documentação oficial
- Vite HMR - API Hot Module Replacement
- Inertia.js - Documentação Inertia