Busca global (⌘K)
Command palette do header: ações, programas do menu.xml e registros pesquisáveis.
Busca global — command palette ⌘K
Palette do header estilo VSCode/Linear: atalho global ⌘K / Ctrl+K, três fontes
de resultado, tudo gateado pelo mesmo gate das rotas
(Mad\Security\PermissionGate::canAccess — fail-closed).
| Seção | Fonte | Quando aparece | Endpoint |
|---|---|---|---|
| Ações | config('mad.search.actions') |
termo casa o label (topo) | embutida no fragmento |
| Programas | menu.xml (permissão + tradução _t{}) |
sempre; scroll infinito 20/20 | embutida no fragmento |
| Registros | config('mad.search.records') |
lazy: 2+ caracteres, debounce 250ms | GET /app/SearchBox/records?static=1&q= |
Arquitetura
layout.blade.php (#search_box_slot na pill do header)
└─ MadTemplate.loadSearchBar()
└─ fetchFragment('app/SearchBox/menu?static=1')
└─ App\Control\Sys\SearchBox::menu() → resources/views/shell/search-box.blade.php
├─ items = SearchBox::items() (menu.xml + permissão)
├─ actions = SearchBox::actions() (config + permissão)
└─ Alpine: ⌘K, ↑↓/↵/esc, overlay teleportado,
fetch lazy de /app/SearchBox/records
App\Control\Sys\SearchBox estende MadComponent apenas pelo contrato do
dispatcher (MadAppController só roteia subclasses); os entry points reais são os
métodos estáticos menu() e records(), chamados via ?static=1. Registro da
rota em routes/web.php:
MadRoutes::exposeClass('SearchBox');
A classe está nos defaultPermissions (todo usuário logado tem acesso). Anônimo
recebe redirect de login — o menu nunca vaza para quem não está autenticado.
mad.search.actions — comandos rápidos
// config/mad.php
'search' => [
'actions' => [
[
'label' => 'Nova mensagem',
'icon' => 'lucide:send', // formato menu.xml (fa:/fas:/far:/mi:/lucide:/img)
'shortcut' => '', // dica VISUAL opcional (não registra tecla)
'program' => 'MessageForm', // gate; null = todo logado
'url' => ['MessageForm'], // [classe] ou [classe, método] → URL amigável
],
[
'label' => 'Alternar tema escuro',
'icon' => 'lucide:moon',
'program' => null,
'js' => 'notchToggleDark', // função GLOBAL do casco; roda local e fecha o palette
],
],
'records' => [],
],
Contrato: url ou js (url ganha se ambos estiverem presentes). url é
montada por MadRoutes::toFriendlyUrl() no servidor — o cliente nunca monta rota
manualmente. Ação js executa window[nome]() e não navega.
mad.search.records — registros pesquisáveis
'search' => [
'records' => [
[
'model' => 'TesteCliente', // FQCN ou short name (App\Models\)
'database' => 'minierp', // conexão EXPLÍCITA (default: 'business')
'columns' => ['nome'], // LIKE %q% com OR entre colunas
'display' => '{nome}', // máscara do título
'subtitle' => '{cidade} ({uf})', // máscara da linha 2 (opcional)
'icon' => 'fas:user',
'program' => 'TesteClienteForm', // gate = TELA DE DESTINO
'edit' => ['TesteClienteForm', 'onEdit'], // navega com &id={pk}
],
],
],
- Fail-closed: sem
program, sem permissão nele, ou semedit→ a fonte inteira fica fora do resultado (nem chega a consultar o banco). - Conexão:
$cls::on($src['database'] ?? 'business')— omitirdatabasenão usa o$connectiondo model, cai embusiness. Declare sempre. - Busca:
orWhere($col, 'like', "%{$q}%")com OR entre ascolumns, dentro de umwhere(function …)— as demais cláusulas da fonte não vazam do agrupamento. - Limite de 5 resultados por fonte (
QuerySource::recordsFromQuery($qb, null, 5)); máscaras ({nome},{cidade} ({uf})) resolvidas porModelOptionsLoader::mask(). - Resiliente por fonte: se a query de uma fonte lançar (tabela/conexão ausente),
o
Throwableé logado viaerror_log('[SearchBox] fonte …')e só aquela fonte é pulada — o palette continua respondendo as outras. - A URL de edição é amigável (ex.:
/app/testes/cliente/onEdit?id=5) — abre o shell com o drawer/form de edição já preenchido. O id vem de$rec->id(coluna literalid), então o model da fonte precisa expor esse atributo. - Escala por config: uma nova fonte de busca entra com zero PHP — só uma entrada no array.
Gates de permissão
As três fontes usam \Mad\Security\PermissionGate::canAccess($program) — o
mesmo gate de MadAppController/rotas. Isso inclui os defaultPermissions
(telas universais como MessageForm).
Não use
SystemPermission::checkPermissionem features de busca/navegação: ele só olhasession('programs')+public_classese não inclui os defaults — telas universais somem da busca para um usuário real (bug já pego em validação visual). Regra: o que a rota deixa abrir, a busca pode mostrar.
Navegação
- Escolha (clique ou ↵) navega por URL amigável (
window.location = it.url). - Nunca
Mad.go(classe)para telas de slug: no modo allowlist, classe crua semexposeClassresponde 404 — a rota canônica é o slug (/app/usuarios, não/app/SystemUserList/show). - Teclado:
⌘K/Ctrl+Kabre ·↑↓navega as 3 seções como lista única (comscrollIntoViewno item ativo) ·↵abre ·escfecha. - Scroll infinito dos programas: janela de 20, expande a 48px do fim da lista E quando a seta ↓ chega perto do fim.
Gotchas conhecidos
| Sintoma | Causa | Fix |
|---|---|---|
| Editei a blade do palette e nada mudou | cache do MadBlade segura o fragmento compilado | rm -rf storage/framework/mad-blade/* |
| Programa novo não aparece na busca | session('programs') é populada no login |
logout/login (ou re-loadSessionVars) |
Editei MadTemplate.js e o browser ignora |
cache por ?appver= |
bump do appver na tag em resources/views/shell/partials/libraries-builder.blade.php |
| Item navega para 404 | alvo sem rota no allowlist | use o slug (MadRoutes::screen/expose) — toFriendlyUrl resolve sozinho se a rota existir |
| fetch de fragmento "falha" com 200 | dispatcher static=1 responde Content-Type: application/json |
use o helper fetchFragment do MadTemplate (já trata e loga falha com a URL) |
Testes
tests/Feature/SearchBoxTest.php — 10 cenários: permissão por fonte
(fail-closed), tradução/URL amigável, atalho global no markup, 2+ caracteres,
coexistência de fontes com permissões independentes, destino de edição
renderizando preenchido, ação js local.
Veja também
- Correio interno — a ação rápida "Nova
mensagem" do palette aponta para
MessageForm. - Roteamento › Rotas públicas —
MadRoutes::screen/expose/exposeClasse como o slug vira a URL amigável usada pela navegação do palette.