> For the complete documentation index, see [llms.txt](https://docs.decentraland.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.decentraland.org/contributor/contributor-pt/guias-do-contribuidor/padroes-de-web-ui/custom-components.md).

# Componentes Personalizados

Distinguimos entre dois tipos de componentes personalizados, cada um com processos e expectativas diferentes.

## Tipos de Componentes

### A) Componentes Personalizados Específicos do Projeto

Componentes construídos para um projeto ou tela específicos, não destinados ao reuso em outros projetos.

**Exemplos:**

* Um `Caixa` com layout especial usado dentro de um projeto
* Um `Card` variante com layout personalizado para as telas de um projeto
* Visualizações de dados específicas do projeto
* Componentes de layout pontuais

**Quando usar:**

* Componente resolve um problema único de um projeto
* Pouco provável que seja necessário em outros projetos
* Muito específico para generalizar

### B) Componentes Candidatos a UI2

Componentes destinados ao reuso em múltiplos projetos e produtos.

**Exemplos:**

* `Navbar` - Navegação em todo o site
* `UserMenu` - Menu da conta do usuário
* Padronizado `Modal` diálogos
* Componentes sendo migrados do UI1

**Quando usar:**

* Componente será usado em múltiplos projetos
* Representa um padrão comum do Decentraland
* Substitui ou estende um componente do UI1

***

## Componentes Específicos do Projeto

### Requisitos

#### Usar MUI como Base

**DEVE** estender componentes MUI existentes sempre que possível:

```tsx
// ✅ Bom: Estende MUI Card
import { Card as MuiCard } from '@mui/material';
import { styled } from '@mui/material/styles';

const ProjectCard = styled(MuiCard)(({ theme }) => ({
  padding: theme.spacing(3),
  display: 'flex',
  flexDirection: 'column',
  gap: theme.spacing(2),
}));

// ❌ Ruim: Constrói do zero
const ProjectCard = styled('div')(({ theme }) => ({
  padding: theme.spacing(3),
  borderRadius: '4px',
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
  // Duplicando funcionalidade do Card
}));
```

**Não bifurque ou duplique padrões que o MUI já cobre:**

* Use `Card` em vez de criar uma caixa customizada com sombras
* Use `Button` em vez de criar uma âncora estilizada
* Use `TextField` em vez de criar um input personalizado
* Estender `Dialog` em vez de criar um modal personalizado

#### Somente Valores do Tema

**DEVE** usar valores do tema UI2:

```tsx
// ✅ Bom: Todos os valores do tema
const StyledBox = styled('div')(({ theme }) => ({
  color: theme.palette.text.primary,
  backgroundColor: theme.palette.background.paper,
  padding: theme.spacing(2),
  borderRadius: theme.shape.borderRadius,
  border: `1px solid ${theme.palette.divider}`,
}));

// ❌ Ruim: Valores ad-hoc
const StyledBox = styled('div')({
  color: '#333333',
  backgroundColor: '#FFFFFF',
  padding: '16px',
  borderRadius: '8px',
  border: '1px solid #E0E0E0',
});
```

**Nenhum valor arbitrário permitido:**

* Cores: Use `theme.palette` ou `dclColors`
* Espaçamentos: Use `theme.spacing(n)`
* Raio da borda: Use `theme.shape.borderRadius`
* Tipografia: Use `theme.typography` variantes
* Breakpoints: Use `theme.breakpoints` ajudantes

#### Estados e Acessibilidade

**DEVE** definir e implementar todos os estados interativos:

```tsx
const ActionButton = styled('button')(({ theme }) => ({
  // Estado base/ocioso
  padding: theme.spacing(1, 2),
  backgroundColor: theme.palette.primary.main,
  color: theme.palette.primary.contrastText,
  border: 'none',
  borderRadius: theme.shape.borderRadius,
  cursor: 'pointer',
  transition: theme.transitions.create(['background-color', 'transform']),
  
  // Estado hover
  '&:hover': {
    backgroundColor: theme.palette.primary.dark,
  },
  
  // Estado focus (navegação por teclado)
  '&:focus-visible': {
    outline: `2px solid ${theme.palette.primary.main}`,
    outlineOffset: 2,
  },
  
  // Estado ativo/pressionado
  '&:active': {
    transform: 'scale(0.98)',
  },
  
  // Estado desabilitado
  '&:disabled': {
    backgroundColor: theme.palette.action.disabledBackground,
    color: theme.palette.action.disabled,
    cursor: 'not-allowed',
  },
}));
```

**DEVE** implementar acessibilidade básica:\*\*

* **Navegação por teclado** - Focável e operável com teclado
* **Indicadores de foco** - Estados de foco visíveis
* **Labels ARIA** - Onde o texto não é visível
* **HTML semântico** - Usar elementos apropriados
* **Contraste de cor** - Atendimento aos padrões WCAG AA

### Exemplo: Componente Específico do Projeto

```tsx
// src/components/LandCard/LandCard.tsx
import { Card, CardContent, CardActions, Typography, Button } from '@mui/material';
import { styled } from '@mui/material/styles';
import type { Parcel } from '@/types';

interface LandCardProps {
  parcel: Parcel;
  onTransfer: (id: string) => void;
  onView: (id: string) => void;
}

const StyledCard = styled(Card)(({ theme }) => ({
  display: 'flex',
  flexDirection: 'column',
  height: '100%',
  transition: theme.transitions.create('transform'),
  
  '&:hover': {
    transform: 'translateY(-4px)',
  },
}));

const CoordinatesText = styled(Typography)(({ theme }) => ({
  color: theme.palette.text.secondary,
  fontFamily: theme.typography.fontFamilyMono,
}));

export function LandCard({ parcel, onTransfer, onView }: LandCardProps) {
  return (
    <StyledCard>
      <CardContent>
        <Typography variant="h6" gutterBottom>
          {parcel.name || `Parcel ${parcel.x},${parcel.y}`}
        </Typography>
        <CoordinatesText variant="body2">
          ({parcel.x}, {parcel.y})
        </CoordinatesText>
        <Typography variant="body2" color="text.secondary">
          Owner: {parcel.owner}
        </Typography>
      </CardContent>
      <CardActions>
        <Button size="small" onClick={() => onView(parcel.id)}>
          Ver
        </Button>
        <Button size="small" onClick={() => onTransfer(parcel.id)}>
          Transferir
        </Button>
      </CardActions>
    </StyledCard>
  );
}
```

***

## Componentes Candidatos a UI2

Componentes que serão compartilhados entre projetos requerem padrões mais elevados e documentação mais completa.

### Requisitos

#### Alinhamento ao Tema

**DEVE** confiar exclusivamente nos valores do tema UI2:

```tsx
// ✅ Bom: Integração completa com o tema
const NavbarContainer = styled('nav')(({ theme }) => ({
  backgroundColor: theme.palette.background.paper,
  borderBottom: `1px solid ${theme.palette.divider}`,
  padding: theme.spacing(0, 2),
  height: 64,
  display: 'flex',
  alignItems: 'center',
  gap: theme.spacing(2),
  
  [theme.breakpoints.down('md')]: {
    padding: theme.spacing(0, 1),
  },
}));
```

#### Cobertura no Storybook

**DEVE** adicionar stories abrangentes no Storybook:

**Cobertura obrigatória:**

1. **Todas as props e variantes**
   * Todas as combinações de props
   * Todas as variações de tamanho
   * Todas as variações de cor
2. **Todos os estados**
   * Ocioso/padrão
   * Carregando
   * Erro
   * Desabilitado
   * Hover (via addon de pseudo estados)
   * Foco (via addon de pseudo estados)
3. **Interações**
   * Manipuladores de clique
   * Envio de formulários
   * Navegação por teclado
4. **Esquemas de cor**
   * Modo claro
   * Modo escuro
5. **Comportamento responsivo**
   * Breakpoints chave (xs, md, lg)
   * Documentar comportamento em cada breakpoint

**Arquivo de exemplo do Storybook:**

```tsx
// Navbar.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Navbar } from './Navbar';

const meta: Meta<typeof Navbar> = {
  title: 'Components/Navbar',
  component: Navbar,
  parameters: {
    layout: 'fullscreen',
  },
  argTypes: {
    variant: {
      control: 'select',
      options: ['default', 'compact'],
    },
    showUserMenu: {
      control: 'boolean',
    },
  },
};

export default meta;
type Story = StoryObj<typeof Navbar>;

export const Default: Story = {
  args: {
    variant: 'default',
    showUserMenu: true,
  },
};

export const Compact: Story = {
  args: {
    variant: 'compact',
    showUserMenu: true,
  },
};

export const WithoutUserMenu: Story = {
  args: {
    variant: 'default',
    showUserMenu: false,
  },
};

export const Loading: Story = {
  args: {
    variant: 'default',
    showUserMenu: true,
    isLoading: true,
  },
};

// Testar diferentes viewports
export const Mobile: Story = {
  args: {
    variant: 'compact',
    showUserMenu: true,
  },
  parameters: {
    viewport: {
      defaultViewport: 'mobile1',
    },
  },
};

export const Tablet: Story = {
  args: {
    variant: 'default',
    showUserMenu: true,
  },
  parameters: {
    viewport: {
      defaultViewport: 'tablet',
    },
  },
};

// Testar esquemas de cor
export const DarkMode: Story = {
  args: {
    variant: 'default',
    showUserMenu: true,
  },
  parameters: {
    backgrounds: {
      default: 'dark',
    },
  },
};
```

### Estrutura do Componente

Componentes candidatos a UI2 DEVEM seguir esta estrutura:

```
src/components/Navbar/
├── Navbar.tsx           # Componente principal
├── Navbar.styles.ts     # Componentes estilizados
├── Navbar.stories.tsx   # Stories do Storybook
├── Navbar.test.tsx      # Testes unitários
├── types.ts             # Tipos TypeScript
├── index.ts             # Exports públicos
└── README.md            # Documentação do componente
```

### Requisitos de Documentação

**DEVE** incluir no README do componente:

1. **Propósito** - Qual problema isso resolve?
2. **Uso** - Como usar o componente
3. **Props** - Todas as props com tipos e descrições
4. **Exemplos** - Casos de uso comuns
5. **Acessibilidade** - Suporte de teclado, labels ARIA
6. **Theming** - Quais valores do tema ele usa
7. **Notas de migração** - Se estiver substituindo um componente do UI1

**Exemplo de README:**

````markdown
# Navbar

Componente de navegação do site com menu de usuário e comportamento responsivo.

## Uso

\```tsx
import { Navbar } from 'decentraland-ui2';

function App() {
  return (
    <Navbar
      variant="default"
      showUserMenu={true}
      onLogoClick={() => navigate('/') }
      onLoginClick={handleLogin}
    />
  );
}
\```

## Props

| Prop         | Type                   | Default   | Description                   |
| ------------ | ---------------------- | --------- | ----------------------------- |
| variant      | 'default' \| 'compact' | 'default' | Variação de navegação         |
| showUserMenu | boolean                | true      | Mostrar menu do usuário quando logado |
| onLogoClick  | () => void             | -         | Manipulador de clique no logo |
| onLoginClick | () => void             | -         | Manipulador de clique do botão de login |

## Acessibilidade

* Navegação por teclado: Tabular pelos itens do menu
* ARIA: Marcos e labels apropriados
* Leitor de tela: Anuncia o estado do menu

## Theming

Usa estes valores do tema:

* `theme.palette.background.paper`
* `theme.palette.divider`
* `theme.spacing`
* `theme.breakpoints`

````

### Requisitos de Testes

**DEVE** incluir testes para:

* Renderização de props
* Interações do usuário
* Recursos de acessibilidade
* Comportamento responsivo
* Estados de erro

```tsx
// Navbar.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Navbar } from './Navbar';

describe('Navbar', () => {
  it('deve renderizar o logo', () => {
    render(<Navbar />);
    expect(screen.getByRole('banner')).toBeInTheDocument();
  });

  it('deve chamar onLogoClick quando o logo for clicado', async () => {
    const onLogoClick = jest.fn();
    render(<Navbar onLogoClick={onLogoClick} />);
    
    await userEvent.click(screen.getByRole('link', { name: /decentraland/i }));
    expect(onLogoClick).toHaveBeenCalled();
  });

  it('deve ser navegável por teclado', async () => {
    render(<Navbar />);
    const firstLink = screen.getAllByRole('link')[0];
    
    firstLink.focus();
    expect(firstLink).toHaveFocus();
  });
});
```

***

## Matriz de Decisão

Use isto para decidir qual tipo de componente criar:

| Pergunta                                  | Específico do Projeto | Candidato a UI2 |
| ----------------------------------------- | --------------------- | --------------- |
| Outros projetos usarão isto?              | Não                   | Sim             |
| O UI1 tem um equivalente?                 | N/A                   | Provavelmente   |
| Precisa de documentação no Storybook?     | Não                   | **Sim**         |
| Precisa de testes abrangentes?            | Básico                | **Extensivo**   |
| Revisão de design necessária?             | Nível do projeto      | **Nível UI2**   |
| Pode usar padrões específicos do projeto? | Sim                   | **Não**         |
| Deve funcionar em todos os temas?         | Não                   | **Sim**         |

***

## Processo de Aprovação

### Componentes Específicos do Projeto

1. Revisão de código pelo mantenedor do projeto
2. Verificar conformidade com o tema
3. Testar no contexto do projeto
4. Fazer merge quando aprovado

### Componentes Candidatos a UI2

1. Revisão e aprovação de design
2. Revisão técnica de design
3. Implementação
4. Stories do Storybook
5. Testes abrangentes
6. Revisão de acessibilidade
7. Revisão de código
8. PR para o repositório UI2
9. Versionar e publicar
10. Atualizar projetos dependentes

***

## Boas Práticas

### Composição Sobre Customização

```tsx
// ✅ Bom: Compor componentes MUI
function FeatureCard({ title, children }) {
  return (
    <Card>
      <CardContent>
        <Typography variant="h6">{title}</Typography>
        {children}
      </CardContent>
    </Card>
  );
}

// ❌ Ruim: Re-implementar a funcionalidade do Card
function FeatureCard({ title, children }) {
  return (
    <div className="custom-card">
      <div className="custom-card-content">
        <h3>{title}</h3>
        {children}
      </div>
    </div>
  );
}
```

### Aprimoramento Progressivo

Começar simples e adicionar recursos conforme necessário:

1. Versão básica com funcionalidade central
2. Adicionar comportamento responsivo
3. Adicionar recursos de acessibilidade
4. Adicionar interações avançadas
5. Otimizar desempenho

### Documentação Primeiro

Antes de escrever código:

1. Escrever README do componente
2. Definir a interface de props
3. Listar estados necessários
4. Planejar stories do Storybook
5. Então implementar

***

## Próximos Passos

* Rever [Estilização & Theming](/contributor/contributor-pt/guias-do-contribuidor/padroes-de-web-ui/styling-and-theming.md) para detalhes de implementação
* Veja [Guia de Migração](https://github.com/decentraland/docs/blob/main/contributor/contributor-guides/web-ui-standards/broken-reference/README.md) para migrações de UI1 para UI2
* Verificar [Visão Geral do Processo](/contributor/contributor-pt/guias-do-contribuidor/padroes-de-web-ui/process-overview.md) para o fluxo de trabalho completo


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.decentraland.org/contributor/contributor-pt/guias-do-contribuidor/padroes-de-web-ui/custom-components.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
