Convenções (do / don't)
Os padrões que mantêm o design system coerente — e os que silenciosamente quebram.
Estas regras vêm da migração que consolidou três paletas em drift em uma. São aplicadas por review, não por lint — leia antes de mandar uma mudança de UI.
Tokens
Do — use classes semânticas do Tailwind
<div className="bg-card border border-border text-foreground">
<p className="text-muted-foreground">Helper copy</p>
<button className="bg-primary text-primary-foreground">Continue</button>Don't — hardcode cores
// Tudo isso quebra theme-switching e drifta com o tempo.
<div className="bg-black border border-white/10 text-white">
<p className="text-zinc-400">Helper copy</p>
<button className="bg-white text-black">Continue</button>
<div style={{ background: 'oklch(0% 0 0)' }}>Mesmo bg-white/[0.05] é um smell, exceto se você está modelando um alpha overlay sobre uma surface conhecida (ex. card hover). O token (hover:bg-accent) é preferido quando existe um.
Fonts
Do — confie no root layout
apps/client/app/layout.tsx já carrega Geist Sans e Geist Mono via next/font/google e expõe como --font-geist-sans / --font-geist-mono. Use font-sans e font-mono em componentes.
Don't — recarregar fonts por componente
// Don't.
import { Geist } from 'next/font/google';
const geist = Geist({ subsets: ['latin'] });
export function MyComponent() {
return <div className={geist.className}>...</div>;
}Carga de fonts por componente envia <link>s duplicados, derrota o preload, e produz inconsistências FOIT entre rotas.
Brand primitives
Do — importe de @/components/brand
import { BrandMark, GridGlowBackground } from '@/components/brand';
<header>
<BrandMark>
<VersionChip />
</BrandMark>
</header>Don't — re-roll do wordmark, do grid, do chip
// Don't — três páginas vão acabar com três wordmarks diferentes.
<span className="font-semibold tracking-[-0.02em] text-lg">UseDeploy</span>
// Don't — o grid e o glow são intencionalmente um componente por uma razão.
<div className="absolute inset-0 [background-image:linear-gradient(...)]" />Se você precisa de uma variante que o primitive não expõe, adicione uma prop ao primitive — não bifurque.
Páginas de auth
Do — envolva o conteúdo do form em <AuthCard>
// app/(auth)/login/page.tsx
export default function LoginPage() {
return (
<AuthCard
title="Sign in"
subtitle="Welcome back."
footer={<Link href="/register">Create an account</Link>}
>
<LoginForm />
</AuthCard>
);
}O (auth)/layout.tsx monta o chrome da página (GridGlowBackground, container centralizado). A página só declara seu form.
Don't — re-implementar o chrome por página
// Don't.
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="rounded-lg border border-white/10 bg-white/5 p-8">
<h1>Sign in</h1>
<LoginForm />
</div>
</div>
);Este é o padrão de bug que produziu três definições de wrapper antes de #145.
Auth client
Do — use cookies via apiClient com credentials
const { data, error } = await api.POST('/auth/login', { body: { email, password } });
// Cookie definido pelo server; sem token para armazenar.Don't — alcançar localStorage / bearer tokens
BetterAuth emite um cookie HTTP-only que o browser envia de volta automaticamente porque apiClient tem credentials: 'include' (withCredentials: true para callsites de axios). Não há token para capturar.
Schemas de form
Do — importe de @app/contracts, estenda para campos client-only
import { auth } from '@app/contracts';
export const registerFormSchema = auth.SignUpInput
.extend({ confirmPassword: z.string() })
.refine((d) => d.password === d.confirmPassword, {
message: 'Passwords must match',
path: ['confirmPassword'],
});Don't — re-definir campos conhecidos do server
// Don't — drifta do server, manda 400 aos users, o typecheck não pega.
export const registerFormSchema = z.object({
fullName: z.string().min(1),
emailAddress: z.string().email(),
password: z.string().min(8),
});Veja contracts para a história do bug.
Em dúvida
Abra o source de um primitive — são pequenos (o diretório brand/ inteiro está abaixo de 400 linhas). Os tokens são 250 linhas de globals.css. Leia o código; é a fonte de verdade.