Componentes
4. Busca
seed-componentes.md v1.43 · §04seção 6 de 9804-busca-estavel-validado-pelo-rafael-em-2026-08.md · MD5 9c6d4c7dTítulo completo no canon: Busca — estável · validado pelo Rafael em 2026-08-02 (v0.8)
Consome o form-field (§2) e o input (§3): herda moldura, tokens, contrato HTML e o clear (C2). Esta seção documenta o que é da busca: as duas variantes por papel, a semântica de landmark, o feedback acessível de resultados, o estado zero-resultados e o comportamento por contexto/plataforma.
Base de evidência: 3 rodadas (2026-08-02): Carbon (search + active search pattern), National Archives, ICDS/DWP, Baymard (search-within, query types, no-results), Pencil&Paper (enterprise), UXPin, GOV.UK a11y (caso Léonie Watson) + alphagov (debounce de live region) · ecossistema command palette (Linear/Slack/VS Code/cmdk-shadcn, Maggie Appleton), Algolia (4 guias, mobile) · Material 3 SearchBar/SearchView (Android), EUI/Elastic, NN/g + Baymard no-results. Decisões D1–D9 aprovadas pelo Rafael em 2026-08-02.
4.1 Papel — duas variantes, um componente#
busca-filtro |
busca-navegacional |
|
|---|---|---|
| Emprego | Filtrar dataset visível (tabelas do ERP, listas, chat) | Buscar e IR para resultados (site institucional, busca global) |
| Disparo | Tempo real com debounce (Carbon active search: roda a cada caractere, SEM botão) | Enter/botão → página ou superfície de resultados |
| Escopo | Módulo/tabela atual por padrão (Baymard: usuários esperam fortemente "buscar dentro de onde estou"; escopo global só explícito) | Global, com escopo opcional |
| Botão | Não existe | Existe (visível no site; ícone-submit no app) |
Régua tempo real × submit (D5, UXPin): dataset pequeno/médio de resposta rápida → tempo real; consulta pesada de servidor → Enter/submit.
4.2 Anatomia#
busca-filtro: [🔍] [campo………………………] [✕] [⌘K*] *chip de atalho, só busca global desktop
busca-navegacional: [🔍 campo……………………… ✕ ▐Buscar▌] ← GRUPO ATADO: botão DENTRO da moldura
Grupo atado na navegacional (v0.8, decisão da validação de 2026-08-02): o botão Buscar vive DENTRO da moldura do campo (height 100%, radius contínuo do wrapper, sem gap) — campo e botão lidos como UMA peça. Porquê: botão externo de mesma altura nominal cria ilusão de desalinhamento (44px de verde cheio ao lado de 44px com borda fina) e mantém viva uma classe inteira de bugs de alinhamento; atado, o alinhamento é estrutural. Padrão National Archives/sites de busca. Descartado: botão externo com gap (a origem do defeito visual pego pelo Rafael em duas rodadas de validação). O foco do grupo usa :focus-within no wrapper (§3.10). · Lupa 20px no slot leading (aria-hidden) · campo herda §3 (DOM estável) · clear herda C2 integralmente + Esc também limpa (Carbon) · chip de atalho: slot de descobribilidade do futuro command palette (fronteira §4.11), tipografia JetBrains Mono 11px, rótulo POR PLATAFORMA (⌘K no Mac, Ctrl+K no Windows/Linux — mostrar ⌘ para quem não tem a tecla é ruído). Comportamento intermediário (v0.8, decisão de 2026-08-02): enquanto a palette não existe, Ctrl/⌘+K FOCA a busca global (com preventDefault — rouba o atalho do navegador — e aria-keyshortcuts declarado); o chip é um botão real que também foca. Quando a palette nascer (Bloco 5/6), o mesmo atalho passa a abri-la — a memória muscular treinada não se perde. Origem: pergunta do Rafael na validação do preview ("como fazer o ⌘K funcionar") — a resposta virou spec.
4.3 Decisões D1–D9 (aprovadas 2026-08-02)#
| # | Decisão | Porquê | Descartado |
|---|---|---|---|
| D1 | Duas variantes por papel | Carbon separa active search (sem botão) de navegacional; empregos e teclado opostos | Componente único ambíguo |
| D2 | label-hidden legítimo + placeholder INSTRUTIVO |
Carbon: lupa+placeholder bastam, evite rótulo; Baymard/Pencil&Paper: placeholder ensina O QUE é buscável — antídoto enterprise para "não sei o que tem no dataset" | Rótulo visível obrigatório (GOV.UK-family — mantido como opção da navegacional em site com heading); placeholder decorativo |
| D3 | role="search" + type="search" + enterkeyhint="search" |
Landmark navegável por leitor de tela; teclado virtual com tecla Buscar | div sem landmark; type=text |
| D4 (emendada pela D4-b) | Live region anuncia SÓ a contagem, polite+aria-atomic, debounce ~300ms; contagem SEMPRE visível (incl. zero) |
Caso GOV.UK/Léonie Watson: live region na lista inteira lê TODOS os resultados a cada tecla; alphagov: sem debounce, contagens obsoletas são anunciadas | Live region no container de resultados; assertive (interrompe a digitação) |
| D4-b (v0.13 — supersede do markup D4) | Contagem em DOIS nós: o visível (sem role) atualiza a cada tecla; o falado é nó separado visually-hidden role="status" debounced ~300ms — aplicação retroativa do §1.13 ("separada do nó visual") |
Validação executada 2026-08-03: o nó único do markup original defasava o visual em 300ms e mostrava contagem obsoleta — exatamente os bugs GOV.UK que motivaram o §1.13 | Nó único visível+falado (markup D4 original) |
| D5 | Tempo real × submit por volume; escopo ao contexto atual por padrão | UXPin (custo de servidor); Baymard: 94% mobile não suportam "buscar dentro" → saltos de escopo e resultados irrelevantes | Tempo real universal; escopo global por padrão no ERP |
| D6 | Fronteiras: command palette → pattern futuro (roadmap); sugestões/autocomplete → anatomia do combobox; chip ⌘K como slot | Palette é composto (modal+combobox+lista) e pressupõe navegação visível saudável (Appleton: ponte GUI↔CLI, não band-aid); shadcn Command/cmdk já na stack quando chegar a hora |
Palette como componente do Bloco 2; sugestões duplicadas na busca e no combobox |
| D7 | Plataforma: busca GLOBAL em toque expande em superfície full-screen ao focar; desktop = dropdown ancorado ao campo; filtro LOCAL permanece inline | M3 SearchBar/SearchView: full-screen no mobile, docked em tela grande; buscas anteriores aparecem na expansão | Dropdown minúsculo sobre teclado virtual; full-screen para filtro de lista (mata o contexto) |
| D8 | Barra composta com filtros estruturados → Bloco 6 (Dados); sintaxe de query avançada → camada de produto | EUI/Elastic (a dona do Elasticsearch) separa formalmente EuiFieldSearch do EuiSearchBar (query builder) | Filtros e sintaxe embutidos no componente de busca |
| D9 | Estado zero-resultados com declaração inequívoca + recuperação por contexto | NN/g: usuários frequentemente NEM PERCEBEM que não houve resultado; Baymard: ~50% dos sites sem caminho de recuperação, 68% beco sem saída; estratégias comprovadas incluem contato direto | "0 resultados" mudo; esconder a lista sem explicar |
4.4 Estados — nota estrutural#
A busca NÃO valida conteúdo: os estados error/warning/success do §2.4 não se aplicam (não existe "busca errada" — existe busca sem resultados, que é estado da EXPERIÊNCIA, não do campo). Estados do campo: default, hover, focus, filled (com clear visível), loading (consulta em voo — spinner + "Consultando…"), disabled (uso restrito §2.4). Detalhe técnico: type="search" dispara clear NATIVO no WebKit — suprimir (::-webkit-search-cancel-button{display:none}) para não duplicar com o nosso clear (C2).
4.5 Feedback de resultados (D4)#
Contagem visível junto aos resultados: "12 resultados" / "12 resultados para 'fazenda'" — sempre, inclusive "0 resultados" (Carbon). Espelho acessível: live region separada (role="status", aria-atomic="true"), atualizada com debounce de ~300ms, contendo SÓ a frase de contagem.
4.6 Zero resultados (D9) — anatomia e microcopy por contexto#
Declaração + eco da query + recuperação:
| Contexto | Microcopy padrão | Recuperação |
|---|---|---|
| ERP (filtro) | "Nenhum resultado para 'x' em Propostas." | [Limpar busca] [Buscar em todo o sistema] — ataca o salto de escopo |
| Site | "Não encontramos nada para 'x'." | Buscas alternativas/áreas do site + "Fale com a engenharia" (telefone/WhatsApp) — o beco sem saída vira lead |
| App | Idem site/ERP conforme a variante | Recentes + populares na superfície expandida (Algolia/M3) |
Nunca esconder que não houve resultado; nunca deixar a tela simplesmente vazia. O componente visual completo de empty state nasce no Bloco 3 — aqui fica a REGRA; lá, a peça.
4.7 Comportamento por contexto (tabela integrada — compromisso de 2026-08-02)#
| Aspecto | Produto (ERP/chat) | Site institucional | App/toque |
|---|---|---|---|
| Variante | filtro, escopada ao módulo | navegacional (submit → resultados) | filtro OU navegacional, full-width |
| Acesso | toolbar; futuro ⌘K com chip visível | header, campo com botão | ícone que expande OU campo fixo; botão no lugar do atalho (não há Cmd no toque) |
| Ao focar (busca global) | dropdown ancorado ao campo | — | superfície full-screen (M3) com recentes |
| Sugestões | recentes no empty (quando houver combobox) | query suggestions (fronteira combobox) | desde o 1º caractere, 6–8 máx (Algolia) |
| Feedback | contagem + live region | página de resultados com contagem | skeleton/progress em rede lenta (Algolia) |
| Zero resultados | limpar/ampliar escopo | alternativas + Fale com a engenharia | recentes + populares |
4.8 Acessibilidade#
role="search" no container (um por página com nome distinto se houver múltiplas: aria-label="Buscar propostas") · label visualmente oculto SEMPRE presente (<label class="sr">Buscar propostas</label>) · clear e botão no tab order · Esc limpa com foco permanecendo no campo · live region conforme §4.5 · lupa aria-hidden · contraste herdado (nenhum par novo).
4.9 Microcopy#
Placeholder instrutivo NOMEIA o buscável: "Buscar por cliente, UC ou proposta…" (produto) · "Buscar no site…" + rótulo oculto específico (site). Contagem sempre com a query ecoada quando há espaço. Zero-resultados: tabela §4.6 — tom SEED: direto, sem culpar ("Não encontramos nada para 'x'", nunca "Sua busca falhou").
4.10 Código — delta sobre §3 (HTML/CSS e React)#
<search class="seed-search" role="search" aria-label="Buscar propostas">
<label class="seed-visually-hidden" for="busca-prop">Buscar propostas</label>
<div class="seed-input">
<svg class="seed-search__icon" aria-hidden="true" viewBox="0 0 20 20" width="20" height="20"><path fill="currentColor" d="M8.5 2a6.5 6.5 0 1 0 3.94 11.66l4.2 4.2 1.42-1.42-4.2-4.2A6.5 6.5 0 0 0 8.5 2Zm0 2a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Z"/></svg>
<input id="busca-prop" type="search" inputmode="search" enterkeyhint="search"
autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false"
placeholder="Buscar por cliente, UC ou proposta…">
<button class="seed-input__btn" type="button" aria-label="Limpar busca" hidden>…✕…</button>
</div>
<p class="seed-search__count" role="status" aria-atomic="true"></p><!-- só a contagem, debounced -->
</search>
.seed-search input[type="search"]::-webkit-search-cancel-button{display:none} /* clear nativo suprimido — C2 assume */
.seed-search__icon{color:var(--seed-field-help-text);flex:none}
.seed-search__count{font-size:12px;color:var(--seed-field-help-text)}
export function useDebouncedStatus(count: number | null, query: string, delay = 300) {
const [msg, setMsg] = React.useState("");
React.useEffect(() => {
if (count === null) return;
const t = setTimeout(() =>
setMsg(query ? `${count} resultado${count === 1 ? "" : "s"} para "${query}"`
: `${count} resultado${count === 1 ? "" : "s"}`), delay);
return () => clearTimeout(t); /* debounce: mata anúncio obsoleto (alphagov) */
}, [count, query, delay]);
return msg; /* renderizar no nó FALADO (visually-hidden role="status" aria-atomic) — o nó visível recebe a contagem crua a cada tecla (D4-b) */
}
export function SearchInput(props: Omit<InputProps, "prefix"> & { landmarkLabel: string }) {
const { landmarkLabel, ...rest } = props;
return (
<search role="search" aria-label={landmarkLabel}>
<label className="sr-only" htmlFor={rest.id}>{landmarkLabel}</label>
<Input prefix={<SearchIcon className="size-5" aria-hidden />} clearable
type="search" enterKeyHint="search" autoComplete="off"
autoCapitalize="off" spellCheck={false} {...rest} />
</search>
);
}
4.11 Fronteiras registradas#
Command palette (⌘K): pattern candidato do ERP, Bloco 5/6 — shadcn Command (cmdk) é a base quando chegar; pré-requisito: navegação visível saudável. · Dropdown de sugestões/autocomplete: anatomia do combobox (item deste bloco); busca-com-sugestões = composição dos dois. · Barra com filtros estruturados + sintaxe de query: Bloco 6 / camada de produto (EUI). · Sugestões geradas por usuários exigem moderação (caso Castorama: site fora do ar por sugestões impróprias) — regra de produto. · Peça visual de empty state: Bloco 3.
4.12 Aplicação dos 7 testes (§1.12)#
- Container 3:1: herda §2/§3 (nenhum par novo). ✅ 2. Mecanismos: filled = clear presente; loading = spinner+texto; zero-resultados = mensagem+ações (nunca só ausência). ✅ 3. Grayscale: lupa/✕/spinner independem de matiz. ✅ 4. Par: filtro × navegacional distinguíveis pelo botão/chip. ✅ 5. Regra do um: uma live region, uma frase de contagem. ✅ 6. Estado atual: query ecoada na contagem e no zero-resultados — quem chega agora sabe O QUE foi buscado. ✅ 7. Polegar 360px: full-width, fonte 16px, clear com hit-area plena, contagem visível acima da dobra com teclado aberto; busca global expande full-screen (D7). ✅
Também cita o §4: banco-dataviz-di, banco-dataviz-dp, banco-navegacao, tela-busca, tela-gantt, tela-painel.