# Contrato · Régua

> **Modelo de como escrever contrato de componente.** Anatomia, props, estados e
> slots, num formato que humano e IA implementam igual, em qualquer framework.
> Contrato é spec, não implementação: por isso ele foi escrito **antes** de o time
> fechar o framework, e continuou valendo intacto quando fechou em Vue. Contrato que
> só serve depois da escolha não é contrato, é comentário de código.

> **Implementado em `app/src/componentes/Ruler/Ruler.tsx`** (26/ago/2026; era
> `Regua` até 23/set/2026, props traduzidas pela regra de Idioma — ver
> `CHANGELOG.md` v0.55.0), com CSS
> em `estilo/12-v3-terreno.css` (`.regua-g`/`.rotcorte`/`.marco`/`.escala`) e
> `estilo/11-v2-fila.css` (`.trilha`/`.corte`/`.band`) — as mesmas folhas que o
> protótipo carrega. Escrito antes de `SourcedNumber`, ficou sem implementação por mais
> tempo que qualquer um dos 7 da Fase 1 — achado revisando prints reais do produto
> pedidos pelo usuário, não por auditoria do backlog. **Não é um dos 7 da Fase 1**:
> `ds/gera.py` (`FASE_1`) e `Layout.tsx` (`COMPONENTES`) travam esses dois em
> exatamente sete nomes por teste, e Régua não é um deles — vive só no Storybook.
>
> **Duas variantes, uma prop (`size`)**: "grande" (`.regua-g`, a que este
> documento descreve por inteiro) e "compacta" (`.regua`, dentro de cada linha
> da fila, `pintaFila()` em `prototipo/index.html` — só `.vals` com o valor em
> negrito, sem rótulo por marcador). A compacta ficou registrada como
> pendência na entrega original e ganhou implementação em 27/ago/2026 (Fase 3
> da reconstrução do protótipo, `/fatia/fila`) — ver "Variante compacta"
> abaixo.

A régua é a assinatura do produto e **não tem equivalente em nenhuma biblioteca
genérica**, porque ela carrega regra de negócio, não só aparência: mostra posição
contra um corte, não um valor solto.

## Anatomia

| Parte | Papel | Obrigatória |
|---|---|---|
| `trilha` | o intervalo em que a coisa existe | sim |
| `cutoff` | a linha do usuário. Traço fino com ponto no topo | não |
| `marker` | onde este item cai. Losango | não |
| `cutoffLabel`, `markerLabel` | nome de cada marcador. Sem ele ninguém sabe o que é o losango | sim, se houver marcador |
| `escala` | só as pontas do intervalo | sim |
| `note` | ressalva de confiança, quando houver | não |

## Props

| Prop | Tipo | Default | Observação |
|---|---|---|---|
| `min`, `max` | number | — | as pontas. **Precisam de origem declarada** (ver regra abaixo) |
| `cutoff` | number \| null | null | ausente esconde o traço, não desenha em zero |
| `marker` | number \| null | null | ausente esconde o losango |
| `cutoffLabel`, `markerLabel` | string | — | obrigatórios quando o marcador existe |
| `band` | `{from, to}` \| null | null | **só com origem.** Ver o caso da faixa preta |
| `status` | `ok` \| `below` \| `no-data` | `ok` | tinge o marco |
| `size` | `md` ("grande") \| `sm` ("compacta") | `md` | `sm` esconde o rótulo por marcador e troca `escala` por `vals` — ver "Variante compacta" |

## Estados

1. **Com corte e com marco.** Os dois rótulos aparecem e não podem colidir: quando
   as caixas se sobrepõem, um vai para a esquerda e o outro para a direita.
2. **Só corte.** É a régua do onboarding. Escala de input, sem nada plotado.
3. **Sem dado.** Trilha vazia com a razão escrita, nunca trilha cinza silenciosa.
4. **Fora do intervalo.** Valor acima de `max` fixa no fim e a nota diz que estourou.

## A regra que este componente carrega

**Nada é plotado na trilha sem origem declarada.** É a regra `nenhum-numero-orfao`
aplicada aqui, e ela nasceu de um defeito real neste componente: a régua tinha uma
faixa preta chapada em 214 a 268 mi, escrita na mão numa versão antiga, sem rótulo,
que não batia mais com o VGV que o produto calculava. Foi pega por um convidado numa
reunião, não pelo time.

Consequência dura: `band` sem fonte não renderiza. E se a trilha é só escala de
input, então **nada** é plotado nela, senão ela deixou de ser controle e virou
gráfico.

## Variante compacta

Uma linha da fila não tem espaço nem motivo pra rótulo por marcador: o
número já está na coluna ao lado (`.ident .end`), e a régua ali existe só
pra mostrar POSIÇÃO, não pra explicar de novo o que já foi dito. Por isso a
compacta:

- raiz `.regua`, não `.regua-g`;
- não renderiza `.rotcorte` nenhum — `cutoffLabel`/`markerLabel` viram sem
  efeito (não quebram em dev se ausentes, diferente da grande);
- troca a `.escala` (min/max) por `.vals`: só o valor do marco, em negrito,
  sem legenda;
- mantém `trilha`/`cutoff`/`marker`/`band`/`note` idênticos à grande — mesma
  matemática de posição, mesma regra de origem, mesma checagem de
  `status` contra `marker`/`cutoff`, mesma exigência de `note` quando estoura
  o teto. Compacta não é "menos rigorosa", é só menos rotulada.

`status="no-data"` já servia às duas variantes desde o início (sempre
renderizou `.regua`, nunca `.regua-g`) — a linha "morta" da fila
(`.morta-tx`) não precisou de nenhuma mudança.

## Acessibilidade

`role="img"` com `aria-label` dizendo a posição em palavras ("R$ 227 mi, acima do
seu corte de R$ 200 mi"). O losango não é alvo de toque: quem edita é o campo.
Contraste do marcador contra a trilha ≥ 3:1, que é a régua de não-texto.

## Fora do contrato

Arrastar o marcador. Hoje o valor é digitado num campo e a régua só reflete. Se um
dia arrastar, isso muda de leitura para controle e o contrato precisa de estado de
foco, de teclado e de passo.

## Achado construindo, não estava no documento original

- **`status="no-data"` precisa ser um SEGUNDO FORMATO DE PROPS, não uma
  variante do mesmo formato.** A primeira versão do componente pedia
  `min`/`max`/`origin` mesmo no estado 3 — mas sem posição pra plotar esses
  campos não fazem sentido nenhum, e prop que não faz nada é pior que prop
  faltando (mesma regra que `contratos/badge.md` já registra pro `ponto` do
  Badge). Corrigido com união discriminada em `status`, mesma técnica de
  `Botao` (`icone`/`rotulo`) e `Badge` (`clicavel`/`removivel`): duas formas
  de props, `PropsSemDado` e `PropsComEscala`, TypeScript cobra os campos
  certos em cada uma.
- **`band` exige a PRÓPRIA origem**, separada da origem da escala
  (`min`/`max`/`cutoff`/`marker`) — é exatamente essa separação que faltava no
  caso da faixa preta original: uma faixa pode vir de uma fonte diferente do
  resto da régua (por exemplo, "referência de mercado" sobre uma escala
  "derivada" do VGV do cenário).
- **A checagem de origem (`validaOrigem`) tinha um buraco real**: se
  `origin` estivesse completamente AUSENTE (não só incompleta), nenhuma das
  checagens por valor batia, e a validação passava calada — achado
  escrevendo o teste que reproduz o caso da faixa preta com
  `band={{from,to}}` sem `origin` nenhuma. Corrigido em `@/lib/origin`
  (compartilhado com `SourcedNumber`): agora checa primeiro se `origin` é uma das
  quatro chaves válidas, antes de checar o campo companheiro.
- **`status` (`ok`/`below`) é checado contra os números em desenvolvimento.**
  O documento original descreve `status` como prop independente, mas um
  `status="below"` que não bate com `marker < cutoff` é a MESMA categoria de
  falha silenciosa que a faixa preta — um sinal visual desconectado dos
  números reais. `Ruler` quebra em dev se os dois discordarem.
- **Colisão de rótulos portada do protótipo por medição real (`arrumaRotulos()`
  em `prototipo/index.html`), não por limiar fixo de porcentagem.** Dois
  rótulos de tamanhos diferentes colidem em pontos de percentual diferentes
  — só `getBoundingClientRect()` nos dois elementos resolve certo. Verificado
  na vitrine: a story com corte a 47,6% e marco a 54% aplica `rot-esq`/
  `rot-dir` sozinha; a story "fora do intervalo" (marco fixado em 100%, longe
  do corte) não aplica nenhuma das duas.
- **Rótulos vazando pro DOM como atributo HTML inválido.** `cutoffLabel`,
  `markerLabel`, `min`, `max`, `cutoff`, `marker`, `prefix`, `suffix` e
  `decimals` foram lidos do objeto de props pra uso interno, mas não tinham
  sido excluídos do resto espalhado no `<div>` raiz — o React avisava no
  console, e o `test:vitrine` (axe em Chromium real) pegou de verdade,
  rodando a própria story. Mesma categoria de defeito que `Badge.spec.tsx`
  já trava pra `rotuloRemover`/`onRemover`.

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

Terceiro e quarto achados de `--t4` nesta sessão, e um de `--t3` — mesmo
defeito, quinta e sexta ocorrência: `test:vitrine` (axe em Chromium real)
reprovou `.rotcorte small` e `.escala` (`--t4`, 4,19:1) e `.morta-tx` (`--t3`,
4,31:1), todos abaixo do 4,5:1 exigido. Corrigidos pra `--t2` (7,95:1) em
`estilo/12-v3-terreno.css` e `estilo/11-v2-fila.css`.

**Sétima ocorrência, e desta vez corrigida na fonte, não no seletor.** A
Fila (`/fatia/fila`, 31/ago/2026) reproduziu o mesmo defeito de `--t4` em
dois lugares novos (`.pos.num`, `.mapa-pe`) — medido ao vivo no navegador
(`getComputedStyle` + composição real do `--shell` translúcido sobre
`--mundo`, não a string do CSS): 4,19–4,41:1, mesma faixa das seis vezes
anteriores. As seis primeiras foram remendadas trocando o seletor pra
`--t2`; o token `--t4` em si nunca mudou, então todo uso novo continuava
nascendo quebrado. Desta vez o token foi corrigido em
`estilo/00-tokens.css` (`#73737F` → `#666670`, 4,97–5,11:1 contra o pior
caso real medido), então não deveria haver oitava ocorrência por este
motivo específico. Os remendos antigos em `--t2` não foram revertidos
(contraste sobrando não é defeito).

## Pendências deste componente

- **`.band-risco` e `.band-sob` não são expostos via prop.** O CSS já tem as
  duas variantes da faixa, mas o contrato só descreve `status` tingindo o
  MARCO, não a faixa — se surgir necessidade real, é decisão de design nova.
- ~~Sem página de documentação própria, sem entrada na barra lateral.~~
  **Resolvido.** Página em `/componentes/regua` (`ReguaDoc.tsx`,
  27/ago/2026), com entrada em "Componentes adicionais". Frase
  desatualizada, achada na auditoria de 21/set/2026.
