Pular para o conteúdo

Stack

  • React 19.1 + TypeScript 5.9
  • Vite (não Webpack/Create React App)
  • Node >= 22.3 (mínimo obrigatório)
  • TailwindCSS 4 - todos os estilos devem usar Tailwind
  • Radix UI - componentes base headless
  • shadcn/ui - componentes pré-construídos
  • Lucide React - ícones (não use outros pacotes de ícones)
  • Framer Motion - animações
  • Zustand - state management global
    • SEMPRE use devtools middleware
    • SEMPRE use shallow ao selecionar múltiplos valores
    • SEMPRE versione a store com version
  • TanStack Query (@tanstack/react-query) - data fetching e cache
  • React Hook Form + Zod - formulários e validação
  • React Router 7 - roteamento
  • Zod 4 - validação e parsing de dados
  • class-variance-authority (cva) - variantes de componentes
  • clsx + tailwind-merge - merge de classes CSS
  • date-fns - manipulação de datas (não use moment.js ou dayjs)
  • Vitest (não Jest)
  • Testing Library (@testing-library/react)
  • Playwright (Testes e2e)
  • MSW - mock de API
  • Biome - linting e formatting (NÃO use ESLint ou Prettier)

Terminal window
gaio-frontend/
├── packages/
│ ├── common/ # Código compartilhado, utils, types, hooks
│ ├── components/ # Biblioteca de componentes UI base
│ ├── console/ # Aplicação principal (console)
│ └── site/ # Site institucional
├── local.env # Variáveis de ambiente local (NÃO commitado)
├── deploy.env # Config de deploy (NÃO commitado)
└── package.json # Root package.json (workspaces)

O projeto usa npm workspaces para gerenciar o monorepo. Cada package tem seu próprio package.json.

  • Use @gaio/common para importar do common (hooks, utils, types)
  • Use @gaio/components para importar componentes base
  • Nunca use caminhos relativos entre pacotes (../../packages/common)
  • Imports locais dentro do mesmo package podem usar caminhos relativos ou aliases @/
Terminal window
console (aplicação principal)
├── depende de → @gaio/common
├── depende de → @gaio/components
└── usa → radix-ui, react-query, zustand, etc.
components (biblioteca UI base)
├── depende de → @gaio/common
└── usa → radix-ui, lucide-react, framer-motion
common (base compartilhada)
└── hooks, utils, types, theme

Terminal window
src/
├── components/ # Componentes reutilizáveis por categoria
│ ├── actions/ # Componentes de ações (delete, edit, settings)
│ ├── animations/ # Componentes animados
│ ├── buttons/ # Componentes de botões
│ ├── cards/ # Componentes de cards
│ ├── chart/ # Componentes de gráficos
│ ├── contact/ # Componentes relacionados a contatos
│ ├── custom-fields/ # Componentes de campos customizados
│ ├── dates/ # Componentes relacionados a datas
│ ├── dynamic-filter/ # Sistema de filtros dinâmicos
│ ├── fields/ # Componentes de campos de formulário
│ ├── header/ # Componentes do header
│ ├── modal/ # Componentes de modais
│ ├── screens/ # Componentes de layouts e sidebar
│ ├── sentiment/ # Componentes de análise de sentimento
│ ├── tags/ # Componentes de tags
│ ├── ui/ # Componentes UI genéricos
│ └── utils/ # Componentes utilitários
├── features/ # Features complexas com domínio próprio
│ ├── chat/ # Feature de chat completa
│ │ ├── components/
│ │ ├── message/
│ │ ├── store/ # Store Zustand específica do chat
│ │ ├── types/
│ │ └── utils/
│ └── contacts/ # Feature de contatos completa
│ └── editor/
├── screens/ # Telas/páginas da aplicação
│ ├── admin/ # Telas administrativas
│ ├── user/ # Telas de usuário
│ └── error/ # Telas de erro
├── hooks/ # Custom hooks globais
├── services/ # Serviços de API
├── utils/ # Utilitários gerais
├── config/ # Configurações
├── constants/ # Constantes
└── router/ # Configuração de rotas

Baseado na estrutura real do projeto

  1. Componentes em src/components/:

    • Pasta: kebab-case/
    • Arquivo: component.tsx
    • Exemplo: src/components/alert/component.tsx
    • Função exportada: PascalCase (ex: export function Alert())
  2. Componentes em src/features/:

    • Pasta: kebab-case/
    • Arquivos específicos: kebab-case.tsx
    • Exemplo: src/features/chat/chat-header.tsx
    • Função exportada: PascalCase (ex: export function ChatHeader())
  3. Outros arquivos:

    • Hooks: use-kebab-case.ts (ex: use-auth.ts)
    • Services: kebab-case.ts (ex: user.ts em services/)
    • Utils: kebab-case.ts (ex: format-date.ts)
    • Types: kebab-case.ts ou kebab-case.types.ts
    • Stores: kebab-case.ts (ex: chat.ts em features/chat/store/)

src/components/button/component.tsx
import { type ComponentProps } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@gaio/components";
const buttonVariants = cva("base-classes-here", {
variants: {
variant: {
default: "variant-classes",
outline: "variant-classes",
},
size: {
default: "size-classes",
sm: "size-classes",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export interface ButtonProps
extends ComponentProps<"button">, VariantProps<typeof buttonVariants> {
// Props específicas aqui
}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={buttonVariants({ variant, size, className })}
{...props}
/>
);
}
src/features/chat/chat-header.tsx
import { cn } from "@gaio/components";
import type { ChatHeaderProps } from "./types/chat";
export function ChatHeader({ className, ...props }: ChatHeaderProps) {
return (
<div className={cn("chat-header-classes", className)} {...props}>
{/* Conteúdo */}
</div>
);
}
  1. Estrutura de pastas:

    • Componentes base: src/components/[nome]/component.tsx
    • Features: src/features/[feature]/[nome].tsx
  2. Use TypeScript - tipagem completa obrigatória

  3. Props interface - sempre exporte a interface de props

    • SEMPRE estenda ComponentProps<'elemento'> para herdar props nativas (className, style, etc.)
    • Combine com VariantProps<typeof variants> quando usar CVA
  4. cn() - use cn() do @gaio/components para merge de classes

  5. cva - SEMPRE use CVA para criar variações de componentes

    • Nunca crie variações manualmente com objetos ou condicionais
    • Exemplos de variações: size, variant, color, state
  6. TailwindCSS only - nunca use CSS modules


src/hooks/use-custom-hook.ts
import { useState, useEffect } from "react";
export function useCustomHook(param: string) {
const [state, setState] = useState<Type>(initialValue);
useEffect(() => {
// Effect logic
}, [param]);
return { state, setState };
}

O package @gaio/common fornece hooks reutilizáveis:

import {
useBeforeUnload,
useBodyScrollLock,
useDebounce,
useBreakpoint,
useIntersectionObserver,
useScroll,
// ...
} from "@gaio/common";

src/services/user.ts
import { api } from "@/lib/api";
import type { User } from "@/types/user";
export const userService = {
getUser: async (id: string): Promise<User> => {
const response = await api.get(`/users/${id}`);
return response.data;
},
updateUser: async (id: string, data: Partial<User>): Promise<User> => {
const response = await api.patch(`/users/${id}`, data);
return response.data;
},
};
src/hooks/use-user.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { userService } from "@/services/user";
export function useUser(id: string) {
return useQuery({
queryKey: ["user", id],
queryFn: () => userService.getUser(id),
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userService.updateUser,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["user", data.id] });
},
});
}

SEMPRE siga este padrão para stores Zustand:

  1. devtools middleware - para debug no Redux DevTools
  2. version - versionar a store (incrementar ao mudar estrutura)
  3. shallow - usar ao consumir múltiplos valores
src/features/chat/store/chat.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import type { Chat } from "../types/chat";
interface ChatState {
selectedChat: Chat | null;
chats: Chat[];
setSelectedChat: (chat: Chat | null) => void;
setChats: (chats: Chat[]) => void;
}
export const useChatStore = create<ChatState>()(
devtools(
(set) => ({
selectedChat: null,
chats: [],
setSelectedChat: (selectedChat) => set({ selectedChat }),
setChats: (chats) => set({ chats }),
}),
{
name: "chat-store", // Nome para Redux DevTools
version: 1, // OBRIGATÓRIO: versionar
},
),
);
src/features/chat/chat-list.tsx
import { useChatStore } from './store/chat';
import { shallow } from 'zustand/shallow';
export function ChatList() {
// ✅ CORRETO - múltiplos valores COM shallow
const { chats, setSelectedChat } = useChatStore(
(state) => ({
chats: state.chats,
setSelectedChat: state.setSelectedChat
}),
shallow // ← OBRIGATÓRIO
);
// ✅ CORRETO - valor único NÃO precisa shallow
const selectedChat = useChatStore((state) => state.selectedChat);
// ❌ ERRADO - múltiplos valores SEM shallow
const { chats, setSelectedChat } = useChatStore((state) => ({
chats: state.chats,
setSelectedChat: state.setSelectedChat
}));
return (
<div>
{/* Renderizar chats */}
</div>
);
}

Incremente version ao mudar estrutura do state:

  • ✅ Adicionar campos
  • ✅ Remover campos
  • ✅ Mudar tipos
  • ✅ Alterar objetos aninhados
// Version 1
interface ChatState {
selectedChat: Chat | null;
chats: Chat[];
}
// Version 2 - adicionou campo
interface ChatState {
selectedChat: Chat | null;
chats: Chat[];
unreadCount: number; // ← Novo, incrementar para version: 2
}

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const formSchema = z.object({
email: z.string().email('E-mail inválido'),
password: z.string().min(8, 'Senha deve ter no mínimo 8 caracteres'),
});
type FormData = z.infer<typeof formSchema>;
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(formSchema),
});
const onSubmit = (data: FormData) => {
// Handle submission
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
{/* ... */}
</form>
);
}

import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Button } from './component';
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
});
  • Use Vitest (não Jest)
  • Arquivo: [nome].test.tsx ao lado do componente
  • Use Testing Library queries semânticas

  1. Sempre use tipagem explícita - evite any a todo custo
  2. Prefira tipos estreitos - use union types ao invés de genéricos demais
  3. Use type guards - valide tipos em runtime
  4. Imutabilidade - prefira readonly e const
  5. Composição sobre herança - use tipos compostos

  1. Funções exportadas/públicas: SEMPRE tipagem explícita de retorno
  2. Funções internas/privadas simples: Inferência OK se óbvio
  3. Funções genéricas/complexas: SEMPRE tipagem explícita
  4. Parâmetros: SEMPRE tipados (nunca inferir)
// ✅ Função exportada - tipo de retorno explícito
export function calculateTotal(items: CartItem[], discount: number): number {
return items.reduce((sum, item) => sum + item.price, 0) * (1 - discount);
}
// ✅ Função interna simples - inferência OK
function sumPrices(items: CartItem[]) {
return items.reduce((sum, item) => sum + item.price, 0); // inferido como number
}
// ✅ Arrow functions exportadas - tipo explícito
export const formatUser = (user: User): FormattedUser => ({
id: user.id,
fullName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase(),
});
// ✅ Arrow function interna - inferência OK se simples
const normalizeEmail = (email: string) => email.toLowerCase().trim();
// ✅ Async exportada - tipo explícito
export async function fetchUser(id: string): Promise<User> {
const response = await api.get(`/users/${id}`);
return response.data;
}
// ✅ Callback/handler interno - inferência OK
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
Logger.debug(e.currentTarget.value); // inferido como void
};
// ✅ Função genérica - SEMPRE explícita
function mapArray<T, U>(items: T[], mapper: (item: T) => U): U[] {
return items.map(mapper);
}
// ❌ NUNCA use any
function processData(data: any) {
return data.map((item: any) => item.value);
}
// ❌ Parâmetros sem tipo
function calculate(a, b) {
return a + b;
}
// ❌ Função exportada sem tipo de retorno
export function getUser(id: string) {
return api.get(`/users/${id}`); // Que tipo retorna?
}
// ❌ Função complexa sem tipo de retorno
function complexOperation(data: Data[]) {
if (data.length === 0) return null;
if (data.length === 1) return data[0];
return data; // null | Data | Data[] - ambíguo!
}

SEMPRE tipagem explícita:

  • ✅ Funções exportadas (API pública)
  • ✅ Funções assíncronas
  • ✅ Funções com múltiplos return types
  • ✅ Funções genéricas
  • ✅ Callbacks complexos
  • ✅ Métodos de classe públicos

Inferência OK:

  • ✅ Funções auxiliares internas simples
  • ✅ Arrow functions em componentes (onClick, onChange)
  • ✅ Transformações simples de dados
  • ✅ Guards de tipo óbvios

  1. SEMPRE use try-catch em operações assíncronas
  2. SEMPRE tipagem de erro específica
  3. NUNCA deixe catch vazio
  4. SEMPRE logar erros apropriadamente
  5. Retorne tipos explícitos de sucesso/erro
// Type helper para resultado de operações
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// Uso em serviços
async function fetchUserSafe(id: string): Promise<Result<User>> {
try {
const response = await api.get(`/users/${id}`);
return { success: true, data: response.data };
} catch (error) {
if (error instanceof ApiError) {
return {
success: false,
error: new Error(`Failed to fetch user: ${error.message}`),
};
}
return {
success: false,
error: new Error("Unknown error occurred"),
};
}
}
// Uso no componente
async function loadUser(id: string) {
const result = await fetchUserSafe(id);
if (result.success) {
setUser(result.data);
} else {
showError(result.error.message);
}
}
// Sempre tipagem específica de erro
class ApiError extends Error {
constructor(
message: string,
public statusCode: number,
public code?: string,
) {
super(message);
this.name = "ApiError";
}
}
// Uso correto
async function updateUser(id: string, data: UpdateUserData): Promise<User> {
try {
const response = await api.patch(`/users/${id}`, data);
return response.data;
} catch (error) {
// Type guard para erro conhecido
if (error instanceof ApiError) {
if (error.statusCode === 404) {
throw new Error(`User ${id} not found`);
}
if (error.statusCode === 403) {
throw new Error("Permission denied");
}
}
// Erro desconhecido
if (error instanceof Error) {
throw new Error(`Failed to update user: ${error.message}`);
}
// Fallback
throw new Error("An unexpected error occurred");
}
}
// Defina erros específicos do domínio
class ValidationError extends Error {
constructor(
public field: string,
message: string,
) {
super(message);
this.name = "ValidationError";
}
}
class NotFoundError extends Error {
constructor(
public resource: string,
public id: string,
) {
super(`${resource} with id ${id} not found`);
this.name = "NotFoundError";
}
}
// Uso em funções
async function deleteUser(id: string): Promise<void> {
try {
await api.delete(`/users/${id}`);
} catch (error) {
if (error instanceof ApiError && error.statusCode === 404) {
throw new NotFoundError("User", id);
}
throw error;
}
}
// No componente
try {
await deleteUser(userId);
showSuccess("User deleted");
} catch (error) {
if (error instanceof NotFoundError) {
showError(`User not found`);
} else if (error instanceof ValidationError) {
showError(`Invalid ${error.field}`);
} else {
showError("Failed to delete user");
}
}
// NUNCA faça isso
try {
await api.get("/users");
} catch (error) {
Logger.error(error); // error é any
}
// NUNCA catch vazio
try {
await api.post("/users", data);
} catch (e) {
// Silenciar erro é perigoso
}
// NUNCA capture tudo sem especificar
try {
const result = await complexOperation();
} catch {
return null; // Perde informação do erro
}

// Type guard com is
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"email" in value &&
typeof value.id === "string" &&
typeof value.email === "string"
);
}
// Uso
function processResponse(data: unknown) {
if (isUser(data)) {
// TypeScript sabe que data é User aqui
Logger.debug(data.email);
}
}
// Type guard para arrays
function isUserArray(value: unknown): value is User[] {
return Array.isArray(value) && value.every(isUser);
}
// Union com discriminador
type ApiResponse<T> =
| { status: 'loading' }
| { status: 'error'; error: Error }
| { status: 'success'; data: T };
// Uso com exhaustive checking
function handleResponse<T>(response: ApiResponse<T>) {
switch (response.status) {
case 'loading':
return <Spinner />;
case 'error':
return <ErrorMessage error={response.error} />;
case 'success':
return <DataDisplay data={response.data} />;
default:
// Exhaustive check - não compila se faltar um case
const _exhaustive: never = response;
return _exhaustive;
}
}

// Partial - todos campos opcionais
type UpdateUser = Partial<User>;
// Required - todos campos obrigatórios
type CompleteUser = Required<User>;
// Pick - selecionar campos
type UserPreview = Pick<User, "id" | "name" | "avatar">;
// Omit - excluir campos
type UserWithoutPassword = Omit<User, "password">;
// Record - objeto com chaves tipadas
type UserMap = Record<string, User>;
// Readonly - imutável
type ImmutableUser = Readonly<User>;
// Extrair tipo de retorno de função
type UserServiceReturn = ReturnType<typeof userService.getUser>;
// Extrair tipo de parâmetros
type UserServiceParams = Parameters<typeof userService.getUser>;
// Extrair tipo de Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type UserData = UnwrapPromise<ReturnType<typeof fetchUser>>;
// Non-nullable
type NonNullableUser = NonNullable<User | null | undefined>;
// Array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type UserFromArray = ArrayElement<User[]>;
// Tipo condicional
type IsString<T> = T extends string ? true : false;
// Extrair campos de um tipo específico
type StringKeys<T> = {
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
type UserStringFields = StringKeys<User>; // 'name' | 'email' | ...
// Deep Readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

// Use readonly em interfaces
interface User {
readonly id: string;
readonly email: string;
name: string; // Apenas campos que mudam ficam mutáveis
}
// Use const assertions
const CONFIG = {
API_URL: "https://api.example.com",
TIMEOUT: 5000,
} as const;
// Agora CONFIG é deeply readonly
type Config = typeof CONFIG; // { readonly API_URL: "https://api.example.com", ... }
// Arrays imutáveis
const ROLES = ["admin", "user", "guest"] as const;
type Role = (typeof ROLES)[number]; // 'admin' | 'user' | 'guest'
// Readonly em parâmetros
function processUsers(users: readonly User[]): void {
// users.push(...) // Erro! Não pode modificar
const newUsers = [...users, newUser]; // Ok! Cria novo array
}

// Componente genérico
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<div>
{items.map((item) => (
<div key={keyExtractor(item)}>
{renderItem(item)}
</div>
))}
</div>
);
}
// Uso
<List<User>
items={users}
renderItem={(user) => <UserCard user={user} />}
keyExtractor={(user) => user.id}
/>
// Handlers específicos
type ButtonClickHandler = React.MouseEvent<HTMLButtonElement>;
type InputChangeHandler = React.ChangeEvent<HTMLInputElement>;
type FormSubmitHandler = React.FormEvent<HTMLFormElement>;
// Componente
interface FormProps {
onSubmit: (data: FormData) => void;
}
function Form({ onSubmit }: FormProps) {
const handleSubmit = (e: FormSubmitHandler) => {
const formData = new FormData(e.currentTarget);
onSubmit(Object.fromEntries(formData));
};
return <form onSubmit={stopPropagate(handleSubmit)}>...</form>;
}
// Children específico
interface CardProps {
children: React.ReactNode;
header?: React.ReactElement<HeaderProps>;
}
// Function as children
interface RenderProps<T> {
data: T;
children: (data: T) => React.ReactNode;
}
function DataProvider<T>({ data, children }: RenderProps<T>) {
return <>{children(data)}</>;
}

Antes de commitar código:

  • Nenhum uso de any (exceto casos extremos justificados)
  • Todos os tipos exportados estão documentados
  • Try-catch em todas operações assíncronas
  • Erros tipados adequadamente
  • Type guards onde necessário
  • Uso de utility types quando apropriado
  • Interfaces prefiram readonly onde aplicável
  • Discriminated unions para estados complexos
  • Exhaustive checking em switches
  • Props de componentes totalmente tipadas

// 1. Usar any
const data: any = fetchData();
// 2. Type assertion desnecessário
const user = data as User; // Perigoso se data não for User
// 3. Non-null assertion sem garantia
const value = possiblyNull!; // Pode quebrar em runtime
// 4. Ignorar erros
try {
await operation();
} catch {} // Silenciar erro
// 5. Tipos muito amplos
function process(data: object) {} // object é muito genérico
// 6. Mutação de readonly
function modify(arr: readonly string[]) {
arr.push("new"); // Erro!
}
// 1. Type guards
if (isUser(data)) {
Logger.debug(data.email);
}
// 2. Validação adequada
const user = validateUser(data); // Lança erro se inválido
// 3. Null checking
const value = possiblyNull ?? defaultValue;
// 4. Log apropriado
try {
await operation();
} catch (error) {
logger.error("Operation failed", { error });
throw error;
}
// 5. Tipos específicos
interface ProcessData {
id: string;
value: number;
}
function process(data: ProcessData) {}
// 6. Criar nova array
function modify(arr: readonly string[]): string[] {
return [...arr, "new"];
}

Terminal window
npm run build # Build todos workspaces
npm run lint # Biome lint + typecheck
npm run test # Rodar testes
npm run serve:demo # Dev apontando para stg
  • local - desenvolvimento local
  • test/dev - ambiente de desenvolvimento
  • demo/stg - staging
  • live/prd - produção

O projeto usa EZ4 para deploy na AWS.

  • @ez4/common - Tipos e utilitários
  • @ez4/utils - Funções gerais
  • @ez4/project - Config de projeto
  • @ez4/aws-bucket - S3 Bucket
  • @ez4/aws-cloudfront - CloudFront (CDN)
  • @ez4/distribution - Contrato de distribuição

Este documento contém diretrizes obrigatórias. Claude Code deve seguir TODOS os padrões.

  • Li arquivos relevantes na pasta?
  • Identifiquei componentes/código similar?
  • Identifiquei padrões de nomenclatura?
  • Identifiquei estrutura de código?
  • Mostrei exemplos do código existente?
  • Recebi confirmação?
  • Seguindo EXATAMENTE os padrões identificados?
  • Reutilizando código ao invés de duplicar?
  • TypeScript com tipagem completa?
  • Usando Biome (não ESLint/Prettier)?
  • Usando TailwindCSS (não CSS modules)?
  • Usando bibliotecas corretas (Vitest, não Jest)?
  • Nomenclatura correta?
    • Componentes base: [pasta]/component.tsx?
    • Features: [feature]/kebab-case.tsx?
    • Stores com devtools + version?
    • Usando shallow em Zustand?

  • ❌ Nunca use ESLint ou Prettier (use Biome)
  • ❌ Nunca use Jest (use Vitest)
  • ❌ Nunca use CSS modules (use Tailwind)
  • ❌ Nunca use moment.js/dayjs (use date-fns)
  • ❌ Nunca implemente sem analisar código existente
  • ❌ Nunca duplique código que já existe
  • ❌ Nunca crie padrões novos sem justificativa
  • ❌ Nunca use Context API (use Zustand)
  • ❌ Nunca faça fetch direto (use React Query)
  • ❌ Nunca commite código sem tipagem TypeScript
  • ❌ Nunca commite secrets/API keys
  • ❌ Nunca crie store Zustand sem devtools + version
  • ❌ Nunca use múltiplos valores Zustand sem shallow
  • ❌ Nunca ignore a estrutura de pastas do projeto

P: Onde criar componentes reutilizáveis? R: Em src/components/[categoria]/component.tsx

P: Onde criar features complexas? R: Em src/features/[feature-name]/

P: Como nomear arquivos de componentes? R: Componentes base: component.tsx. Features: kebab-case.tsx

P: Onde ficam as stores Zustand? R: Dentro das features: src/features/[feature]/store/

P: Como usar ícones? R: Lucide React (já instalado)

P: Como estilizar? R: TailwindCSS + cva para variantes