Files

7.3 KiB

Diretrizes de tema: claro e escuro

Regras para manter os dois temas consistentes. Não tratam da identidade visual (degradê da marca, arredondamentos, hierarquia): essas decisões já estão tomadas e não mudam.


1. Só existem os tons declarados

A escala do Tailwind anda de 100 em 100: 50, 100, 200, 300 … 900, 950. Os meios-tons (150, 250, 350, 450, 550, 650, 750, 850) não existem por padrão. Uma classe como text-slate-350 não gera CSS nenhum, e o elemento herda silenciosamente a cor do pai. Era a causa da maior parte das inconsistências: o mesmo texto ficava certo num tema e lavado no outro.

Os oito meios-tons estão declarados em app/globals.css, cada um no ponto médio exato entre os dois degraus oficiais vizinhos:

tom hex entre
slate-150 #eaeef4 100 e 200
slate-250 #d6dee8 200 e 300
slate-350 #b0bccc 300 e 400
slate-450 #7c8ca2 400 e 500
slate-550 #56647a 500 e 600
slate-650 #3d4b5f 600 e 700
slate-750 #283548 700 e 800
slate-850 #162032 800 e 900

Precisa de um tom novo? Declare no @theme antes de usar. Nunca escreva uma classe com um valor que não está na escala.


2. A regra da simetria

No tema escuro o fundo é escuro, então o texto precisa clarear.

  • Texto: o número do dark: é sempre menor que o do claro. text-slate-800 dark:text-slate-200 ✅ · text-slate-400 dark:text-slate-500 ❌
  • Fundo e borda: o número do dark: é sempre maior. bg-white dark:bg-slate-900 ✅ · border-slate-200 dark:border-slate-800 ✅

Um par invertido não quebra o build e não aparece em revisão de código: só enfraquece o contraste justamente onde ele deveria aumentar. Havia 71 deles.


3. Pares canônicos por papel

Use estes. Os números são a razão de contraste no tema claro (sobre branco) e no escuro (sobre slate-900). O mínimo para texto é 4.5:1; para ícone ou elemento gráfico, 3:1.

Papel Par Claro Escuro
Título / texto principal text-slate-850 dark:text-slate-100 16.3 16.3
Texto principal alternativo text-slate-800 dark:text-slate-200 14.6 14.5
Texto secundário text-slate-700 dark:text-slate-300 10.4 12.0
Corpo de leitura text-slate-650 dark:text-slate-350 8.9 9.3
Rótulo / apoio text-slate-500 dark:text-slate-400 4.8 7.0
Menor ênfase text-slate-500 dark:text-slate-450 4.8 5.2

text-slate-500 dark:text-slate-400 é o par mais usado do projeto (100 ocorrências): é o rótulo em caixa alta, a legenda, o texto de apoio.

Não use slate-400 como cor de texto no tema claro: sobre branco dá 2.56:1. Era o erro mais repetido.


4. Exceção: superfícies escuras nos dois temas

Alguns blocos são escuros independente do tema: painéis de CTA com degradê, bloco de código, tooltip do diagrama de pinos. Dentro deles:

  • Não use variante dark:. O fundo não muda, então o texto também não deve.
  • Escreva um valor só: text-slate-200, text-white, text-slate-350.

Exemplo correto, no bloco de código (CodeExample.jsx):

<div className="flex bg-slate-950">                      {/* escuro sempre */}
  <span className="text-slate-500 pr-4 ...">{idx + 1}</span>   {/* sem dark: */}

4.1. Botão nunca inverte preto ↔ branco

O padrão abaixo não é aceitável, apesar de tecnicamente "acompanhar" o tema:

/* ERRADO: preto no tema claro, branco no escuro */
className="bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-950"

Ele deixa a interface sem cor e faz o mesmo botão trocar completamente de aparência entre os temas. Botão carrega a própria cor, igual nos dois temas:

Papel Classe Contraste com texto branco
Ação principal bg-gradient-to-r from-purple-600 to-indigo-600 text-white 5.4 a 6.3
Ação secundária bg-blue-600 hover:bg-blue-700 text-white 5.2
Ação terciária bg-white dark:bg-slate-900 + borda + texto neutro ·

Como a cor é a mesma nos dois temas, o texto branco também é, sem variante dark:. Isso vale inclusive dentro de painel escuro: o botão de gradiente funciona sobre fundo claro e escuro.


4.2. Painel de destaque acompanha o tema

Painéis de CTA não podem ficar escuros no tema claro. O padrão é um degradê suave da marca no claro e o degradê escuro no escuro:

className="bg-gradient-to-br from-indigo-50 to-purple-50
           dark:from-slate-950 dark:to-purple-950/20
           border border-indigo-200 dark:border-slate-800/80"

Com o conteúdo pareado: título text-slate-900 dark:text-white, corpo text-slate-600 dark:text-slate-400, etiqueta bg-indigo-500/10 text-indigo-700 dark:bg-white/10 dark:text-slate-350.


4.3. Separação de superfície no tema claro

Entre near-whites o contraste é sempre baixo. Quem separa o card do fundo é a borda, não o fundo. Por isso:

Camada Valor Contraste
Fundo da página from-slate-100 via-slate-50 to-purple-100/40 ·
Card bg-white (sólido, sem /70 ou /80) 1.10 vs fundo
Borda do card border-slate-300 1.48 vs card
Divisória interna border-slate-200 mais sutil

border-slate-200/60 sobre card branco dava 1.14, praticamente imperceptível. Não use borda com opacidade para delimitar card.


5. Cor de série vem do dado, não da classe

Cada série tem sua cor em public/series.json (campo cor). Ela entra por style, nunca por classe Tailwind:

<span style={{ backgroundColor: `${serie.cor}18`, color: serie.cor }} />

Esses valores são iguais nos dois temas. Ao usá-los como fundo, mantenha o alfa baixo (de 18 a 30 em hex) para que funcione sobre superfície clara e escura. Como texto, só sobre superfície neutra, nunca sobre outra cor de série.

Nunca escreva cor fixa em style inline

style inline vence qualquer classe, inclusive as variantes dark: e hover:, que deixam de existir para aquele elemento. Só cor vinda do dado pode ir no inline.

/* ERRADO: anula border-slate-300 e dark:border-slate-800/80 */
<div className="border border-slate-300 dark:border-slate-800/80"
     style={{ borderColor: 'rgba(148, 163, 184, 0.1)' }} />

/* ERRADO: 'transparent' anula também o hover:border-purple-400/50 */
style={{ borderColor: isSelected ? serie.cor : 'transparent' }}

/* CERTO: sem valor, a classe assume o estado de repouso */
style={{ borderColor: isSelected ? serie.cor : undefined }}

Esse bug é traiçoeiro porque some depois do primeiro hover: o onMouseLeave faz style.borderColor = '', que apaga o inline e devolve o controle à classe. A borda passa a aparecer e nunca mais some, dando a impressão de que só falha "antes de passar o mouse".


6. Auditar antes de commitar

npm run auditar-temas

Aponta quatro coisas: tom que não existe na escala, par invertido, par abaixo de AA e cor fixa em style inline. Saída esperada: tudo zero.

Sai com código 1 se achar problema, então serve direto em hook de pre-commit ou em CI:

npm run auditar-temas && npm run build