Conteúdo
Números, datas & unidades
src/lib/format.ts ou src/lib/format-date.ts — nunca de um toLocaleString inline na tela. Os exemplos abaixo são gerados pelas funções reais, agora.Dinheiro
| Função | Entrada | Saída |
|---|---|---|
| formatBRL(1234.5) | 1234.5 | R$ 1.234,50 |
| formatBRL(0) | 0 | R$ 0,00 |
| formatCustoUnitario(0.24) | 0.24 | R$ 0,24 |
| formatCustoUnitario(0.0375) | 0.0375 | R$ 0,0375 |
| formatBRL(18420) | 18420 | R$ 18.420,00 |
NotaPor que 4 casas abaixo de R$ 0,10
> 0 na condição mantém o zero de verdade em duas casas.| Regra | Detalhe |
|---|---|
| Sempre tabular-nums | Valor é a coluna mais comparada da tela. |
| Alinhado à direita em tabela | Números comparáveis alinham pela unidade. |
| Sinal explícito em delta | “+12%”, “−R$ 340” — sem sinal, quem lê não sabe a direção. |
| Nunca abreviar em tela de dinheiro | “R$ 18.420”, não “R$ 18,4k”: é conta de clínica, não gráfico de investidor. |
Números, quantidades e percentuais
| Função | Saída |
|---|---|
| formatIntBR(1284) | 1.284 |
| formatNum(161.74) | 161,74 |
| formatNum(12.5, 2, 2) | 12,50 |
| formatQty(1.5) | 1,5 |
| formatPercent(0.125) | 12,5% |
| formatPercent(0.84) | 84% |
| formatAnos(1) | 1 ano |
| formatAnos(2.5) | 2,5 anos |
formatAnos existe por um detalhe de português: sem ela, a lista da entrevista escrevia “1 anos”. Toda unidade que pode chegar no singular merece a mesma atenção.
Armadilha já pagaIntl.NumberFormat é caro
toLocaleString constrói um formatador NOVO a cada chamada, e uma lista paga isso por linha. Os helpers guardam singletons por combinação de opções — é por isso que eles existem, além da consistência.Telefone
| Contexto | Função | Resultado |
|---|---|---|
| Exibição em lista | formatPhoneBR('+5531982968101') | +55 31 98296-8101 |
| Campo de formulário | formatPhoneInput('+5511912345678') | (11) 91234-5678 |
| Digitando | maskPhoneInput('11912345678') | (11) 91234-5678 |
| Sem telefone | formatPhoneBR(null) | — |
| Regra | Detalhe |
|---|---|
| Telefone é mono | font-mono text-[13px]: lê-se caractere a caractere. |
| Canônico é E.164 | +5511912345678 no banco. A máscara é conveniência da tela; a action normaliza de qualquer jeito. |
| Começou com +, a máscara desliga | Número internacional entra cru e a validação cuida. |
| DDD nunca começa com zero | “011 3123-4567” é discagem com tronco, não número — sem o guard virava +5501…, E.164 que o WhatsApp rejeita. |
| O 9º dígito tem duas formas | O WhatsApp devolve número antigo SEM o 9 enquanto a agenda guarda COM. Dedup e reconhecimento aceitam as duas — ou fabricam duplicata. |
| Ausência é “—” | Travessão, não “null”, não string vazia. |
Datas e horas
| Função | Para que | Nota |
|---|---|---|
| formatBrDate(iso) | Data em pt-BR com timeZone de exibição | Aceita as opções do Intl. |
| formatBrDateTime(iso) | Data + hora | Idem. |
| brDayKey(value) | Chave YYYY-MM-DD pra comparar “mesmo dia?” | Carimbo estragado agrupa numa chave própria em vez de derrubar a lista. |
| labelDoMes('2026-08-01') | “agosto de 2026” | UTC no formatter de propósito: a entrada já é data de CALENDÁRIO, e reinterpretar num fuso local voltaria um dia na virada do mês. |
| shortDuration(ms) | “2 h 10 min”, “45 min”, “menos de 1 min” | Duração curta e humana. |
| sinceShort(iso) | Quanto tempo faz | Usa Date.now() — fora do corpo do componente. |
Armadilha já pagaFuso de exibição ≠ fuso da clínica
format-date.ts crava São Paulo — serve pra MOSTRAR. Onde o dia muda o que a clínica recebe (assinatura, agenda, grade de horários), a chave sai de chaveDoDia(instante, tenants.timezone). Confundir os dois é como uma clínica no Acre perde uma consulta de madrugada.Armadilha já pagaRótulo de régua vem do domínio
fromMinutes(minuto), nunca de Math.floor(minuto / 60): a régua começa no primeiro opens_at da semana, que pode não ser hora cheia. Uma clínica que abre 13:30 via a primeira linha rotulada “13:00” — e o clique no vão pré-preenchia meia hora errada em TODO agendamento feito pelo calendário.| Situação | Formato |
|---|---|
| Data numa lista | 12/08/2026 |
| Data com dia da semana | qui, 12/08 |
| Hora | 14:00 (24h, sempre — “2 PM” não existe em pt-BR) |
| Faixa de horário | 14:00–14:30 (travessão sem espaço) |
| Tempo relativo curto | há 2 min · há 3 h · ontem |
| Mês | agosto de 2026 |
| Hidratação | suppressHydrationWarning quando o texto depende do relógio |
Documentos e endereço
| Função | Saída |
|---|---|
| formatCnpj('12ABC34501DE35') | 12.ABC.345/01DE-35 |
| formatCep('01310930') | 01310-930 |
O CNPJ é alfanumérico desde a IN RFB 2.229/2024 — daí [A-Z0-9] nas 12 primeiras posições e [0-9] só nos dois dígitos verificadores. Máscara que assume \d em tudo já nasce errada.
Regras gerais
Sim
124 agendados · 84% de comparecimento · +55 11 91234-5678
R$ 18.420,00 recuperáveis
Tabular em tudo, telefone em mono, moeda com duas casas, percentual com uma.
Não
124 agendados · 84.0% de comparecimento · +5511912345678
R$ 18420 recuperáveis
Ponto decimal em pt-BR, E.164 cru na tela, moeda sem separador de milhar e sem casas — e nada tabular.
// Nunca na tela:
value.toLocaleString("pt-BR", { style: "currency", currency: "BRL" })
// Sempre:
import { formatBRL } from "@/lib/format";
formatBRL(value)