31 KiB
Frontend de Quasar 🎮
Visión general
El frontend de Quasar es una Single Page Application (SPA) moderna construida con React + Vite, utilizando shadcn/ui para componentes UI y Tailwind CSS para estilos. La aplicación está diseñada con una estética de modo oscuro sofisticada, priorizando la accesibilidad, la responsividad y un excelente rendimiento.
Stack tecnológico
| Categoría | Tecnología | Versión | Propósito |
|---|---|---|---|
| Framework | React | ^18.3.0 | Framework base |
| Build tool | Vite | ^5.0.0 | Build tool y dev server |
| Lenguaje | TypeScript | ^5.3.0 | Type safety |
| Estado | TanStack Query | ^5.0.0 | Gestión de estado y cache de datos |
| Routing | TanStack Router | ^1.0.0 | Routing con carga de datos |
| Formularios | TanStack Form | ^0.0.0 | Manejo de formularios |
| Validación | Zod | ^3.0.0 | Validación de esquemas |
| Componentes UI | shadcn/ui | latest | Componentes UI reutilizables |
| Estilos | Tailwind CSS | ^3.4.0 | Framework de estilos |
| Iconos | lucide-react | ^0.300.0 | Iconos |
| HTTP | axios | latest | Cliente HTTP |
| Fechas | date-fns | latest | Manipulación de fechas |
| Utils | clsx | ^2.0.0 | Utilidad de clases condicionales |
| Utils | tailwind-merge | ^2.0.0 | Fusión de clases Tailwind |
Arquitectura del sistema
Diagrama de arquitectura
graph TB
subgraph Browser
UI[Componentes UI]
Router[TanStack Router]
Query[TanStack Query]
Forms[TanStack Form + Zod]
end
subgraph Services
API[API Client]
Auth[Auth Service]
end
UI --> Router
UI --> Query
UI --> Forms
Router --> Query
Query --> API
Forms --> Query
API --> Backend[Backend API]
style UI fill:#1e293b
style Router fill:#334155
style Query fill:#334155
style Forms fill:#334155
style API fill:#475569
style Backend fill:#64748b
Estructura de carpetas
frontend/
├── src/
│ ├── components/
│ │ ├── ui/ # Componentes shadcn/ui
│ │ ├── layout/ # Componentes de layout
│ │ ├── games/ # Componentes específicos de juegos
│ │ ├── roms/ # Componentes específicos de ROMs
│ │ ├── shared/ # Componentes compartidos
│ │ └── ... # Otros componentes específicos
│ ├── pages/ # Páginas de la aplicación
│ │ ├── DashboardPage.tsx
│ │ ├── GamesPage.tsx
│ │ ├── GameDetailPage.tsx
│ │ ├── GamesNewPage.tsx
│ │ ├── ImportPage.tsx
│ │ ├── PlatformsPage.tsx
│ │ ├── TagsPage.tsx
│ │ ├── SettingsPage.tsx
│ │ └── ExportPage.tsx
│ ├── api/ # Servicios de API y tipos
│ │ ├── client.ts # Cliente HTTP base
│ │ ├── games.ts # Servicios de juegos
│ │ ├── platforms.ts # Servicios de plataformas
│ │ ├── tags.ts # Servicios de etiquetas
│ │ ├── import.ts # Servicios de importación
│ │ ├── settings.ts # Servicios de configuración
│ │ └── types.ts # Tipos de API
│ ├── query/ # Configuración de TanStack Query
│ │ └── client.tsx # Cliente de Query
│ ├── form/ # Configuración de TanStack Form
│ │ └── config.tsx # Configuración de Form + Zod
│ ├── router/ # Configuración de TanStack Router
│ │ └── router.tsx # Configuración del router
│ ├── types/ # Definiciones TypeScript
│ │ └── index.ts # Tipos globales
│ ├── hooks/ # Custom hooks
│ ├── lib/ # Utilidades y configuraciones
│ │ ├── utils.ts # Utilidades generales
│ │ └── ... # Otras utilidades
│ ├── styles/ # Estilos globales
│ │ └── globals.css # CSS global con variables
│ └── layout/ # Componentes de layout
│ ├── Header.tsx # Header principal
│ ├── Sidebar.tsx # Sidebar de navegación
│ └── Layout.tsx # Layout principal
├── public/ # Assets estáticos
├── index.html # HTML entry point
├── package.json # Dependencias y scripts
├── tsconfig.json # Configuración TypeScript
├── vite.config.ts # Configuración Vite
├── tailwind.config.ts # Configuración Tailwind
└── postcss.config.js # Configuración PostCSS
Sistema de diseño
Principios de diseño
- Modo oscuro por defecto: La aplicación está diseñada para funcionar principalmente en modo oscuro
- Alto contraste: Asegurar legibilidad excelente en todos los contextos
- Consistencia: Componentes y patrones consistentes en toda la aplicación
- Accesibilidad: Cumplimiento con WCAG AA
- Performance: Componentes optimizados para rendimiento
- Responsividad: Mobile-first con breakpoints claros
Paleta de colores (Modo oscuro)
Colores semánticos
/* Primary - Violet (Acción principal) */
--primary-50: #f5f3ff;
--primary-100: #ede9fe;
--primary-200: #ddd6fe;
--primary-300: #c4b5fd;
--primary-400: #a78bfa;
--primary-500: #8b5cf6;
--primary-600: #7c3aed; /* Color principal */
--primary-700: #6d28d9;
--primary-800: #5b21b6;
--primary-900: #4c1d95;
--primary-950: #2e1065;
/* Secondary - Slate (Acciones secundarias) */
--secondary-50: #f8fafc;
--secondary-100: #f1f5f9;
--secondary-200: #e2e8f0;
--secondary-300: #cbd5e1;
--secondary-400: #94a3b8;
--secondary-500: #64748b;
--secondary-600: #475569;
--secondary-700: #334155;
--secondary-800: #1e293b;
--secondary-900: #0f172a;
--secondary-950: #020617;
/* Success - Emerald (Estados positivos) */
--success-50: #ecfdf5;
--success-100: #d1fae5;
--success-200: #a7f3d0;
--success-300: #6ee7b7;
--success-400: #34d399;
--success-500: #10b981;
--success-600: #059669;
--success-700: #047857;
--success-800: #065f46;
--success-900: #064e3b;
--success-950: #022c22;
/* Warning - Amber (Estados de advertencia) */
--warning-50: #fffbeb;
--warning-100: #fef3c7;
--warning-200: #fde68a;
--warning-300: #fcd34d;
--warning-400: #fbbf24;
--warning-500: #f59e0b;
--warning-600: #d97706;
--warning-700: #b45309;
--warning-800: #92400e;
--warning-900: #78350f;
--warning-950: #451a03;
/* Error - Red (Estados de error) */
--error-50: #fef2f2;
--error-100: #fee2e2;
--error-200: #fecaca;
--error-300: #fca5a5;
--error-400: #f87171;
--error-500: #ef4444;
--error-600: #dc2626;
--error-700: #b91c1c;
--error-800: #991b1b;
--error-900: #7f1d1d;
--error-950: #450a0a;
/* Info - Sky (Información) */
--info-50: #f0f9ff;
--info-100: #e0f2fe;
--info-200: #bae6fd;
--info-300: #7dd3fc;
--info-400: #38bdf8;
--info-500: #0ea5e9;
--info-600: #0284c7;
--info-700: #0369a1;
--info-800: #075985;
--info-900: #0c4a6e;
--info-950: #082f49;
Uso de colores
| Propósito | Color | Hex |
|---|---|---|
| Background | Slate-950 | #020617 |
| Card | Slate-950 | #020617 |
| Card hover | Slate-900 | #0f172a |
| Border | Slate-800 | #1e293b |
| Primary action | Violet-600 | #7c3aed |
| Primary hover | Violet-700 | #6d28d9 |
| Text primary | Slate-50 | #f8fafc |
| Text secondary | Slate-400 | #94a3b8 |
| Text muted | Slate-500 | #64748b |
Tipografía
Font family
font-family:
'Inter',
system-ui,
-apple-system,
sans-serif;
Escala de tamaños
| Token | Tamaño | Line-height | Uso |
|---|---|---|---|
| text-xs | 12px | 16px | Labels pequeños, captions |
| text-sm | 14px | 20px | Texto secundario |
| text-base | 16px | 24px | Texto de cuerpo |
| text-lg | 18px | 28px | Texto de cuerpo grande |
| text-xl | 20px | 28px | Subtítulos |
| text-2xl | 24px | 32px | Títulos pequeños |
| text-3xl | 30px | 40px | Títulos medianos |
| text-4xl | 36px | 44px | Títulos grandes |
| text-5xl | 48px | 56px | Display |
Font weights
| Token | Weight | Uso |
|---|---|---|
| font-normal | 400 | Texto de cuerpo |
| font-medium | 500 | Texto de énfasis |
| font-semibold | 600 | Subtítulos |
| font-bold | 700 | Títulos |
Espaciado
Escala de espaciado (base 4px)
| Token | Valor | Uso |
|---|---|---|
| p-0, m-0 | 0px | Sin espaciado |
| p-1, m-1 | 4px | Micro espaciado |
| p-2, m-2 | 8px | Espaciado pequeño |
| p-3, m-3 | 12px | Espaciado medio-pequeño |
| p-4, m-4 | 16px | Espaciado medio |
| p-5, m-5 | 20px | Espaciado medio-grande |
| p-6, m-6 | 24px | Espaciado grande |
| p-8, m-8 | 32px | Espaciado extra grande |
| p-10, m-10 | 40px | Espaciado extra extra grande |
| p-12, m-12 | 48px | Espaciado masivo |
Rutas de la aplicación
Diagrama de rutas
graph LR
A[/] --> B[Dashboard]
C[/games] --> D[Lista de juegos]
E[/games/:id] --> F[Detalle de juego]
G[/games/new] --> H[Crear juego]
I[/import] --> J[Importar ROMs]
K[/platforms] --> L[Gestión de plataformas]
M[/tags] --> N[Gestión de etiquetas]
O[/settings] --> P[Configuración]
Q[/export] --> R[Exportar/Importar]
style A fill:#7c3aed
style C fill:#7c3aed
style E fill:#7c3aed
style G fill:#7c3aed
style I fill:#7c3aed
style K fill:#7c3aed
style M fill:#7c3aed
style O fill:#7c3aed
style Q fill:#7c3aed
Descripción de rutas
| Ruta | Componente | Descripción |
|---|---|---|
/ |
Dashboard | Vista general con estadísticas y juegos recientes |
/games |
GamesPage | Lista paginada de juegos con filtros y búsqueda |
/games/:id |
GameDetailPage | Vista detallada de un juego con metadata, ROMs, compras |
/games/new |
GamesNewPage | Formulario para crear un nuevo juego |
/import |
ImportPage | Interfaz para escanear e importar ROMs locales |
/platforms |
PlatformsPage | Gestión de plataformas |
/tags |
TagsPage | Gestión de etiquetas |
/settings |
SettingsPage | Configuración de la aplicación |
/export |
ExportPage | Exportar/importar colección |
Componentes UI (shadcn/ui)
Componentes base
| Componente | Uso principal |
|---|---|
| Button | Acciones principales y secundarias |
| Card | Contenedores de contenido |
| Input | Entradas de texto |
| Select | Selección de opciones |
| Dialog | Modales y diálogos |
| Table | Tablas de datos |
| Badge | Etiquetas y estados |
| Avatar | Avatares de usuario |
| DropdownMenu | Menús desplegables |
| Tabs | Navegación por pestañas |
| Form | Formularios |
| Separator | Separadores visuales |
| ScrollArea | Áreas con scroll |
| Tooltip | Información contextual |
| Skeleton | Estados de carga |
| AlertDialog | Confirmaciones |
| Toast | Notificaciones |
Variantes y tamaños
Button
Variantes:
default: Acción principal (violet)destructive: Acción destructiva (rojo)outline: Acción secundaria con bordesecondary: Acción secundaria (slate)ghost: Acción sutil sin bordelink: Estilo de enlace
Tamaños:
sm: 32px de alturadefault: 40px de alturalg: 48px de alturaicon: 32px x 32px (cuadrado)
Card
<Card>
<CardHeader>
<CardTitle>Título</CardTitle>
<CardDescription>Descripción opcional</CardDescription>
</CardHeader>
<CardContent>Contenido principal</CardContent>
<CardFooter>Acciones</CardFooter>
</Card>
Badge
Variantes:
default: Violetsecondary: Slatedestructive: Rojooutline: Borde solosuccess: Verdewarning: Amarillo
Componentes específicos
Componentes de Juegos
GameCard
Componente de tarjeta para mostrar un juego en una lista o grid.
interface GameCardProps {
game: Game;
onClick?: () => void;
onEdit?: () => void;
onDelete?: () => void;
}
Estructura:
<Card className="group hover:border-primary/50 transition-colors cursor-pointer">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<GameCover gameId={game.id} className="h-16 w-12 rounded" />
<div className="min-w-0">
<CardTitle className="line-clamp-1">{game.title}</CardTitle>
<CardDescription className="line-clamp-1">
{game.platforms.map((p) => p.name).join(', ')}
</CardDescription>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="opacity-0 group-hover:opacity-100">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={onEdit}>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onDelete} className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Eliminar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className="pb-3">
<div className="flex flex-wrap gap-2">
{game.tags.slice(0, 3).map((tag) => (
<Badge key={tag.id} variant="secondary" className="text-xs">
{tag.name}
</Badge>
))}
{game.tags.length > 3 && (
<Badge variant="secondary" className="text-xs">
+{game.tags.length - 3}
</Badge>
)}
</div>
</CardContent>
<CardFooter className="pt-0">
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<HardDrive className="h-3 w-3" />
{game.romFiles.length} ROMs
</div>
{game.releaseDate && (
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{formatDate(game.releaseDate)}
</div>
)}
</div>
</CardFooter>
</Card>
GameList
Componente de lista de juegos con filtros y paginación.
interface GameListProps {
filters?: GameFilters;
onFiltersChange?: (filters: GameFilters) => void;
}
Estructura:
<div className="space-y-6">
<GameFilters filters={filters} onChange={onFiltersChange} />
<GameGrid games={games} isLoading={isLoading} />
<GamePagination pagination={pagination} onPageChange={onPageChange} />
</div>
GameDetail
Página de detalle de un juego.
<div className="space-y-6">
<GameDetailHeader game={game} />
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Resumen</TabsTrigger>
<TabsTrigger value="roms">ROMs</TabsTrigger>
<TabsTrigger value="purchases">Compras</TabsTrigger>
<TabsTrigger value="metadata">Metadata</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<GameOverview game={game} />
</TabsContent>
<TabsContent value="roms">
<GameRoms game={game} />
</TabsContent>
<TabsContent value="purchases">
<GamePurchases game={game} />
</TabsContent>
<TabsContent value="metadata">
<GameMetadata game={game} />
</TabsContent>
</Tabs>
</div>
Componentes de ROMs
RomFileCard
Componente de tarjeta para mostrar un archivo ROM.
interface RomFileCardProps {
rom: RomFile;
game?: Game;
onLink?: () => void;
onUnlink?: () => void;
}
ImportRomWizard
Wizard para importar ROMs desde el sistema de archivos.
Pasos del wizard:
- Configurar rutas: Seleccionar directorios para escanear
- Escanear: Ejecutar escaneo de archivos
- Revisar: Revisar resultados y detectar duplicados
- Importar: Importar ROMs seleccionadas
Componentes de Metadata
MetadataSearch
Componente para buscar metadata de APIs externas (IGDB, RAWG).
interface MetadataSearchProps {
onSelect: (metadata: ExternalMetadata) => void;
game?: Game;
}
Estructura:
<div className="space-y-4">
<div className="flex gap-2">
<Input
placeholder="Buscar en IGDB, RAWG..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<Button onClick={handleSearch}>
<Search className="mr-2 h-4 w-4" />
Buscar
</Button>
</div>
{isLoading && <MetadataSearchResultsSkeleton />}
{results && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{results.map((result) => (
<MetadataResultCard key={result.id} result={result} onSelect={() => onSelect(result)} />
))}
</div>
)}
</div>
Componentes de Dashboard
DashboardStats
Tarjetas de estadísticas del dashboard.
interface DashboardStatsProps {
stats: {
totalGames: number;
totalRoms: number;
totalPlatforms: number;
recentGames: number;
};
}
Estructura:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
title="Total de juegos"
value={stats.totalGames}
icon={<Gamepad2 className="h-4 w-4" />}
/>
<StatCard
title="Total de ROMs"
value={stats.totalRoms}
icon={<HardDrive className="h-4 w-4" />}
/>
<StatCard
title="Plataformas"
value={stats.totalPlatforms}
icon={<Monitor className="h-4 w-4" />}
/>
<StatCard
title="Juegos recientes"
value={stats.recentGames}
icon={<Clock className="h-4 w-4" />}
/>
</div>
Layout Components
AppLayout
Layout principal de la aplicación.
interface AppLayoutProps {
children: React.ReactNode;
}
Estructura:
<div className="min-h-screen bg-background">
<Header />
<div className="flex">
<Sidebar />
<main className="flex-1 p-6">{children}</main>
</div>
</div>
Header
Header de la aplicación.
interface HeaderProps {
onMenuToggle?: () => void;
}
Estructura:
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur">
<div className="container flex h-14 items-center gap-4">
<Logo />
<GlobalSearch />
<div className="ml-auto flex items-center gap-2">
<ThemeToggle />
<UserMenu />
</div>
</div>
</header>
Sidebar
Sidebar de navegación.
interface SidebarProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
Estructura:
<aside className="fixed inset-y-0 left-0 z-40 w-64 border-r border-border bg-background">
<div className="flex h-full flex-col">
<SidebarHeader />
<SidebarNav />
<SidebarFooter />
</div>
</aside>
Gestión de estado
TanStack Query
- Cache de datos: Cache automático de respuestas de API
- Invalidación: Invalidación inteligente de cache
- Optimistic updates: Actualizaciones optimistas
- Background refetch: Refetch en background
// Ejemplo de query
const gamesQuery = useQuery({
queryKey: ['games', { page, limit, search }],
queryFn: () => fetchGames({ page, limit, search }),
staleTime: 5 * 60 * 1000, // 5 minutos
});
// Ejemplo de mutation
const createGameMutation = useMutation({
mutationFn: createGame,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['games'] });
},
});
TanStack Router
- File-based routing: Rutas basadas en archivos
- Route loaders: Carga de datos antes de renderizar
- Type-safe routing: Tipado de parámetros y búsqueda
- Code splitting: División de código automática
// Ejemplo de route loader
export const loader = ({ params }: LoaderArgs) => {
return queryClient.ensureQueryData({
queryKey: ['game', params.id],
queryFn: () => fetchGame(params.id),
});
};
TanStack Form + Zod
// Esquema de validación
const gameSchema = z.object({
title: z.string().min(1, 'El título es requerido'),
description: z.string().optional(),
releaseDate: z.date().optional(),
platforms: z.array(z.string()).min(1, 'Selecciona al menos una plataforma'),
tags: z.array(z.string()).default([]),
})
type GameFormData = z.infer<typeof gameSchema>
// Componente de formulario
function GameForm() {
const form = useForm({
defaultValues: {
title: '',
description: '',
platforms: [],
tags: [],
},
validators: {
onChange: gameSchema,
},
})
const { mutate: createGame } = useCreateGame()
const handleSubmit = (data: GameFormData) => {
createGame(data)
}
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field
name="title"
validators={{
onChange: ({ value }) =>
!value ? 'El título es requerido' : undefined,
}}
>
{(field) => (
<div>
<label htmlFor={field.name}>Título</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors && (
<span>{field.state.meta.errors.join(', ')}</span>
)}
</div>
)}
</form.Field>
<button type="submit">Guardar</button>
</form>
)
}
API Client
// API client base
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
timeout: 10000,
});
// Interceptor para manejo de errores
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Redirigir a login
}
return Promise.reject(error);
}
);
// Servicios de API
export const gamesApi = {
list: (params: GamesListParams) => apiClient.get('/games', { params }),
get: (id: string) => apiClient.get(`/games/${id}`),
create: (data: CreateGameDto) => apiClient.post('/games', data),
update: (id: string, data: UpdateGameDto) => apiClient.patch(`/games/${id}`, data),
delete: (id: string) => apiClient.delete(`/games/${id}`),
};
Accesibilidad
WCAG AA Compliance
- Contraste: Mínimo 4.5:1 para texto normal, 3:1 para texto grande
- Keyboard navigation: Navegación completa por teclado
- Focus indicators: Indicadores visuales de foco
- Screen reader support: ARIA labels y roles apropiados
- Semantic HTML: Uso correcto de elementos semánticos
Implementación
// Ejemplo de componente accesible
<Button
aria-label="Crear nuevo juego"
type="button"
onClick={handleCreate}
>
<Plus className="mr-2 h-4 w-4" />
Nuevo juego
</Button>
Responsividad
Breakpoints
/* Tailwind breakpoints */
sm: 640px /* Móvil grande */
md: 768px /* Tablet */
lg: 1024px /* Desktop pequeño */
xl: 1280px /* Desktop */
2xl: 1536px /* Desktop grande */
Estrategia mobile-first
// Ejemplo de componente responsivo
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{games.map((game) => (
<GameCard key={game.id} game={game} />
))}
</div>
Optimizaciones
Performance
- Code splitting: División de código por rutas
- Lazy loading: Carga diferida de componentes
- Image optimization: Optimización de imágenes
- Memoization: Uso de React.memo y useMemo
- Virtual scrolling: Para listas largas
Bundle size
- Tree shaking: Eliminación de código no usado
- Compression: Gzip/Brotli compression
- CDN: Uso de CDN para dependencias
Configuración
Scripts de desarrollo
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
"type-check": "tsc --noEmit",
"test": "vitest",
"test:ui": "vitest --ui",
"test:e2e": "playwright test"
}
}
Variables de entorno
# API
VITE_API_URL=http://localhost:3000
# Feature flags
VITE_ENABLE_ANALYTICS=false
VITE_ENABLE_TELEMETRY=false
Configuración de Tailwind
import type { Config } from 'tailwindcss';
const config: Config = {
darkMode: ['class'],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
},
},
plugins: [],
};
export default config;
Configuración de Vite
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
});
Testing
Estrategia de testing
- Unit tests: Componentes y hooks individuales
- Integration tests: Flujos de usuario
- E2E tests: Flujos completos con Playwright
Herramientas
- Vitest: Unit tests
- Testing Library: Testing de componentes React
- Playwright: E2E tests
Patrones de diseño
Page Layout
<div className="min-h-screen bg-background">
<Header />
<div className="flex">
<Sidebar />
<main className="flex-1 p-6">
<PageHeader />
<PageContent />
</main>
</div>
</div>
Card Grid
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{items.map((item) => (
<Card key={item.id}>{/* Card content */}</Card>
))}
</div>
Form Layout
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Título</FormLabel>
<FormControl>
<Input placeholder="Escribe el título..." {...field} />
</FormControl>
<FormDescription>Descripción del campo</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit">Guardar</Button>
</div>
</form>
</Form>
Estados de carga
Skeleton Loading
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<Card key={i}>
<CardHeader>
<Skeleton className="h-4 w-3/4" />
</CardHeader>
<CardContent>
<Skeleton className="h-20 w-full" />
</CardContent>
</Card>
))}
</div>
) : (
// Actual content
)}
Empty State
<div className="flex flex-col items-center justify-center py-12">
<div className="rounded-full bg-muted p-4">
<Inbox className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="mt-4 text-lg font-semibold">No hay elementos</h3>
<p className="mt-2 text-sm text-muted-foreground">Comienza añadiendo tu primer elemento.</p>
<Button className="mt-4">
<Plus className="mr-2 h-4 w-4" />
Añadir elemento
</Button>
</div>
Iconos
Se utiliza lucide-react para todos los iconos. Iconos comunes:
| Icono | Uso |
|---|---|
Home |
Dashboard |
Gamepad2 |
Juegos |
Upload |
Importar |
Settings |
Configuración |
Monitor |
Plataformas |
Tag |
Etiquetas |
Download |
Exportar |
Search |
Búsqueda |
Plus |
Crear nuevo |
Edit |
Editar |
Trash2 |
Eliminar |
Filter |
Filtros |
SortAsc |
Ordenar |
ChevronRight |
Navegación |
Menu |
Menú móvil |
X |
Cerrar |
Check |
Confirmar |
AlertCircle |
Advertencia |
Info |
Información |
Loader2 |
Carga |
Implementación
Prerrequisitos
- Node.js 18+
- Yarn 4.x (package manager)
Instalación
# Navegar al directorio frontend
cd frontend
# Instalar dependencias
yarn install
# Iniciar desarrollo
yarn dev
Build para producción
# Build
yarn build
# Preview
yarn preview
Metadatos
Autor: Quasar Frontend Team Última actualización: 2026-02-22 Versión: 1.0.0 Estado: Implementación completa en progreso