# Contrato · Bloco

> Segue o modelo de `contratos/ruler.md`. Implementado em
> `app/src/componentes/Block/Block.tsx`, com CSS em
> `estilo/12-v3-terreno.css` (`.shell`/`.bloco`/`.cab`) — a **mesma** folha
> que o protótipo carrega. Especificação original em
> `handoff/componentes.md` §3.
>
> **Renomeado de `bloco.md` em 21/set/2026**: os identificadores de código
> viraram `Block`/`BlockHeader`/`BlockTitle`/`BlockActions`/`BlockBody`/
> `BlockFootnote`/`BlockEmpty`/`BlockLoading` (eram `Bloco`/`BlocoCabecalho`/
> `BlocoTitulo`/`BlocoAcoes`/`BlocoCorpo`/`BlocoNota`/`BlocoVazio`/
> `BlocoCarregando`), e as props de `BlockEmpty`/`BlockLoading` também
> (`icone`→`icon`, `linhas`→`lines`) — mesma leva do `Botao`→`Button`, ver
> `contratos/button.md` pro motivo. As classes CSS (`.shell`, `.bloco`,
> `.cab`...) não mudaram.

O cartão que embrulha toda seção da tela — "Leis e incentivos do estudo",
"Documentos do estudo", cada assunto da coluna esquerda do terreno.

## Anatomia

| Parte | Papel | Obrigatória |
|---|---|---|
| `Block` | `<div class="shell"><div class="bloco">`, o envelope | sim |
| `BlockHeader` | `.cab` — linha do topo | não |
| `BlockTitle` | `<h3>` dentro do cabeçalho | não, mas quase sempre presente |
| `BlockActions` | grupo de ação empurrado pro fim do cabeçalho | não |
| `BlockBody` | o conteúdo — **transparente**, ver abaixo | não |
| `BlockFootnote` | nota de pé opcional, hairline em cima | não |
| `BlockEmpty` | resposta para corpo sem dado (Regra 4) | usar dentro de `BlockBody` |
| `BlockLoading` | resposta para corpo em carregamento (Regra 4) | usar dentro de `BlockBody` |

```tsx
<Block>
  <BlockHeader>
    <BlockTitle>Leis e incentivos do estudo</BlockTitle>
    <BlockActions>
      <Button variant="outline" size="sm">Adicionar</Botao>
    </BlockActions>
  </BlockHeader>
  <BlockBody>
    {/* linhas de conteúdo — ou BlockEmpty, ou BlockLoading */}
  </BlockBody>
  <BlockFootnote>Não inclui laudo de solo, passivo ambiental nem custo de aprovação.</BlockFootnote>
</Block>
```

## Por que `BlockBody` é transparente (não renderiza `<div>`)

A marcação real do protótipo não tem uma camada "corpo": `.cab` e as linhas de
conteúdo (`.doc`, por exemplo) são **irmãos diretos** dentro de `.bloco`.

```html
<div class="shell"><div class="bloco">
  <div class="cab"><h3>Leis e incentivos do estudo</h3></div>
  <div class="doc">...</div>
  <div class="doc">...</div>
  <button class="add-lei">...</button>
</div></div>
```

Se `BlockBody` renderizasse uma `<div>`, essa camada extra mudaria quem é
irmão de quem — e o CSS deste sistema usa seletor de irmão de propósito
(`.bloco+.bloco{border-top:...}`; `.doc` já usa `:first-of-type`). Uma `<div>`
a mais quebraria esse tipo de regra sem gerar erro nenhum: o mesmo risco que
o comentário sobre `display` vencer `hidden` em `estilo/00-tokens.css` já
descreve para outro caso. `BlockBody` é um Fragment — existe só para o JSX
ficar autoexplicativo, não deixa rastro no DOM. Testado: um filho dentro de
`BlockBody` aparece como irmão direto de `.cab`, não como neto.

## Composição, não configuração (Regra 7)

`Block` não tem prop `titulo`, `carregando` nem `vazio`. O consumidor decide o
que renderiza dentro de `BlockBody`:

```tsx
<BlockBody>
  {carregando ? (
    <BlockLoading lines={4} />
  ) : cenarios.length === 0 ? (
    <BlockEmpty icon="search">
      <b>Nenhum cenário viável</b> para este terreno com as premissas atuais.
    </BlockEmpty>
  ) : (
    cenarios.map((c) => <LinhaCenario key={c.id} {...c} />)
  )}
</BlockBody>
```

Três booleanos (`carregando`/`vazio`/`erro`) seriam oito combinações, e a
metade não faria sentido nenhum. `children` é uma decisão só, a certa.

## Estados (Regra 4)

`componentes.md` pede três: "com conteúdo, vazio e carregando". `Block`
entrega os três — `BlockEmpty` e `BlockLoading` são componentes, não
props, então nunca renderizam junto por engano.

| Estado | Como | Observação |
|---|---|---|
| com conteúdo | `children` livre dentro de `BlockBody` | — |
| vazio | `<BlockEmpty>` | Regra 4: "vazio não é erro, é resposta legítima do produto" — mensagem, não desculpa |
| carregando | `<BlockLoading lines={n} />` | barra do tamanho aproximado do texto, não spinner — ver abaixo |

## Por que `BlockLoading` é barra, não spinner

Um spinner conta o tempo passando; uma barra do tamanho aproximado da linha
real conta **onde** o conteúdo vai aparecer. Quando o dado chega, o layout já
tinha a forma certa reservada — menos salto, menos piscada. As quatro
larguras (`.barra:nth-child(1..4)`, de 58% a 94%) existem só pra não parecer
grade de tijolo idêntica; acima de 4 linhas, as barras seguintes usam a
largura padrão do bloco (100%), o que é aceitável e não foi otimizado além
disso.

Anima `opacity`, não `width`: mais barato pro navegador, não dispara reflow a
cada quadro, e respeita `prefers-reduced-motion` — mesmo padrão já usado em
`.coach` (`estilo/02-shell.css`).

## Achado construindo, não estava no CSS

`componentes.md` descreve "cabeçalho com título e ações opcionais à direita",
mas **nenhum `.cab` do protótipo hoje tem mais que o `h3`** — conferido com
grep no arquivo inteiro. A regra `.cab-acoes{margin-left:auto;...}` é nova,
adicionada em `estilo/12-v3-terreno.css`, e usa o mesmo idioma que `.t-acoes`
já usa no mesmo arquivo (`margin-left:auto` no grupo de ação, não
`justify-content` no pai) — não um jeito novo de alinhar à direita.

Da mesma forma, `.bloco-nota`, `.bloco-vazio` e `.bloco-carregando` são
CSS **novo**, porque nenhum bloco do protótipo tinha nota de pé, estado vazio
nem estado de carregamento — "novo" é o próprio `componentes.md` que marca o
Número, mas os **estados** de Block também são invenção nova sobre uma
anatomia existente, não extração.

### Contraste, medido e corrigido durante a construção

A primeira versão de `.bloco-nota` usava `color:var(--t3)` (13px) e a story
de exemplo usava `var(--t4)` (11,5px). O `test:vitrine` (axe em Chromium
real) reprovou os dois: **4,31:1** e **4,19:1** respectivamente, contra o
4,5:1 exigido — o mesmo achado que `app/src/paginas/Layout.css` já registra
para `--t3`/`--t4` sobre `--mundo`. Corrigido pra `--t2` (7,95:1) nos dois
lugares. Não é a primeira vez que esse defeito específico aparece nesta
sessão, o que é o próprio argumento para o gate automatizado existir: o erro
se repete, o teste pega toda vez.

## Acessibilidade

- `BlockLoading` usa `role="status"` com `aria-label="Carregando"`, então
  o leitor de tela anuncia sem depender de texto visível repetido em cada
  barra decorativa.
- `BlockEmpty` aceita `icon` opcional, via o componente `Icon` (decorativo
  por padrão — `aria-hidden`), nunca SVG solto.
- Contraste de todo texto novo medido em Chromium real via `test:vitrine`,
  não calculado de cabeça.

## Fora do contrato

- **`Popover`, `Option`, `Document`** não são parte deste componente. As
  linhas dentro de `BlockBody` no exemplo da vitrine (`Vitrine` em
  `Block.stories.tsx`) usam marcação solta (`<div>`) porque a story é mais
  antiga que o `Document` — hoje ele já existe e seria o certo a compor ali;
  achado desatualizado numa auditoria em 21/set/2026, não corrigido nesta
  leva (é troca de conteúdo de story, fora do escopo do rename).
- **Nenhuma prop de layout** (`padding`, `largura`) — `.bloco{padding:22px
  24px}` é fixo, igual ao CSS real. Se algum dia precisar de padding
  diferente, é token novo com nome de função, não número solto numa prop.

## Pendências deste componente

- **Sem região ARIA nomeada.** `Block` não liga `aria-labelledby` do `<h3>`
  ao contêiner — cada bloco não é hoje um landmark `role="region"`
  navegável por leitor de tela como uma seção nomeada. Ficaria de pé com
  `useId()` + contexto interno; não implementado porque nenhum outro
  componente desta sessão usa Context, e o ganho não estava no escopo
  pedido pelo `componentes.md`.
- **`BlocoErro` não existe.** A Regra 4 do `AGENTS.md` pede quatro saídas
  para tela com dado — carregando, **erro**, vazio, sucesso —, mas
  `componentes.md` §3 lista só três para Block especificamente ("com
  conteúdo, vazio e carregando"). A lacuna é real e está registrada aqui,
  não silenciada.
- **`ComAcaoNoCabecalho` é a única prova visual** de que `BlockActions`
  funciona — nenhum bloco do protótipo aprovado usa essa capacidade ainda,
  então "visualmente igual ao protótipo" não pode ser verificado por
  comparação de tela real, só pela consistência com `.t-acoes`.
- ~~Sem página de documentação própria.~~ **Resolvido.** Block é um dos 7
  da Fase 1 — sempre teve página própria em `/componentes/bloco`
  (`BlocoDoc.tsx`, 27/ago/2026). A frase estava errada desde que foi
  escrita, não só desatualizada; achado na auditoria de 21/set/2026.
