# Contrato · Dialog

> Segue o modelo de `contratos/ruler.md`. Implementado em
> `app/src/componentes/Dialog/Dialog.tsx`, com CSS em
> `estilo/20-popover-torrada.css` (`.onb`) — a **mesma** folha que o
> protótipo carrega. Sem contrato original: não é um dos 7 da Fase 1, nem
> tinha spec em `handoff/componentes.md` — construído a partir do CSS/HTML
> reais do onboarding, achados revisando os prints do usuário (26/ago/2026).

> **Não é um dos 7 da Fase 1.** Tem página própria em `/componentes/dialog`
> desde 27/ago/2026, em "Componentes adicionais" — não "Componentes · N de 7",
> que é só pros 7 da Fase 1.

**Use `@radix-ui/react-dialog`. Não implemente foco, teclado nem
modalidade.** — Regra 5 do AGENTS.md.

## Escopo desta entrega: só o envelope genérico

O onboarding real do protótipo é um assistente de várias telas com anel de
progresso, opções de rádio customizadas (`.op`), campos numéricos
(`.numerao`) e resumo (`.resumo`). Isso é **conteúdo de produto** específico
do fluxo de onboarding, não parte do primitivo Dialog — mesmo raciocínio que
excluiu a variante compacta da Régua. Este componente entrega o que
**qualquer** modal do sistema vai precisar: moldura, X de fechar, título,
descrição, corpo, rodapé.

## Anatomia

| Parte | Componente | Classe |
|---|---|---|
| disparador | `DialogTrigger` | — |
| fundo | (interno a `DialogContent`) | `.onb` |
| moldura | `DialogContent` | `.onb-card` |
| fechar (canto) | interno a `DialogContent`, `closeButton` opcional | `.onb-red` |
| cabeçalho | `DialogHeader` | `.onb-cab` |
| título | `DialogTitle` (obrigatório, ver Acessibilidade) | `h2` |
| descrição | `DialogDescription` (opcional) | `.sub` |
| corpo | `DialogBody` | `.onb-corpo` |
| nota | `DialogNote` (opcional) | `.onb-nota` |
| rodapé | `DialogFooter` | `.onb-pe` |
| espaçador do rodapé | `DialogFooterSpacer` | `.pe-vao` |
| fechar (genérico) | `DialogClose` | — |

```tsx
<Dialog>
  <DialogTrigger asChild><Botao peso="marca">Abrir calibragem</Botao></DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Em qual padrão de empreendimento vocês querem focar agora?</DialogTitle>
      <DialogDescription>Define o padrão que testamos primeiro.</DialogDescription>
    </DialogHeader>
    <DialogBody>{/* conteúdo específico do produto */}</DialogBody>
    <DialogFooter>
      <DialogClose asChild><LinkButton>Responder depois</LinkButton></DialogClose>
      <DialogFooterSpacer />
      <DialogClose asChild><Botao peso="marca">Continuar</Botao></DialogClose>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

`DialogContent` sempre embrulha `children` em `.onb-main` (padding + scroll
— estrutural, não opcional) e sempre renderiza o X do canto por padrão
(`closeButton={false}` tira, caso raro).

## Comportamento (da primitiva, verificado no navegador — Playwright, story
"Comportamento" em `Dialog.stories.tsx`, roda no `test:vitrine`)

- abre no clique
- foco entra no conteúdo (trap) — não fica no disparador
- Esc fecha **e** devolve o foco ao disparador
- X fecha, e devolve o foco ao disparador
- clique no fundo (`.onb`) fecha
- **modalidade de verdade**: o resto da página fica `aria-hidden` e com
  `pointer-events:none` enquanto o modal está aberto — achado
  escrevendo o teste: `userEvent.click(document.body)` foi recusado
  ("Unable to perform pointer interaction... pointer-events: none"), porque
  o clique de "fora fecha" precisa mirar o **overlay** (`.onb`), não
  `document.body` — o próprio `body` está inerte enquanto o modal está aberto.

## Achado construindo, não estava previsto

- **`.onb-card` precisou de posicionamento próprio.** O CSS original contava
  com `.onb-card` sendo FILHO de `.onb` (`display:grid;place-items:center`
  centralizava o filho) — mas Radix renderiza `Overlay` e `Content` como
  IRMÃOS dentro do mesmo Portal, nunca aninhados. `.onb-card` ganhou
  `position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)` própria,
  e `width:min(444px,calc(100% - 48px))` no lugar de `width:min(444px,100%)`
  pra preservar a margem de borda que o `padding:24px` do `.onb` original
  dava via grid.
- **Sem animação de entrada/saída** — mesmo motivo já registrado em
  `contratos/popover.md`: `transition` de CSS liga a `Presence` interna do
  Radix, que espera a transição acabar antes de desmontar. Sem uma
  animação de saída implementada de verdade, isso só cria uma janela onde
  dois modais coexistem — já medido uma vez no Popover, não repetido aqui
  de propósito.
  - Diferente do Popover: aqui um `transform` ESTÁTICO (sem transição) não
    colide com nada da primitiva, porque Radix Dialog não usa Floating
    UI/Popper (não há disparador nem colisão de borda pra calcular) — só a
    TRANSIÇÃO precisou sair, não o transform de centralização em si.
- **"Clique fora" tem que mirar o overlay, não `document.body`.** Achado
  real escrevendo o `play()`: Radix desativa `pointer-events` do resto da
  página enquanto o modal é modal — comportamento correto da primitiva, mas
  quebrou a primeira versão do teste, que clicava em `document.body` direto.

## Acessibilidade

- `role="dialog"` com nome acessível via `RadixDialog.Title`
  (`aria-labelledby` automático) — **obrigatório**: Radix já avisa no
  console se `DialogTitle` faltar, não reimplementado aqui (diferente do
  Popover, que precisou de checagem própria porque `Popover.Content` não
  tem um mecanismo equivalente).
- Foco trap dentro do conteúdo, devolvido ao disparador ao fechar.
- Resto da página `aria-hidden` + inerte enquanto aberto (modalidade real).

## Fora do contrato

- **O onboarding real (várias telas, progresso, campos próprios)** — ver
  "Escopo desta entrega" acima.
- **Foco, teclado e modalidade** são da primitiva.

## Pendências deste componente

- **O onboarding específico do produto não foi construído** — anel de
  progresso, opções `.op`, campos `.numerao`, resumo `.resumo`. Um
  componente novo (ou vários) por cima deste envelope, quando houver
  necessidade real.
- **Testes de interação em jsdom não cobrem o comportamento real** — mesmo
  raciocínio do Popover: a verificação mora no `play()` da story
  "Comportamento" (Chromium real via `test:vitrine`).
- ~~Sem página de documentação própria, sem entrada na barra lateral.~~
  **Resolvido** — ver nota no topo do contrato. Achado desatualizado na
  auditoria de 21/set/2026.
