Muitos apps, um sistema
A falha comum: um kit é criado para o app A, os botões do app B são 4px mais altos e 2px mais arredondados, então o app B bifurca o botão "só desta vez". Cinco apps depois, há seis botões, oito selects e três modais, todos fazendo o mesmo trabalho.
Faça o componente compartilhado ser tokenizado: ele carrega comportamento, marcação e estados, e cada tema fornece as medidas. Em seguida, consolide app por app.
Componentes tokenizados
Leia cada medida que difere entre os apps a partir de um token de tema:
@theme {
--spacing-btn-sm: 1.75rem; --spacing-btn-px-sm: 0.625rem;
--spacing-btn-md: 2.25rem; --spacing-btn-px-md: 0.875rem;
--spacing-field: 2.125rem; /* altura de input / select */
--text-label: 0.8125rem;
}
md: "h-btn-md px-btn-px-md text-sm",
icon: "size-btn-md",
Então:
- Uma suíte sobrescreve os tokens uma vez em seu tema, e cada app dentro dela obtém seu próprio controle sob medida.
- Raio, preenchimentos e sombras já são tokens (
rounded-field,bg-accent,--shadow-card). - Preenchimentos de pintura como cor mais uma classe decorativa, por exemplo
bg-accent accent-fill. Um tema que só define a decoração (um brilho) nunca renderiza um botão transparente.
Veja references/button.tsx.
Passo de consolidação, por app
-
Inventarie as cópias locais. Busque em cada app por
export function (Btn|Button|IconBtn|Select|Field|Input|Modal|Dialog|Sheet|Badge|Empty|Pager|Toast|Stat|Status|Spinner). Conte os pontos de uso. -
Substitua e exclua onde o componente compartilhado corresponde após a tematização. Mapeie nomes de variantes explicitamente e escreva o mapeamento:
Nome local Variante compartilhada primary,navyaccentsecondaryoutlinequiet,softsubtledanger(sólido)danger-solid -
Mantenha itens específicos do app como app-específicos:
- widgets de domínio, gráficos e chrome do app;
- controles cujo formato é a marca (slabs iOS, controles planos Carbon, CTAs de 48px em pílula);
- controles com comportamento que o compartilhado não possui (checkbox tri-estado, chips removíveis, um item de reset "Qualquer", filtros multi-seleção).
Um wrapper fino é aceitável quando adiciona uma única coisa. Anote o motivo em um comentário.
-
Aposente
<select>s nativos e selects baseados em menu por um único Select.- Radix Select reserva
""como "sem valor": use um sentinela como"all"e mapeie de volta. - Encaminhe
aria-labelledbyearia-describedby. - Scripts de passo a passo que chamam
selectOption()devem clicar no combobox e depois na opção.
- Radix Select reserva
-
Verifique cada app em relação às suas próprias capturas de tela (veja
visual-regression-pass). Diferenças reais geralmente são intencionais: tamanhos, brilho, escala ao pressionar.
Armadilhas do tailwind-merge (cn())
tailwind-merge só conhece a escala embutida do Tailwind. Tokens personalizados quebram de três maneiras silenciosas:
- Tokens de espaçamento desconhecidos.
cn("h-btn-md", "h-10")mantém ambos, porque não sabe queh-btn-mdé uma altura, e a ordem da folha de estilo escolhe o vencedor. Umh-10do chamador perdia para o token e um botão de 40px renderizava com 36px. Registre os tokens:extendTailwindMerge({ extend: { theme: { spacing: ["btn-sm","btn-md","field", …] } } }). - Tamanhos de texto desconhecidos.
cn("text-meta", "text-ink")tratatext-metacomo cor e o descarta. Registre os tamanhos:theme: { text: ["meta","micro","label"] }. - Sombras e cores personalizadas.
text-14ao lado detext-t3, oushadow-[var(--x)]arbitrária ao lado de uma sombra nomeada: o mesmo problema. Quando uma base de código tem uma escala personalizada fechada em todos os lugares, useclsxsimples lá.
Veja references/cn.ts.
Foco e validação
- Um estilo
aria-invalid:ring-*superafocus-visible:ring-*e esconde o anel de foco em um campo inválido focado. Escrevaaria-invalid:not-focus-visible:ring-…. - Elementos
<button>simples dentro de formulários os enviam. Se o Botão compartilhado não padrãotype="button", todo ponto de uso convertido que não seja de envio deve defini-lo.