# Contrato · Badge

> Segue o modelo de `contratos/ruler.md`. Implementado em
> `app/src/componentes/Badge/Badge.tsx`, com CSS em `estilo/31-badge.css` — a
> **mesma** folha que o protótipo carrega, via `@import` em `10-v1-entrada.css`.

**Três eixos independentes**, e a composição é o quarto, que não mexe nos outros
três. A arquitetura veio do modelo do shadcn porque a versão anterior nossa não
escalava, e isso está escrito em comentário no próprio CSS.

O padrão de implementação é o que importa: **a variante define três custom
properties locais e a base as consome.** É o que faz um tom novo custar três
linhas em vez de reescrever a regra.

```
.bdg          { --bd-tinta; --bd-sup; --bd-cheio }      ← a base consome
.bdg-ok       { --bd-tinta: var(--ok-tx); ... }         ← a variante define
```

`--bd-tinta` usa o par **-tx** (tom escuro), não o tom 600, migrado em
25/ago/2026 pra paleta do shadcn: aqui a cor é texto sobre o tingimento pálido, e
o tom 600 sozinho reprova o 4,5:1. Ver `estilo/31-badge.css` e o CHANGELOG v0.7.0.

`--bd-tinta`, `--bd-sup` e `--bd-cheio` são **locais do Badge**. Não estão em
`00-tokens.css` e não deveriam estar: são mecanismo interno do componente, não
decisão do sistema.

## A contagem certa dos eixos

`handoff/componentes.md` erra **três vezes** sobre este componente. A contagem
real, lida do CSS e medida no navegador:

| Eixo | Quantos | Quais |
|---|---|---|
| TONE | **6** | `neutral` (base), `primary`, `success`, `warning`, `danger`, `muted` |
| WEIGHT | **4** | `soft` (base), `solid`, `outline`, `ghost` |
| SIZE | **3** | `sm`, `md` (base), `lg` |

O documento diz 5 tons (esquece `.bdg-marca`), diz 2 tamanhos, e afirma que `sm`
"não existe implementado". `.bdg-sm` está em `31-badge.css` e sempre esteve:
`padding:1px 6px; font-size:var(--fs-rotulo); --is:11px` (era `10.5px` literal,
corrigido na revisão de legibilidade de ponta a ponta de 26/ago/2026 — abaixo
do piso do sistema). Há teste travando os três.

## Props

| Prop | Tipo | Default | Observação |
|---|---|---|---|
| `tone` | 6 valores acima | `neutral` | define as três custom properties locais |
| `weight` | 4 valores acima | `soft` | |
| `size` | 3 valores acima | `md` | `--is`: 11 → 12 → 14px |
| `clickable` | boolean | `false` | vira `<button>`, ganha `.bdg-acao`, chama `onClick` |
| `removable` | boolean | `false` | mostra o X, chama `onRemove` |
| `rounded` | boolean | `false` | `.bdg-num`, 19×19, raio cheio |
| `removeLabel` | string | `Remover` | `aria-label` do X |
| `count` | `ReactNode` | `undefined` | vira `.cnt`. Só existe no DOM quando a prop existe |

`children` carrega o texto. React não tem slot nomeado como Vue; `count` é
prop porque é o jeito idiomático de expressar "conteúdo extra opcional" aqui —
mas continua aceitando qualquer nó (texto, número formatado, elemento), não só
booleano ou string fixa, então a Regra 7 (composição, não configuração)
continua valendo: não é uma flag que muda layout, é conteúdo.

### Sobre `muted`

Existe comentário no CSS explicando que `--t3` dá 4,23:1 sobre a própria tinta e
**reprova** em contraste. `--off` dá 6,3:1. **Não troque de volta.**

### `rounded` é a única exceção ao raio do sistema

`border-radius: var(--r-cheio)` num componente cujo padrão é `var(--r-s)`. O
motivo está no CSS: é um número solto, não uma caixa de texto.

## Estados

Só o **clicável** tem estado. Está escrito no CSS: *"clicável ganha estado,
estático não: se não é clicável, não reage ao mouse"*. Badge estático com
`cursor:pointer` prometeria uma ação que não existe. Há teste garantindo que
`.bdg-acao` não vaza para o estático.

| Estado | Medido no navegador |
|---|---|
| normal | `bg: rgb(231,245,235)` no tone `success` |
| hover | `bg: color-mix(--bd-tinta 14%)` |
| foco | `outline: 3px solid var(--anel)` (regra global) |
| pressionado | `transform: scale(.975)` (global de `button:active`) |
| **desabilitado** | **idêntico ao normal — não existe estilo** |

## Duas coisas que o CSS não resolve, e estão na vitrine escritas

### 1 · Não existe estilo de desabilitado

A Regra 4 exige os cinco estados. O badge clicável não tem o quinto: um
`<button class="bdg bdg-acao" disabled>` fica visualmente igual ao habilitado.

**Não foi inventado aqui.** Criar um estilo de desabilitado é decisão de design
nova (opacidade? tone `muted` forçado? borda tracejada?), não detalhe de
implementação. A vitrine mostra o estado com a etiqueta "desabilitado (sem
estilo)" em vez de esconder o problema.

### 2 · O anel de foco do X é recortado, e isso é defeito de acessibilidade

**Medido:** `.bdg` tem `overflow:hidden`. O anel global alcança 4px além da borda
(3px de linha + 1px de recuo). A caixa de padding do badge deixa **2px** de folga
em volta do X, que tem 16px numa caixa de 20px. Resultado: o anel é cortado em
**2px acima e 2px abaixo**.

Foco visível por teclado é obrigatório pela Regra 4, então isto é defeito, não
decisão. **Não foi consertado**, porque as duas saídas têm consequência real e a
escolha é de design:

- **crescer a folga** muda a altura do badge em toda tela que o usa;
- **anel interno** (`outline-offset` negativo, ou `box-shadow` inset) muda a
  estratégia de foco do sistema, que hoje é uma só e global.

## Acessibilidade

- Estático é `<span>`; clicável é `<button type="button">`. Há teste.
- O X é `<button>` **irmão** do texto, com `aria-label` próprio, e o ícone dentro
  é `aria-hidden`: quem usa leitor ouve o rótulo, não o desenho.
- `clickable` **e** `removable` juntos **quebram em desenvolvimento**: gerariam
  `<button>` dentro de `<button>`, HTML inválido cujo resultado varia entre
  navegadores — o defeito aparece como "funciona na minha máquina". Se a linha
  precisa das duas ações, o padrão certo é badge estático + botão de remover
  irmão, não filho.
- Cor não é o único indicador: o tone sempre acompanha texto.

## Fora do contrato

### Badge nosso não tem ícone

Está escrito em comentário no CSS, com o motivo: o rótulo já diz tudo ("Acima",
"Atravessa"), então o ícone é um segundo sinal para a mesma informação, no tamanho
exato em que ícone falha, na tela de baixa densidade em que o usuário trabalha. O
shadcn e o shadcnblocks têm porque são biblioteca genérica; biblioteca entrega
opção, produto escolhe uma.

**Não adicione ícone ao Badge**, nem que o Block usado como base tenha. Há teste
afirmando que nenhum `<svg>` aparece num badge sem `removable`.

### O ponto de status existe, mas não é prop

Adicionado em 17/set/2026 (decisão de 15/set, `cenario-produto-fechamento.md`):
`.bdg-ok`, `.bdg-atencao` e `.bdg-risco` ganham `::before` de 5px,
`background:currentColor` — CSS puro, sem prop nova. Continua valendo o
`handoff/componentes.md` estar errado (listava "ponto" sem classe nenhuma no
CSS até esta data) e o `.ponto` da fila (2,5px, `--decor`), `.selo-inf .pt` e
`.filtro i.pt` serem coisas diferentes, sem relação com este.

Não virou prop porque não varia por composição — é decorrência do tom
(status = ponto, os outros três tons não), não uma escolha de quem consome.
Se um dia `bdg-marca`/`bdg-calado` precisarem do ponto também, aí sim vira
decisão de prop — não antes, sem uso real pedindo.

### `.u-fim` não é do badge

`margin-left:auto` é utilitário de posição e ficou em `10-v1-entrada.css`. O
comentário original já dizia: "empurrar pro fim da linha é layout, não é o badge".

## Pendências deste componente

- **Quase todo o espaço de variantes não é usado no protótipo.** Só `bdg-ok`,
  `bdg-calado` e `bdg-atencao` aparecem. `solid`, `outline`, `ghost`, `sm`,
  `lg`, `rounded`, `clickable`, `primary`, `danger`, `.cnt` e `.fecha` são oferta do sistema
  sem consumo comprovado. Aceitável num design system, mas "visualmente igual ao
  protótipo" só pode ser afirmado para os três tons usados.
- Sem estado de carregamento (badge que espera um cálculo).
