---
fonte: 01-canonicos/seed-componentes.md
versao_da_fonte: v1.43
secao: 04
titulo: "Busca — `estável` · validado pelo Rafael em 2026-08-02 (v0.8)"
sequencia: 6 de 98
bytes_do_corpo: 14013
md5_do_corpo: 9c6d4c7dcbb2f5d907a3618df5d94a2e
gerado_por: 06-validacao/geradores/gen-camada-ia.py
nota: fatia GERADA — o corpo abaixo é byte a byte o trecho do canônico; edite o canônico, nunca esta fatia. Canônico inteiro em https://ds.seed.eng.br/01-canonicos/seed-componentes.md
---
## 4. 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)

```html
<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>
```

```css
.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)}
```

```tsx
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)

1. 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). ✅

---

