- Introduced DESIGN.md to outline rules for maintaining consistent light and dark themes, including color scales, symmetry rules, and contrast requirements. - Added PinRestrictions component to display pin usage restrictions based on series data, ensuring accurate representation of pin functionalities. - Implemented auditing script (auditar-temas.mjs) to check for theme consistency issues, including nonexistent tones, inverted pairs, low contrast, and inline fixed colors.
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 – 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 — 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 (18–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