workspace/projects/gamilit/docs/95-guias-desarrollo/frontend/COMPONENTES-UI.md
rckrdmrd ea1879f4ad feat: Initial workspace structure with multi-level Git configuration
- Configure workspace Git repository with comprehensive .gitignore
- Add Odoo as submodule for ERP reference code
- Include documentation: SETUP.md, GIT-STRUCTURE.md
- Add gitignore templates for projects (backend, frontend, database)
- Structure supports independent repos per project/subproject level

Workspace includes:
- core/ - Reusable patterns, modules, orchestration system
- projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.)
- knowledge-base/ - Reference code and patterns (includes Odoo submodule)
- devtools/ - Development tools and templates
- customers/ - Client implementations template

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 10:44:23 -06:00

10 KiB

Guía de Componentes UI

Versión: 1.0.0 Última Actualización: 2025-11-28 Aplica a: apps/frontend/src/shared/components/ui/


Resumen

GAMILIT utiliza una librería de componentes UI propios construidos sobre Tailwind CSS y Radix UI primitives. Esta guía documenta los componentes disponibles y cómo utilizarlos.


Stack de UI

  • Tailwind CSS: Estilos utilitarios
  • Radix UI: Primitivos accesibles (Dialog, Dropdown, etc.)
  • CVA (class-variance-authority): Variantes de componentes
  • Lucide Icons: Iconografía
  • Sonner: Toasts/Notificaciones

Componentes Base

Button

import { Button } from '@/shared/components/ui';

// Variantes
<Button variant="primary">Primario</Button>
<Button variant="secondary">Secundario</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Peligro</Button>

// Tamaños
<Button size="sm">Pequeño</Button>
<Button size="md">Mediano</Button>
<Button size="lg">Grande</Button>

// Estados
<Button disabled>Deshabilitado</Button>
<Button isLoading>Cargando...</Button>

// Con icono
<Button>
  <PlusIcon className="w-4 h-4 mr-2" />
  Agregar
</Button>

Input

import { Input } from '@/shared/components/ui';

// Básico
<Input placeholder="Escribe aquí..." />

// Con label
<div>
  <label htmlFor="email">Email</label>
  <Input id="email" type="email" />
</div>

// Con error
<Input error="Este campo es requerido" />

// Variantes
<Input variant="filled" />
<Input variant="outlined" />

// Iconos
<Input leftIcon={<SearchIcon />} placeholder="Buscar..." />
<Input rightIcon={<EyeIcon />} type="password" />

Card

import { Card, CardHeader, CardBody, CardFooter } from '@/shared/components/ui';

<Card>
  <CardHeader>
    <h3>Título de la tarjeta</h3>
  </CardHeader>
  <CardBody>
    Contenido de la tarjeta...
  </CardBody>
  <CardFooter>
    <Button>Acción</Button>
  </CardFooter>
</Card>

// Variantes
<Card variant="elevated">Con sombra</Card>
<Card variant="outlined">Con borde</Card>
<Card variant="filled">Fondo sólido</Card>

// Interactiva
<Card isHoverable onClick={handleClick}>
  Tarjeta clickeable
</Card>

Badge

import { Badge } from '@/shared/components/ui';

// Colores
<Badge>Default</Badge>
<Badge color="success">Completado</Badge>
<Badge color="warning">Pendiente</Badge>
<Badge color="danger">Error</Badge>
<Badge color="info">Nuevo</Badge>

// Tamaños
<Badge size="sm">Pequeño</Badge>
<Badge size="md">Mediano</Badge>

// Con ícono
<Badge>
  <StarIcon className="w-3 h-3 mr-1" />
  Premium
</Badge>

Avatar

import { Avatar, AvatarGroup } from '@/shared/components/ui';

// Básico
<Avatar src={user.avatarUrl} alt={user.name} />

// Fallback con iniciales
<Avatar fallback="JD" />

// Tamaños
<Avatar size="sm" src={...} />  {/* 32px */}
<Avatar size="md" src={...} />  {/* 40px */}
<Avatar size="lg" src={...} />  {/* 56px */}

// Grupo de avatares
<AvatarGroup max={3}>
  <Avatar src={user1.avatar} />
  <Avatar src={user2.avatar} />
  <Avatar src={user3.avatar} />
  <Avatar src={user4.avatar} /> {/* +1 */}
</AvatarGroup>

Componentes de Formulario

Select

import { Select, SelectOption } from '@/shared/components/ui';

<Select
  label="Categoría"
  value={selected}
  onChange={setSelected}
  placeholder="Selecciona una opción"
>
  <SelectOption value="1">Opción 1</SelectOption>
  <SelectOption value="2">Opción 2</SelectOption>
  <SelectOption value="3">Opción 3</SelectOption>
</Select>

Checkbox

import { Checkbox } from '@/shared/components/ui';

<Checkbox
  checked={isChecked}
  onChange={setIsChecked}
  label="Acepto los términos"
/>

// Sin label
<Checkbox checked={...} onChange={...} />

Switch

import { Switch } from '@/shared/components/ui';

<Switch
  checked={isEnabled}
  onChange={setIsEnabled}
  label="Notificaciones"
/>

Form Field

import { FormField, Input, FormError, FormHelper } from '@/shared/components/ui';

<FormField>
  <label>Email</label>
  <Input type="email" {...register('email')} />
  <FormHelper>Usaremos este email para contactarte</FormHelper>
  {errors.email && <FormError>{errors.email.message}</FormError>}
</FormField>

Componentes de Feedback

Modal / Dialog

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/shared/components/ui';

const [isOpen, setIsOpen] = useState(false);

<Button onClick={() => setIsOpen(true)}>Abrir Modal</Button>

<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
  <ModalHeader>
    <h2>Confirmar acción</h2>
  </ModalHeader>
  <ModalBody>
    ¿Estás seguro de que deseas continuar?
  </ModalBody>
  <ModalFooter>
    <Button variant="outline" onClick={() => setIsOpen(false)}>
      Cancelar
    </Button>
    <Button onClick={handleConfirm}>
      Confirmar
    </Button>
  </ModalFooter>
</Modal>

Toast

import { toast } from 'sonner';

// Éxito
toast.success('Guardado correctamente');

// Error
toast.error('Ocurrió un error');

// Info
toast.info('Nueva notificación');

// Con acción
toast('Archivo eliminado', {
  action: {
    label: 'Deshacer',
    onClick: () => undoDelete(),
  },
});

// Promesa
toast.promise(saveData(), {
  loading: 'Guardando...',
  success: 'Guardado',
  error: 'Error al guardar',
});

LoadingSpinner

import { LoadingSpinner } from '@/shared/components/ui';

// Básico
<LoadingSpinner />

// Tamaños
<LoadingSpinner size="sm" />
<LoadingSpinner size="md" />
<LoadingSpinner size="lg" />

// Centrado en página
<div className="flex items-center justify-center h-screen">
  <LoadingSpinner size="lg" />
</div>

EmptyState

import { EmptyState } from '@/shared/components/ui';

<EmptyState
  icon={<InboxIcon className="w-12 h-12" />}
  title="No hay resultados"
  description="No encontramos ejercicios que coincidan con tu búsqueda"
  action={
    <Button onClick={clearFilters}>Limpiar filtros</Button>
  }
/>

Componentes de Navegación

Tabs

import { Tabs, TabList, Tab, TabPanel } from '@/shared/components/ui';

<Tabs defaultValue="tab1">
  <TabList>
    <Tab value="tab1">General</Tab>
    <Tab value="tab2">Avanzado</Tab>
    <Tab value="tab3">Configuración</Tab>
  </TabList>

  <TabPanel value="tab1">
    Contenido de General
  </TabPanel>
  <TabPanel value="tab2">
    Contenido de Avanzado
  </TabPanel>
  <TabPanel value="tab3">
    Contenido de Configuración
  </TabPanel>
</Tabs>

Dropdown

import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '@/shared/components/ui';

<Dropdown>
  <DropdownTrigger>
    <Button variant="outline">
      Opciones <ChevronDownIcon />
    </Button>
  </DropdownTrigger>
  <DropdownContent>
    <DropdownItem onClick={handleEdit}>Editar</DropdownItem>
    <DropdownItem onClick={handleDuplicate}>Duplicar</DropdownItem>
    <DropdownItem onClick={handleDelete} variant="danger">
      Eliminar
    </DropdownItem>
  </DropdownContent>
</Dropdown>

Pagination

import { Pagination } from '@/shared/components/ui';

<Pagination
  currentPage={page}
  totalPages={totalPages}
  onPageChange={setPage}
/>

Componentes de Datos

Table

import { Table, TableHead, TableBody, TableRow, TableCell } from '@/shared/components/ui';

<Table>
  <TableHead>
    <TableRow>
      <TableCell>Nombre</TableCell>
      <TableCell>Email</TableCell>
      <TableCell>Rol</TableCell>
      <TableCell align="right">Acciones</TableCell>
    </TableRow>
  </TableHead>
  <TableBody>
    {users.map((user) => (
      <TableRow key={user.id}>
        <TableCell>{user.name}</TableCell>
        <TableCell>{user.email}</TableCell>
        <TableCell><Badge>{user.role}</Badge></TableCell>
        <TableCell align="right">
          <Button size="sm" variant="ghost">Editar</Button>
        </TableCell>
      </TableRow>
    ))}
  </TableBody>
</Table>

Progress Bar

import { Progress } from '@/shared/components/ui';

// Básico
<Progress value={75} />

// Con label
<Progress value={75} showLabel />

// Colores
<Progress value={100} color="success" />
<Progress value={50} color="warning" />
<Progress value={20} color="danger" />

// XP Progress (específico de GAMILIT)
<XpProgressBar
  currentXp={450}
  maxXp={500}
  level={2}
/>

Componentes de GAMILIT

RankBadge

import { RankBadge } from '@/features/gamification';

<RankBadge rank={userRank} />
<RankBadge rank={userRank} size="lg" showName />

MlCoinsDisplay

import { MlCoinsDisplay } from '@/features/gamification';

<MlCoinsDisplay coins={250} />
<MlCoinsDisplay coins={250} animated /> // Animación al cambiar

AchievementCard

import { AchievementCard } from '@/features/gamification';

<AchievementCard
  achievement={achievement}
  userProgress={userAchievement}
  onClaim={handleClaim}
/>

Theming

Variables CSS

/* shared/styles/variables.css */
:root {
  --color-primary: #3b82f6;
  --color-primary-dark: #2563eb;
  --color-secondary: #8b5cf6;
  --color-success: #22c55e;
  --color-warning: #f59e0b;
  --color-danger: #ef4444;

  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
}

Tailwind Config

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: 'var(--color-primary)',
        'primary-dark': 'var(--color-primary-dark)',
        secondary: 'var(--color-secondary)',
      },
    },
  },
};

Buenas Prácticas

  1. Usar componentes base: No reinventar la rueda
  2. Props consistentes: Mismos nombres en todos los componentes
  3. Variantes con CVA: Para componentes con múltiples estilos
  4. Accesibilidad: Usar ARIA labels y keyboard navigation
  5. Composición: Preferir composición sobre props complejas
  6. Documentar: Storybook para documentación visual

Ver También