0019 — Contraste dos neutros e adoção da paleta de marca¶
| Status | CONCLUÍDA |
| Branch | spec/0019-contraste-e-adocao-da-paleta |
| Criada em | 2026-08-05 |
| Apps afetados | web · e2e (api: sem impacto) |
Status:
RASCUNHO→APROVADA→EM IMPLEMENTAÇÃO→CONCLUÍDA
1. Problema¶
A interface é ao mesmo tempo difícil de ler e apagada. São duas queixas com uma raiz comum: a paleta foi pensada com cuidado e o app usa uma fração dela.
1.1 Os separadores não se veem¶
divider e borderField medem 1,2:1 contra o painel, nos dois temas. A WCAG 1.4.11 (Non-text Contrast) pede 3:1 para o que delimita componente. A 1,2:1 a linha existe no CSS e não na percepção: não se distingue onde um painel termina e outro começa, nem uma linha de tabela da seguinte.
1.2 O texto cinza cansa em jornada inteira¶
text.secondary mede 5,47:1 no claro e 5,80:1 no escuro: passa na WCAG AA, cujo mínimo é 4,5:1. Mas AA é critério de acessibilidade, não de conforto para quem passa oito horas no sistema — e o dado que decide é o tamanho. Os 199 usos de text.secondary estão todos em caption (12px, 74×) e body2 (13px, 72×): não é texto de apoio ocasional, é o corpo do conteúdo, em corpo pequeno. A tolerância da WCAG a contraste menor pressupõe texto grande; aqui vale o contrário, e a régua de leitura prolongada é a AAA, 7:1.
Que a paleta já sabe disso é o mais revelador: neutralInk mede 7,19:1 — o token de texto atenuado já está na régua AAA. Foi o text.secondary, o que o app de fato usa, que ficou para trás.
Além dele, falha o texto que o app atenua por fora da paleta:
| Onde | Como está pintado | Claro |
|---|---|---|
DataRow sem valor | text.disabled × opacity .6 | 1,73:1 |
| Placeholder de campo | default do MUI (42%) | 2,62:1 |
| Rótulos de seção do sider | text.disabled | 2,68:1 |
| Título de painel / seção | secondary.main × opacity .85 | 3,24:1 |
1.3 Painel e fundo quase não se distinguem¶
paper contra background.default mede 1,07:1 no claro. Sem diferença de superfície, toda a estrutura da tela depende justamente das linhas de 1,2:1.
O caminho para separar as duas não é escurecer o fundo, que era a saída óbvia: fundo mais escuro derruba o contraste de todo texto que assenta nele. O caminho é clarear o painel para branco puro — sobe o contraste de tudo que está dentro dos painéis, que é onde vive a maior parte do conteúdo, e separa as superfícies escurecendo o fundo bem menos.
1.4 A marca tem cinco matizes e a paleta adotou três¶
O logo é um swoosh de três braços mais a assinatura, e cada peça é um gradiente:
| Cor | Onde vive no logo | Na paleta |
|---|---|---|
#005577 navy | fim do gradiente da assinatura | primary ✓ |
#008B8B teal | braço do meio do swoosh | secondary ✓ |
#FFA94D laranja | braço de cima do swoosh | highlight ✓ |
#00ACF0 ciano | início da assinatura e do braço azul | ausente |
#FFCB53 ouro | início do braço laranja | ausente |
O ciano não é acessório: a palavra "GARANTORA" inteira é o gradiente #00ACF0 → #005577 — metade da assinatura é ciano. No app ele aparece uma vez, cravado em minúsculas com opacity: .8, numa linha do login.
1.5 A tela é majoritariamente cinza¶
386 ocorrências de neutro contra 254 de cor somadas, e a cor que existe está mal distribuída:
primary 109 · secondary 76 · error 42 · success 11 · warning 10 · highlight 5 · info 1
O laranja — um dos três braços do swoosh, o elemento mais quente e distintivo da marca — tem cinco usos no sistema inteiro. A paleta o define como "a única CTA mais desejada da tela", então alguma escassez é proposital; cinco é escassez além da intenção.
E info é cor duplicada: info.main é #005577, idêntico ao primary. Um slot de paleta que não carrega informação nenhuma — e é exatamente onde o ciano da marca cabe.
1.6 As cores semânticas estão pela metade¶
A paleta declara quatro semânticas — success, warning, error, info — mas só metade do aparato que elas precisam para funcionar. O que existe e o que falta:
success | warning | error | info | |
|---|---|---|---|---|
| cor viva | ✓ | ✓ | ✓ | ✓ (= primary) |
tinta de texto (*Ink) | ✓ 5,13:1 | ausente | ✓ 4,98:1 | ausente |
texto sobre preenchimento (on*) | ✓ | ausente | ✓ | ausente |
| fundo de alerta | ✓ | ✓ | ✓ | ✓ |
| fundo de chip | ✓ | ✓ | ✓ | ausente |
Duas consequências medidas:
warningnão tem tinta e é usado como texto.#F59E0Bmede 2,15:1 sobre o painel — eSummaryCards.tsx:24pinta a taxa de inadimplência comwarning.mainquando ela passa de 5%. O número que avisa que algo vai mal é o mais difícil de ler da tela. A própria paleta reconhece o problema no comentário das tintas ("âmbar 1,70") e mesmo assim nunca criou owarningInk.infoé a severidade padrão do sistema de notificação global. Oproviders/notification.tsxfazSEVERITY[params.type] ?? "info"— toda notificação sem tipo conhecido cai eminfo, que hoje é o navy doprimary, sem tinta e sem chip.
O alcance não é pequeno: <Alert> aparece em 42 arquivos, Dialog em 14, Tooltip em 19, Snackbar em 3.
Há ainda um efeito de ordem: as razões anotadas na paleta (successInk "6,36:1 no chip", dangerInk "5,17:1 no chip") foram medidas sobre o paper atual. Clarear o painel para branco (RN5) muda todos esses fundos — os números melhoram, mas ficam desatualizados no comentário, que é a única coisa que impede o token de derivar de novo.
1.7 A raiz comum¶
Em 1.2 e 1.5 o padrão é o mesmo, e é o da spec 0018: o design system declara a coisa certa e o app passa por fora. accentInk existe (5,92:1) e os títulos usam secondary.main a 85% (3,24:1). neutralInk existe (7,19:1) e o texto atenuado usa text.disabled, que a paleta nunca declarou e por isso cai no default do MUI.
Com um agravante: o trabalho de acessibilidade foi feito — os comentários de successInk, dangerInk e highlightInk trazem as razões medidas uma a uma — e parou nas tintas coloridas, sem alcançar divider, text.disabled, os multiplicadores de opacity nem os dois matizes que a marca tem e a paleta não.
1.8 Por que as duas metades andam juntas¶
Todas as cores vivas da marca são de preenchimento, não de texto: sobre painel branco o teal mede 4,15:1, o ciano 2,57:1, o laranja 1,90:1, o ouro 1,51:1. Nenhuma passa.
Isso é o que impede a resposta ingênua: "usar mais cor" sem a tinta correspondente é piorar o contraste. Adotar marca e consertar legibilidade não são duas tarefas disputando o mesmo arquivo — é uma tarefa só, e foi por isso que esta spec fundiu as duas.
2. Objetivo¶
Que a estrutura da tela se veja e que a marca apareça. Separador que delimita cumpre 3:1, texto de conteúdo cumpre 7:1, painel se distingue do fundo — e as cinco cores do logo existem na paleta, cada uma com sua tinta de texto e uma regra de emprego que dispense gosto pessoal a cada tela.
3. Fora de escopo¶
- Redesenhar o logo ou recolorir a marca. Os
.svgoficiais não se tocam — oLogo.prompt.mdé explícito. Esta spec lê as cores do logo, não as altera. - Mudar o matiz das cores que já estão na paleta.
primary,secondary,highlighte as semânticas continuam onde estão. A spec acrescenta o que falta e corrige neutros, superfícies e o uso das tintas. - Recolorir componentes em massa. A adoção segue a regra da RN11; onde não houver regra, fica neutro. Colorir por decoração é o defeito oposto ao relatado, não a cura.
- Modo de alto contraste ou
prefers-contrast. Primeiro o padrão precisa estar certo. - Contraste de gráfico e mapa de calor. O Centro de Operações tem visualizações com regras próprias; ficam para uma spec de dataviz.
text.disabledde controle genuinamente desabilitado. A WCAG isenta componente inativo (1.4.3), e não é o que está errado — o defeito é usar essa cor como "texto terciário". Ver RN4.- Auditoria de contraste em todas as telas. Esta spec corrige os tokens e o uso indevido que a medição encontrou; não promete varrer cada tela.
4. Regras de negócio¶
Legibilidade¶
- RN1 — Separador que delimita componente cumpre 3:1 contra o fundo em que assenta, medido no pior dos dois lados. Vale para borda de painel, de campo, de diálogo, cabeçalho de tabela e qualquer filete que diga "aqui termina uma coisa e começa outra".
Revisada pelo fix 0004. Esta regra era larga demais: a WCAG 1.4.11 pede 3:1 para o que identifica componente, e só a borda de campo identifica. Painel, diálogo e cabeçalho de tabela decoram, e passaram para 2:1. Ver
fix/0004-separadores-escuros-demais/.
-
RN2 — Separador interno de lista cumpre 1,5:1 (era 2:1 nesta spec; ajustado pelo fix 0004, porque a 2:1 vinte linhas ainda formavam grade), não 3:1. Linha entre itens de uma mesma lista é ritmo de leitura, não fronteira de componente: a 3:1 uma lista de 20 linhas vira uma grade de traços escuros, e a legibilidade que se ganha na linha se perde no conjunto. A distinção entre RN1 e RN2 é a diferença entre cumprir a norma e obedecê-la ao pé da letra onde ela não se aplica.
-
RN3 — Texto de conteúdo cumpre 7:1 (AAA) sobre o painel. Vale para
text.primaryetext.secondary, que juntos pintam praticamente todo o conteúdo em 12–13px. O alvo é AAA e não AA porque o critério aqui é jornada de trabalho, não conformidade mínima. -
RN3a — Todo texto cumpre ao menos 4,5:1 em qualquer superfície do app (painel, fundo, cabeçalho de painel, zebra, chip, alerta). Nenhum texto recebe
opacitypara parecer secundário — atenuação é escolha de cor, não de transparência, porqueopacityreduz o contraste sem que a paleta perceba. -
RN3b —
text.secondaryeneutralInkpassam a ser o mesmo valor. Significam a mesma coisa — "texto atenuado, mas para ler" — e manter dois tokens para um conceito foi o que deixou um deles (o que o app usa) 1,7 ponto atrás do outro. -
RN4 —
text.disabledvolta a significar desabilitado. Passa a ser declarado na paleta e só é usado em controle inativo, onde a WCAG isenta. Texto atenuado que o usuário precisa ler usaneutralInk. -
RN5 — Painel se distingue do fundo em pelo menos 1,12:1, e a diferença vem de clarear o painel, não de escurecer o fundo — para não pagar a separação com o contraste do texto que assenta no fundo.
-
RN6 — Todo token novo ou alterado entra com a razão medida anotada ao lado, como já fazem
successInke companhia. Token de contraste sem o número documentado é token que volta a derivar. -
RN7 — As regras valem nos dois temas. Claro e escuro têm valores diferentes e os mesmos alvos. Nenhum dos dois é "o principal".
Marca¶
-
RN8 — O ciano entra ocupando o slot de
info. Hojeinfo.mainé cópia literal doprimarye tem um uso no app: o slot está livre na prática.infopassa a significar dado e informação neutra — a família azul-claro da assinatura. -
RN9 — O ouro entra como o degrau claro do laranja (
highlight.light), que é exatamente o papel dele no logo: o stop inicial do gradiente do braço laranja. Serve a fundo de chip e alerta de pendência, distinto do âmbarwarninggenérico do MUI. -
RN10 — Cor viva de marca nunca entra sozinha. Toda cor nova vem acompanhada da sua tinta de texto, calibrada no pior fundo em que vai aparecer — o próprio chip —, como já fazem
successInk,dangerInkehighlightInk. Cor sem tinta é convite a repetir o defeito que esta spec conserta. -
RN11 — A hierarquia de emprego é a do swoosh, e fica documentada:
| Cor | Emprego |
|---|---|
| laranja / ouro | ação mais desejada da tela, e pendência que espera o usuário |
| teal | marca e navegação — onde o produto se identifica |
| navy | estrutura: cabeçalho, rótulo, moldura |
| ciano | dado e informação neutra |
| semânticas | sucesso, erro, alerta — nunca decorativas |
Sem isso, "usar mais cor" vira decisão de gosto a cada tela, que é como se chegou aos cinco usos do laranja.
- RN12 — Cor substitui neutro só onde carrega informação — ícone de módulo, estado de linha, cabeçalho que identifica seção. Onde a cor não distingue nada, o neutro fica. Colorir por decoração cansa tanto quanto o cinza apagado.
Semântica¶
-
RN13 — Toda cor semântica tem o conjunto completo: cor viva, tinta de texto (
*Ink), texto sobre preenchimento sólido (on*), fundo de alerta e fundo de chip. Quatro semânticas × cinco peças, sem lacuna. Hoje faltamwarningInk,infoInk,onWarning,onInfoechip.info— e é exatamente nas lacunas que o app improvisou. -
RN14 — Tinta semântica é calibrada no pior fundo em que aparece, que é o chip a 14%, não o painel. Calibrar no painel deixa a tinta legível solta e apertada dentro do próprio chip — o fundo tingido come parte da diferença. É o critério que
successInkedangerInkjá seguem, agora escrito. -
RN15 —
on*é escolhido por medição, não por hábito. Branco sobre âmbar mede 2,15:1 e sobre ciano 2,57:1: nesses dois o texto tem de ser escuro (#0F172A, 8,31:1 e 6,94:1). É a mesma lição que o tema escuro já aprendeu noonDanger, com o comentário "o matiz clareia no escuro, então o que fica em cima precisa escurecer". -
RN16 — Ao mudar
paper, as razões anotadas são reaferidas. Clarear o painel (RN5) altera todos os fundos compostos por transparência — chip, alerta, zebra, cabeçalho. Os números nos comentários da paleta são a defesa contra a deriva; comentário desatualizado é pior que comentário ausente, porque parece verificado.
Valores propostos¶
Derivados por busca binária sobre a escala slate (neutros) e sobre as cores do logo (marca), verificados contra cada superfície onde aparecem.
Claro
| Token | Hoje | Proposto | Medido |
|---|---|---|---|
background.paper (RN5) | #FBFCFE | #FFFFFF | 1,13:1 vs fundo |
background.default (RN5) | #F1F5F9 1,07:1 | #EDF1F6 | — |
text.secondary (RN3) | #5F6878 5,47:1 | #465264 | 7,92:1 painel · 6,98:1 fundo |
neutralInk (RN3b) | #4E5666 7,19:1 | #465264 | idem — mesmo token |
divider (estrutural, RN1) | #E2E8F0 1,20:1 | #7B8A9E | 3,52:1 painel · 3,10:1 fundo |
dividerSubtle (lista, RN2) | — | #A9B6C8 | 2,06:1 painel |
borderField | #E3E8ED 1,20:1 | #7B8A9E | 3,52:1 |
text.disabled (RN4) | não declarado | #67768B | 4,51:1 |
info.main (RN8) | #005577 = primary | #00ACF0 | preenchimento (2,57:1) |
infoInk (RN10) | — | #0071B5 | 4,53:1 no chip · 5,21:1 no painel |
highlight.light (RN9) | — | #FFCB53 | preenchimento (1,51:1) |
highlightLightInk (RN10) | — | #9C6800 | 4,51:1 no chip · 4,78:1 no painel |
Escuro
| Token | Hoje | Proposto | Medido |
|---|---|---|---|
text.secondary (RN3) | #9AA4B2 5,80:1 | #ACB4C0 | 7,00:1 painel · 8,95:1 fundo |
neutralInk (RN3b) | #9AA4B2 | #ACB4C0 | idem — mesmo token |
divider (estrutural) | #2A3648 1,20:1 | #64748B | 3,07:1 painel · 3,93:1 fundo |
dividerSubtle | — | #475569 | 1,93:1 painel |
borderField | #2A3648 | #64748B | 3,07:1 |
background.default | #0F172A 1,22:1 | #0B1220 | 1,28:1 vs painel |
text.disabled | não declarado | #8290A4 | 4,51:1 |
info.main (RN8) | #4DA6CC = primary | #00ACF0 | 5,68:1 — passa como texto |
highlight.light (RN9) | — | #FFCB53 | 9,70:1 — passa como texto |
Semânticas — o que falta (RN13), medido no claro
| Token | Situação | Proposto | Medido |
|---|---|---|---|
warningInk | ausente — âmbar como texto é 2,15:1 | #AE5700 | 4,53:1 no chip · 5,04:1 no painel |
infoInk | ausente | #0071B5 | 4,53:1 no chip · 5,21:1 no painel |
onWarning | ausente | #0F172A | 8,31:1 sobre o âmbar |
onInfo | ausente | #0F172A | 6,94:1 sobre o ciano |
chip.info | ausente | rgba(0,172,240,.14) | fundo #DBF3FD |
successInk | existe, reaferir (RN16) | #2E7D32 mantido | 5,42:1 no painel branco |
dangerInk | existe, reaferir (RN16) | #B71C1C mantido | — |
No escuro as quatro semânticas já passam como texto sobre o painel — success 6,33:1, warning 8,30:1, error 5,11:1, info 5,68:1 — e por isso lá cada *Ink continua apontando para a própria cor, como o arquivo já faz e explica.
text.primary não muda: já mede 17,06:1 no claro e 12,16:1 no escuro.
Onde o sistema fica abaixo de 7:1, e por quê. O text.secondary novo mede 6,98:1 sobre o fundo claro e 6,45:1 sobre o cabeçalho de painel no escuro. São superfícies secundárias, o desvio é de centésimos a meio ponto, e forçar 7:1 nelas escureceria o texto a ponto de competir com o text.primary — apagando a hierarquia que o cinza existe para criar. Ambos seguem muito acima dos 4,5:1 da RN3a. Conferidos também: cabeçalho de painel 7,43:1, zebra 7,57:1, chip neutro 6,58:1.
5. Impacto por app¶
apps/api¶
Sem impacto. Cor é tema de apresentação; nenhum modelo, endpoint ou migration.
apps/web¶
Design system (src/design-system/) — o grosso da entrega:
theme/palette.ts— neutros, superfícies,text.disabled,dividerSubtle, a unificaçãotext.secondary/neutralInk, e os dois matizes novos com suas tintas. Cada um com a razão medida anotada (RN6).tokens/colors.cssetokens/dark.css— os mesmos valores, que é a segunda metade da fonte única (oREADME.mddo design system manda replicar).theme/components.ts— ondeMuiDividere as bordas dePaper/Cardescolhem entre estrutural e sutil.prompts/— o.prompt.mdque descrever cor ganha a tabela de emprego da RN11.
Uso indevido a corrigir (RN3a/RN4):
| Arquivo | Hoje |
|---|---|
components/shared/FormPanel.tsx | título com secondary.main + opacity .85 |
components/shared/MobileDetailSection.tsx | idem |
components/shared/DocumentsPanel.tsx | idem, mais ícones em text.disabled |
components/shared/DataRow.tsx | vazio com text.disabled + opacity .6 (1,73:1) |
components/custom-sider/index.tsx | rótulos de seção e badges em text.disabled |
pages/credit-analyses/show.tsx (2×), SelectionBlock, DecisionPanel | título com opacity .85 |
pages/renters/show.tsx, pages/settings/document-types/index.tsx | idem / ícones |
pages/login/index.tsx:148 | #00acf0 cravado com opacity .8 → passa a usar info |
O título de seção em versalete aparece em sete lugares com o mesmo bloco de sx copiado. Extrair para peça compartilhada entra no mesmo passo — é a razão de o defeito ter se espalhado.
Adoção de cor (RN11/RN12) — onde a cor passa a substituir o neutro, sem varredura cega: ícones de módulo no sider, chip de estado que hoje é neutro por falta de cor, e o info recém-criado nos lugares que hoje pintam primary para dizer "dado".
data-testid: nenhum novo. Mudança de cor não altera estrutura.
Sider: sem impacto estrutural — só a cor dos rótulos de seção e dos ícones.
Responsividade & PWA: cor não depende de viewport. Sem impacto no service worker além do rebuild de sempre.
apps/e2e¶
- Spec: cenário que lê as cores computadas de separador, borda de campo, superfície e texto atenuado em telas representativas (lista, formulário, detalhe), calcula a razão WCAG no browser e afere RN1–RN5. Cobre os dois temas — alternando pelo controle do topo — porque metade dos valores só existe no escuro.
- Seeds: nenhum. As telas existentes bastam.
6. Contrato de API¶
Sem alteração. Nenhum endpoint criado, removido ou modificado.
7. Critérios de aceite¶
- [x] CA1. Toda borda de painel, campo, diálogo e cabeçalho de tabela mede ≥ 3:1 contra o fundo adjacente, nos dois temas.
- [x] CA2. Separador interno de lista mede ≥ 2:1 e < 3:1 — visível sem virar grade.
- [x] CA3.
text.primaryetext.secondarymedem ≥ 7:1 sobre o painel, nos dois temas. - [x] CA3a. Nenhum texto da interface mede abaixo de 4,5:1 em superfície nenhuma, incluindo os quatro casos da seção 1.2.
- [x] CA3b.
text.secondaryeneutralInktêm o mesmo valor nopalette.ts. - [x] CA4. Nenhum texto da interface é atenuado por
opacity— a busca poropacityem regra de cor de texto não retorna ocorrência. - [x] CA5.
paperebackground.defaultdiferem ≥ 1,12:1 nos dois temas, e no claro a diferença veio de clarear o painel —background.defaultnão ficou mais escuro que#EDF1F6. - [x] CA6.
text.disabledestá declarado na paleta e só aparece em controle desabilitado. - [x] CA7. As cinco cores do logo existem na paleta, cada uma com tinta de texto que cumpre 4,5:1 no pior fundo em que aparece.
- [x] CA8.
info.maindeixou de ser cópia doprimary, e o#00acf0cravado no login sumiu. - [x] CA9. A tabela de emprego da RN11 está em
docs/design-tokens.md, que hoje só documenta ohighlight. - [x] CA10. Cada token novo ou alterado tem a razão medida anotada no
palette.ts, etokens/colors.css/tokens/dark.csstrazem os mesmos valores. - [x] CA11. As cores de marca existentes não mudaram de matiz — o diff em
palette.tstoca neutros, superfícies, tintas de texto e os dois matizes novos. - [x] CA12. As quatro semânticas têm o conjunto completo da RN13 — cor viva,
*Ink,on*, fundo de alerta e fundo de chip —, sem lacuna nos dois temas. - [x] CA13. Nenhuma cor viva semântica é usada como cor de texto ou de glifo; onde hoje há
warning.maincomo texto (SummaryCards.tsx:24), passa a haverwarningInk. - [x] CA14. Toda tinta semântica mede ≥ 4,5:1 sobre o próprio chip, não só sobre o painel.
- [x] CA15. Toda razão anotada em comentário no
palette.tsfoi reaferida contra as superfícies novas — nenhuma descreve o estado anterior. - [x] CA16.
tsclimpo, sem apontamento novo nos arquivos tocados.
8. Plano de implementação¶
Ordem obrigatória no monorepo: api → web → e2e. Aqui a api não entra.
- [x]
apps/api— sem impacto, nada a fazer - [x]
apps/web—palette.ts: neutros, superfícies,text.disabled,dividerSubtle, unificaçãotext.secondary/neutralInk, com as razões anotadas (RN6) - [x]
apps/web—palette.ts:info= ciano ehighlight.light= ouro, com as tintasinfoInk/highlightLightInk(RN8–RN10) - [x]
apps/web—tokens/colors.cssetokens/dark.csscom os mesmos valores - [x]
apps/web—components.ts: onde cada separador escolhe entre estrutural e sutil - [x]
apps/web— removeropacityde texto e trocartext.disabledporneutralInknos arquivos da tabela (RN3a/RN4) - [x]
apps/web— extrair o título de seção em versalete, hoje copiado em sete lugares - [x]
apps/web—palette.ts: completar as semânticas (RN13) —warningInk,infoInk,onWarning,onInfo,chip.info - [x]
apps/web— trocar cor viva semântica por tinta onde ela é texto ou glifo (CA13) - [x]
apps/web— reaferir e reescrever as razões anotadas na paleta (RN16/CA15) - [x]
apps/web— adoção da RN11/RN12 nos pontos listados, sem varredura cega - [x]
docs/design-tokens.md— tabela de emprego das cinco cores (CA9) - [x]
apps/e2e— spec medindo as razões nos dois temas - [x]
make test(sem impacto esperado — só web) etsc - [x]
/devremote-rebuild+ script de medição de contraste nos dois temas e nos dois viewports, comparando antes e depois
9. Decisões em aberto¶
Nenhuma. As decisões que abriram a spec foram resolvidas antes da escrita:
- Separadores: 3:1 no que delimita, ~2:1 na linha interna de lista.
- Texto: corrigir tudo abaixo de 4,5:1, inclusive o atenuado por
opacity. - Superfícies: reforçar a distinção entre painel e fundo.
- Marca: fundir a adoção das cinco cores nesta spec, em vez de uma 0020 na fila.
- Semântica: completar
success/warning/error/infona mesma passada, já que o buraco está no mesmo arquivo e tem a mesma causa.
Duas correções entraram depois da primeira redação, ambas vindas da validação do usuário:
-
A régua do texto de conteúdo subiu de AA (4,5:1) para AAA (7:1). A primeira versão concluía que "o texto cinza da paleta está correto" porque passava na AA. Passava — e ainda assim cansa, porque os 199 usos estão todos em 12–13px e o critério de quem trabalha o dia inteiro no sistema não é o mínimo de acessibilidade. Junto veio a inversão da estratégia de superfície: clarear o painel em vez de escurecer o fundo.
-
A adoção da marca entrou no escopo. Era para ser a spec 0020, atrás desta. Fundiu porque as duas reescrevem o mesmo
palette.tse porque adotar cor sem tinta pioraria justamente o contraste que a outra metade conserta (seção 1.7).
10. Registro¶
O que mudou de fato¶
| Antes | Depois | |
|---|---|---|
| Borda de painel (claro) | 1,20:1 | 3,33:1 aferido no browser |
| Borda de painel (escuro) | 1,20:1 | 3,53:1 |
| Linha interna de lista | 1,20:1 | 2,06:1 — visível sem virar grade |
| Texto de conteúdo (claro) | 5,47:1 | 7,92:1 |
| Texto de conteúdo (escuro) | 5,80:1 | 7,00:1 |
| Painel × fundo | 1,07:1 | 1,13:1, clareando o painel |
| Taxa de inadimplência em âmbar | 2,15:1 | tinta warningInk, 5,04:1 |
O que divergiu do planejado¶
1. Não eram sete cópias do título de seção — eram oito.
A spec listou sete arquivos com o bloco secondary.main + opacity: .85. Ao varrer, apareceram mais quatro (DocumentsStep 2×, DocumentChecklist, properties/show) e uma oitava que a busca por opacity: 0.85 não pegava: o FinancialSummaryPanel tinha um componente SectionLabel local, com opacity: 0.7 — a mais apagada de todas. Todas foram para components/shared/SectionLabel.tsx.
2. O ouro não ganhou tinta própria.
A tabela de valores previa highlightLightInk: #9C6800. Medindo, o highlightInk que já existe (#8F5400) mede 5,77:1 sobre o chip do ouro — bem acima do piso. Um segundo token diria o que o primeiro já diz, e token duplicado foi exatamente o que deixou neutralInk e text.secondary divergirem (RN3b). O ouro compartilha a tinta do laranja, que é a família dele no logo.
3. text.disabled foi separado por natureza, não convertido em bloco.
A RN4 manda text.disabled significar "desabilitado". Havia ~70 usos, quase nenhum de controle inativo. Separei: os 27 usos em texto (estados vazios, legendas, valores) foram para text.secondary; os ~20 em ícone e marcador ficaram, porque text.disabled agora é declarado a 4,62:1 e cumpre a RN3a — e um ícone de apoio em 7,92:1 pesaria mais que o conteúdo ao lado. A fronteira é "o usuário lê isto?", não o nome do token.
4. O colors.css documentava um logo que não existe.
As "paradas do gradiente do logo" eram #F58220, #00857A, #00A0E3 — nenhuma das três aparece em public/logo.svg. Eram aproximações. Substituídas pelos cinco stops reais.
5. O tema escuro tinha text.secondary divergente entre doc e código.
docs/design-tokens.md dizia rgba(232,234,240,0.70); o código usava #9AA4B2. Resolvido em favor de um valor opaco — cor de texto por transparência é justamente o que faz o contraste escapar do controle da paleta.
Adendo — os chips de status não estavam pálidos: não tinham cor¶
Validando as listas, os chips ("Em análise", "Aprovado", "Em edição") pareciam apagados perto do resto, que havia escurecido. Medindo, o problema era outro e maior: os três renderizavam MuiChip-colorDefault, cinza rgba(0,0,0,.08), idênticos entre si.
A causa é um erro de API que atravessou o app: sx={{ color: cfg.color }} põe a string "success" na propriedade CSS color, não na prop color do Chip. O MUI resolve contra palette.success, que é um objeto — vira CSS inválido e é descartado silenciosamente. Nenhum type check pega, porque sx.color aceita string.
Consequência: a cor de status não comunicava nada desde sempre, e os tokens chip.* e *Ink da paleta — criados exatamente para isso — nunca tiveram um único consumidor.
Corrigido no StatusBadge, que passa a compor fundo tingido + tinta a partir do design system, e nos outros três chips com o mesmo defeito (DecisionPanel, DocumentChecklist, WorkQueueItem), via toneChipSx exportado — um lugar só. Junto foram quatro textos e glifos com color: "error" solto, que caíam no mesmo descarte.
Aferido depois do rebuild, nos dois temas:
| Tom | Fundo (claro) | Tinta sobre ele |
|---|---|---|
info | #DBF3FD | 4,54:1 |
success | #E2EDE2 | 6,53:1 |
default | #E5E7E9 | 6,38:1 |
No escuro, 5,35 · 6,12 · 6,46:1. Antes: um cinza só, para todos.
O e2e mediu o pixel, não o token — e cobrou três coisas¶
O spec calcula as razões a partir do estilo computado no browser, não dos hex da paleta. A diferença apareceu logo: três rodadas até passar, e nenhuma das falhas foi da paleta.
eval()dentro depage.evaluatenão vaza escopo. Os seis cenários falharam de uma vez porque os helpers passados como string declaravamconstdentro de umevalem modo estrito. Trocado poraddInitScript, que pendura tudo emwindow.- O primeiro
.MuiPaper-rootdo documento é a gaveta, não o painel do formulário — o teste media a superfície errada. Passou a usarmain .MuiPaper-root. - Os itens "EM BREVE" da gaveta puxavam o pior caso para 2,82:1. Eles têm
aria-disabledeopacity: .75: componente inativo, que a WCAG 1.4.3 isenta e a spec põe fora de escopo. Excluídos por seletor — o que é diferente de afrouxar o piso.
A terceira rodada expôs uma imprecisão da minha primeira versão do teste: eu exigia AAA de todo texto, incluindo o rótulo de seção em accentInk (6,08:1). A RN3 escopa os 7:1 a text.primary/text.secondary; tinta de marca responde à RN3a (≥4,5:1). O teste passou a separar as duas réguas — via data-ink="brand" no SectionLabel — em vez de baixar a do conteúdo.
Verificação¶
make test: 206 passaram, zero falhas — inclusive a intermitentetest_partner_agency_search, que desta vez caiu do lado bom do desempate. Nenhum arquivo da API foi tocado.tsc: limpo nowebe noe2e.color-contrast.spec.ts: 7/7 em chromium, nos dois temas, contra o dev-remote reconstruído.- Sem rolagem horizontal em nenhuma das três telas medidas.
- Suíte e2e completa: 319 passaram, 6 pulados, zero falhas (16,7 min, chromium + firefox + webkit). Os 6 pulados são os semeadores em massa, já
test.skippor não serem idempotentes contra o banco compartilhado — nada a ver com esta entrega.
Valia rodar: uma mudança de cor que atravessa o app inteiro é justamente o tipo de coisa que quebra um spec que dependia de valor de cor sem dizer. Nenhum dependia.