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.

SKILL.md

GitHub

ux

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 i18n em Prompts e forms
  • Actions: Commands usam Actions de actions
  • Testing: Testar UX com testing

Referências