> 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-es/guias-para-colaboradores/estandares-de-ui-web/custom-components.md).

# Components personalizados

Distinguimos entre dos tipos de componentes personalizados, cada uno con procesos y expectativas diferentes.

## Tipos de componentes

### A) Componentes personalizados específicos del proyecto

Componentes construidos para un proyecto o pantalla específicos, no destinados a reutilizarse en otros proyectos.

**Ejemplos:**

* A `Caja` con diseño especial usado dentro de un solo proyecto
* A `Card` variante con diseño personalizado para las pantallas de un solo proyecto
* Visualizaciones de datos específicas del proyecto
* Componentes de diseño únicos

**Cuándo usar:**

* El componente resuelve un problema único de un solo proyecto
* Poco probable que sea necesario en otros proyectos
* Demasiado específico para generalizar

### B) Componentes candidatos a UI2

Componentes destinados a reutilizarse en múltiples proyectos y productos.

**Ejemplos:**

* `Navbar` - Navegación a nivel del sitio
* `UserMenu` - Menú de cuenta de usuario
* Estandarizado `Modal` diálogos
* Componentes que se están migrando desde UI1

**Cuándo usar:**

* El componente se utilizará en múltiples proyectos
* Representa un patrón común de Decentraland
* Reemplaza o extiende un componente de UI1

***

## Componentes específicos del proyecto

### Requisitos

#### Usar MUI como base

**DEBE** extender componentes MUI existentes siempre que sea posible:

```tsx
// ✅ Bueno: Extiende 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),
}));

// ❌ Malo: Construye desde cero
const ProjectCard = styled('div')(({ theme }) => ({
  padding: theme.spacing(3),
  borderRadius: '4px',
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
  // Duplicando la funcionalidad de Card
}));
```

**No bifurcar ni duplicar patrones que MUI ya cubre:**

* Usa `Card` en lugar de crear una caja personalizada con sombras
* Usa `Button` en lugar de crear un enlace estilizado
* Usa `TextField` en lugar de crear un input personalizado
* Extender `Dialog` en lugar de crear un modal personalizado

#### Sólo valores del tema

**DEBE** usar valores del tema UI2:

```tsx
// ✅ Bueno: Todos los valores del 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}`,
}));

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

**No se permiten valores arbitrarios:**

* Colores: Usar `theme.palette` o `dclColors`
* Espaciado: Usar `theme.spacing(n)`
* Radio de borde: Usar `theme.shape.borderRadius`
* Tipografía: Usar `theme.typography` variantes
* Puntos de quiebre: Usar `theme.breakpoints` helpers

#### Estados y accesibilidad

**DEBE** definir e implementar todos los estados interactivos:

```tsx
const ActionButton = styled('button')(({ theme }) => ({
  // Estado base/inactivo
  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 (navegación por teclado)
  '&:focus-visible': {
    outline: `2px solid ${theme.palette.primary.main}`,
    outlineOffset: 2,
  },
  
  // Estado activo/presionado
  '&:active': {
    transform: 'scale(0.98)',
  },
  
  // Estado disabled
  '&:disabled': {
    backgroundColor: theme.palette.action.disabledBackground,
    color: theme.palette.action.disabled,
    cursor: 'not-allowed',
  },
}));
```

**DEBE** implementar accesibilidad básica:\*\*

* **Navegación por teclado** - Enfocable y operable con teclado
* **Indicadores de foco** - Estados de foco visibles
* **Etiquetas ARIA** - Donde el texto no sea visible
* **HTML semántico** - Usar elementos apropiados
* **Contraste de color** - Cumplir con los estándares WCAG AA

### Ejemplo: Componente específico del proyecto

```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

Los componentes que se compartirán entre proyectos requieren estándares más altos y documentación más completa.

### Requisitos

#### Alineación con el tema

**DEBE** apoyarse exclusivamente en los valores del tema UI2:

```tsx
// ✅ Bueno: Integración completa con el 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 en Storybook

**DEBE** añadir historias de Storybook completas:

**Cobertura requerida:**

1. **Todas las props y variantes**
   * Cada combinación de props
   * Todas las variantes de tamaño
   * Todas las variantes de color
2. **Todos los estados**
   * Inactivo/por defecto
   * Cargando
   * Error
   * Deshabilitado
   * Hover (vía addon de pseudoestados)
   * Focus (vía addon de pseudoestados)
3. **Interacciones**
   * Manejadores de click
   * Envíos de formularios
   * Navegación por teclado
4. **Esquemas de color**
   * Modo claro
   * Modo oscuro
5. **Comportamiento responsivo**
   * Puntos de quiebre clave (xs, md, lg)
   * Documentar el comportamiento en cada punto de quiebre

**Archivo de ejemplo de 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,
  },
};

// Probar 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',
    },
  },
};

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

### Estructura del componente

Los componentes candidatos a UI2 DEBEN seguir esta estructura:

```
src/components/Navbar/
├── Navbar.tsx           # Componente principal
├── Navbar.styles.ts     # Componentes estilizados
├── Navbar.stories.tsx   # Historias de Storybook
├── Navbar.test.tsx      # Pruebas unitarias
├── types.ts             # Tipos de TypeScript
├── index.ts             # Exportaciones públicas
└── README.md            # Documentación del componente
```

### Requisitos de documentación

**DEBE** incluir en el README del componente:

1. **Propósito** - ¿Qué problema resuelve esto?
2. **Uso** - Cómo usar el componente
3. **Props** - Todas las props con tipos y descripciones
4. **Ejemplos** - Casos de uso comunes
5. **Accesibilidad** - Soporte de teclado, etiquetas ARIA
6. **Theming** - Qué valores del tema utiliza
7. **Notas de migración** - Si reemplaza un componente de UI1

**README de ejemplo:**

````markdown
# Navbar

Componente de navegación a nivel del sitio con menú de usuario y comportamiento responsive.

## 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' | Variant de navegación         |
| showUserMenu | boolean                | true      | Mostrar menú de usuario cuando esté conectado |
| onLogoClick  | () => void             | -         | Manejador de click del logo   |
| onLoginClick | () => void             | -         | Manejador de click del botón de login |

## Accesibilidad

* Navegación por teclado: Tabulación por los elementos del menú
* ARIA: Landmarks y etiquetas apropiadas
* Lector de pantalla: Anuncia el estado del menú

## Theming

Usa estos valores del tema:

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

````

### Requisitos de testing

**DEBE** incluir pruebas para:

* Renderizado de props
* Interacciones de usuario
* Funciones de accesibilidad
* Comportamiento responsivo
* Estados de error

```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('should render logo', () => {
    render(<Navbar />);
    expect(screen.getByRole('banner')).toBeInTheDocument();
  });

  it('should call onLogoClick when logo is clicked', async () => {
    const onLogoClick = jest.fn();
    render(<Navbar onLogoClick={onLogoClick} />);
    
    await userEvent.click(screen.getByRole('link', { name: /decentraland/i }));
    expect(onLogoClick).toHaveBeenCalled();
  });

  it('should be keyboard navigable', async () => {
    render(<Navbar />);
    const firstLink = screen.getAllByRole('link')[0];
    
    firstLink.focus();
    expect(firstLink).toHaveFocus();
  });
});
```

***

## Matriz de decisión

Usar esto para decidir qué tipo de componente crear:

| Pregunta                                       | Específico del proyecto | Candidato a UI2 |
| ---------------------------------------------- | ----------------------- | --------------- |
| ¿Otros proyectos usarán esto?                  | No                      | Sí              |
| ¿UI1 tiene un equivalente?                     | N/A                     | Probablemente   |
| ¿Necesita documentación en Storybook?          | No                      | **Sí**          |
| ¿Necesita pruebas exhaustivas?                 | Básicas                 | **Extensas**    |
| ¿Requiere revisión de diseño?                  | A nivel de proyecto     | **A nivel UI2** |
| ¿Puede usar patrones específicos del proyecto? | Sí                      | **No**          |
| ¿Debe funcionar en todos los temas?            | No                      | **Sí**          |

***

## Proceso de aprobación

### Componentes específicos del proyecto

1. Revisión de código por el mantenedor del proyecto
2. Verificar cumplimiento del tema
3. Probar en el contexto del proyecto
4. Merge cuando esté aprobado

### Componentes candidatos a UI2

1. Revisión y aprobación de diseño
2. Revisión técnica de diseño
3. Implementación
4. Historias de Storybook
5. Pruebas completas
6. Revisión de accesibilidad
7. Revisión de código
8. PR al repositorio UI2
9. Versionar y publicar
10. Actualizar proyectos dependientes

***

## Buenas prácticas

### Composición sobre personalización

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

// ❌ Malo: Re-implementar la funcionalidad de Card
function FeatureCard({ title, children }) {
  return (
    <div className="custom-card">
      <div className="custom-card-content">
        <h3>{title}</h3>
        {children}
      </div>
    </div>
  );
}
```

### Mejora progresiva

Comenzar simple y añadir funciones según sea necesario:

1. Versión básica con funcionalidad principal
2. Agregar comportamiento responsive
3. Agregar funciones de accesibilidad
4. Agregar interacciones avanzadas
5. Optimizar rendimiento

### Documentación primero

Antes de escribir código:

1. Escribir README del componente
2. Definir la interfaz de props
3. Listar los estados requeridos
4. Planear historias de Storybook
5. Luego implementar

***

## Siguientes pasos

* Revisar [Estándares de Styling & Theming](/contributor/contributor-es/guias-para-colaboradores/estandares-de-ui-web/styling-and-theming.md) para detalles de implementación
* Ver [Guía de migración](https://github.com/decentraland/docs/blob/main/contributor/contributor-guides/web-ui-standards/broken-reference/README.md) para migraciones de UI1 a UI2
* Comprobar [Resumen del proceso](/contributor/contributor-es/guias-para-colaboradores/estandares-de-ui-web/process-overview.md) para el flujo de trabajo 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-es/guias-para-colaboradores/estandares-de-ui-web/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.
