Componentes UI
Sección titulada «Componentes UI»TalentBricksAI usa ShadCN UI (estilo New York) con Radix UI primitives.
Ubicacion
Sección titulada «Ubicacion»Los componentes estan en app/src/client/components/ui/.
Componentes Disponibles
Sección titulada «Componentes Disponibles»Formularios
Sección titulada «Formularios»| Componente | Archivo | descripción |
|---|---|---|
| Button | button.tsx | Botones con variantes |
| Input | input.tsx | Campo de texto |
| Textarea | textarea.tsx | Área de texto |
| Select | select.tsx | Selector dropdown |
| Checkbox | checkbox.tsx | Casilla de verificación |
| Switch | switch.tsx | Toggle switch |
| Label | label.tsx | Etiqueta para inputs |
| Form | form.tsx | Wrapper de formulario |
| Componente | Archivo | descripción |
|---|---|---|
| Card | card.tsx | Contenedor con borde |
| Dialog | dialog.tsx | Modal dialog |
| Sheet | sheet.tsx | Panel deslizante |
| Separator | separator.tsx | línea divisora |
| Tabs | tabs.tsx | navegación por tabs |
Feedback
Sección titulada «Feedback»| Componente | Archivo | descripción |
|---|---|---|
| Alert | alert.tsx | Mensaje de alerta |
| Toast | toaster.tsx | Notificaciones toast |
| Progress | progress.tsx | Barra de progreso |
| Skeleton | skeleton.tsx | Placeholder de carga |
navegación
Sección titulada «navegación»| Componente | Archivo | descripción |
|---|---|---|
| DropdownMenu | dropdown-menu.tsx | Menu desplegable |
| NavigationMenu | navigation-menu.tsx | Menu de navegación |
| Breadcrumb | Breadcrumb.tsx | Migas de pan (custom, no ShadCN) |
| PageHeader | PageHeader.tsx | Encabezado de página con breadcrumb |
Layouts
Sección titulada «Layouts»| Componente | Archivo | descripción |
|---|---|---|
| AccountLayout | user/layout/AccountLayout.tsx | Layout para páginas de cuenta |
| AccountSidebar | user/layout/AccountSidebar.tsx | Sidebar de navegación de cuenta |
| Componente | Archivo | descripción |
|---|---|---|
| Table | table.tsx | Tabla de datos |
| Avatar | avatar.tsx | Avatar de usuario |
| Badge | badge.tsx | Etiqueta/badge |
Componentes Globales
Sección titulada «Componentes Globales»| Componente | Archivo | descripción |
|---|---|---|
| ScrollToTopButton | ScrollToTopButton.tsx | Botón flotante para volver al inicio de la página |
ScrollToTopButton
Sección titulada «ScrollToTopButton»Botón global que aparece automáticamente después de hacer scroll hacia abajo 300px. Regresa la página al inicio con animación suave.
Características:
- Aparece/desaparece con fade + slide transition (300ms)
- Posicionamiento responsive (móvil: 16px, desktop: 24px desde bordes)
- Tooltip bilingüe (“Volver arriba” / “Scroll to top”)
- Accesible con teclado (Tab + Enter/Space)
- Excluido de admin dashboard y páginas de login/signup
- Scroll throttled para performance (50ms)
Uso:
// Ya integrado globalmente en App.tsx// No requiere importación manual - aparece automáticamente en todas las páginasProps: Ninguna - componente autocontenido
Ubicación: Renderizado en App.tsx junto a Toaster y CookieConsentBanner
La aplicación usa una estrategia de dos librerías para íconos:
| Librería | Uso | Ubicación |
|---|---|---|
| Lucide React | Íconos de UI (65+ archivos) | import { Eye } from 'lucide-react' |
| Iconify | Logos de marca únicamente | app/src/client/components/icons/ |
Componentes disponibles:
| Componente | Archivo | Descripción |
|---|---|---|
| Icon | Icon.tsx | Wrapper genérico para Iconify |
| BrandIcon | BrandIcon.tsx | Logos de marca con soporte dark mode |
Uso de Lucide React (preferido para UI):
import { Eye, EyeOff, Github, Menu, X } from "lucide-react";
<Eye className="w-5 h-5" /><EyeOff className="w-5 h-5 text-gray-500" /><Github className="w-6 h-6" />Uso de Iconify (solo para logos de marca):
import { Icon } from "../client/components/icons";
// OAuth logo<Icon name="logos:google-icon" size={20} />
// Tecnología logo<Icon name="simple-icons:openai" size={48} />Uso de BrandIcon (logos con dark mode):
import { BrandIcon } from "../client/components/icons";
// Logo monocromo con inversión en dark mode<BrandIcon name="logos:prisma" size={48} darkModeInvert />
// Logo colorido sin inversión<BrandIcon name="logos:google-icon" size={20} />Colecciones Iconify disponibles:
logos:*- Logos de marca (Google, Prisma, Astro, etc.)simple-icons:*- Íconos simples de marca (OpenAI, etc.)
Convenciones de tamaño:
- Botones OAuth: 20px
- Toggles de visibilidad: 20px (w-5 h-5)
- Logos de tecnología: 48px
Ver documentación completa en app/src/client/components/icons/README.md
Ejemplos de Uso
Sección titulada «Ejemplos de Uso»import { Button } from '../client/components/ui/button';
// Variantes<Button variant="default">Primario</Button><Button variant="secondary">Secundario</Button><Button variant="destructive">Eliminar</Button><Button variant="outline">Contorno</Button><Button variant="ghost">Ghost</Button><Button variant="link">Link</Button>
// Tamanos<Button size="sm">Pequeno</Button><Button size="default">Normal</Button><Button size="lg">Grande</Button>
// Estados<Button disabled>Deshabilitado</Button><Button loading>Cargando...</Button>import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,} from "../client/components/ui/card";
<Card> <CardHeader> <CardTitle>Título del Card</CardTitle> <CardDescription>descripción opcional</CardDescription> </CardHeader> <CardContent> <p>Contenido principal aqui</p> </CardContent> <CardFooter> <Button>Accion</Button> </CardFooter></Card>;import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,} from "../client/components/ui/dialog";
<Dialog> <DialogTrigger asChild> <Button>Abrir Dialog</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Título</DialogTitle> <DialogDescription>descripción del dialog</DialogDescription> </DialogHeader> <div>Contenido...</div> <DialogFooter> <Button>Confirmar</Button> </DialogFooter> </DialogContent></Dialog>;ForcePasswordChangeModal
Sección titulada «ForcePasswordChangeModal»Modal no-cerrable que bloquea el acceso a la aplicación cuando un usuario debe cambiar su contraseña.
Ubicación: app/src/user/components/ForcePasswordChangeModal.tsx
Props:
open: boolean- Controla si el modal está visible
Características:
- No se puede cerrar: Sin botón X, sin click fuera, sin tecla ESC
- Bloquea la interfaz: Previene acceso a la aplicación hasta cambiar contraseña
- Formulario completo: Contraseña actual, nueva contraseña, confirmación
- Validación con Zod: Mínimo 8 caracteres, contraseñas deben coincidir, nueva debe ser diferente
- Botones show/hide: Para visualizar contraseñas
- Alerta destructiva: Banner rojo explicando la situación
- Recarga automática: La página se recarga después del cambio exitoso
Cuándo aparece:
- Usuario creado por administrador con contraseña temporal
- Administrador cambió la contraseña del usuario
- Campo
mustChangePassworddel usuario estrue
Ejemplo de uso:
import { ForcePasswordChangeModal } from "../user/components/ForcePasswordChangeModal";import { useAuth } from "wasp/client/auth";
function App() { const { data: user } = useAuth(); const mustChangePassword = user?.mustChangePassword === true;
return ( <> <ForcePasswordChangeModal open={mustChangePassword} /> {/* Resto de la aplicación */} </> );}Flujo de uso:
- Usuario inicia sesión con contraseña establecida por admin
- Modal aparece bloqueando la aplicación
- Usuario ingresa contraseña actual y nueva contraseña
- Al enviar, se llama a la operación
changePassword - Si es exitoso,
mustChangePasswordse limpia afalse - Página se recarga mostrando la app sin el modal
Ver guía completa de Force Password Change para más detalles.
Form con React Hook Form
Sección titulada «Form con React Hook Form»import { useForm } from "react-hook-form";import { zodResolver } from "@hookform/resolvers/zod";import * as z from "zod";import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage,} from "../client/components/ui/form";import { Input } from "../client/components/ui/input";import { Button } from "../client/components/ui/button";
const schema = z.object({ email: z.string().email("Email invalido"), password: z.string().min(8, "Minimo 8 caracteres"),});
function LoginForm() { const form = useForm({ resolver: zodResolver(schema), defaultValues: { email: "", password: "" }, });
const onSubmit = data => { console.log(data); };
return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl> <Input placeholder="tu@email.com" {...field} /> </FormControl> <FormMessage /> </FormItem> )} />
<FormField control={form.control} name="password" render={({ field }) => ( <FormItem> <FormLabel>Contrasena</FormLabel> <FormControl> <Input type="password" {...field} /> </FormControl> <FormMessage /> </FormItem> )} />
<Button type="submit">Iniciar sesión</Button> </form> </Form> );}Toast Notifications
Sección titulada «Toast Notifications»import { useToast } from "../client/components/ui/use-toast";
function MyComponent() { const { toast } = useToast();
const handleClick = () => { toast({ title: "Éxito!", description: "La operación se completo correctamente.", }); };
// Toast de error const handleError = () => { toast({ variant: "destructive", title: "Error", description: "Algo salio mal.", }); };
return <Button onClick={handleClick}>Mostrar Toast</Button>;}import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "../client/components/ui/table";
<Table> <TableHeader> <TableRow> <TableHead>Nombre</TableHead> <TableHead>Email</TableHead> <TableHead>Rol</TableHead> </TableRow> </TableHeader> <TableBody> {users.map(user => ( <TableRow key={user.id}> <TableCell>{user.name}</TableCell> <TableCell>{user.email}</TableCell> <TableCell>{user.role}</TableCell> </TableRow> ))} </TableBody></Table>;Tema y Colores
Sección titulada «Tema y Colores»Los colores corporativos de TalentBricks se definen como CSS variables en app/src/client/Main.css
y se consumen a través de tailwind.config.js. El sistema usa HSL para permitir modo claro/oscuro
sin duplicar clases.
Paleta TalentBricks
Sección titulada «Paleta TalentBricks»| Token | Claro | Oscuro | Uso |
|---|---|---|---|
--primary | 287 100% 44% — violeta | 287 90% 75% — violeta claro | CTAs, botones principales |
--secondary | 193 100% 53% — cyan | 193 95% 60% — cyan | Acentos, links, highlights |
--secondary-muted | 193 75% 78% — cyan suave | 193 70% 85% | Gradientes, fondos suaves |
--accent | 193 85% 65% — cyan medio | 193 85% 70% | Hover states, chips |
--background | 0 0% 100% — blanco | 210 50% 5% — azul muy oscuro | Fondo de página |
--card-subtle | 31 57% 96% — crema | 233 24% 15% — azul oscuro | Fondos de tarjetas alternativas |
--destructive | 0 84.2% 60.2% — rojo | 0 62.8% 63% — rojo suave | Eliminar, errores |
--success | 141 71% 48% — verde | igual | Confirmaciones, éxito |
--warning | 36 100% 50% — naranja | igual | Advertencias |
Variables completas (claro)
Sección titulada «Variables completas (claro)»:root { --primary: 287 100% 44%; /* violeta TalentBricks */ --primary-foreground: 0 0% 98%; --secondary: 193 100% 53%; /* cyan TalentBricks */ --secondary-foreground: 0 0% 9%; --secondary-muted: 193 75% 78%; --accent: 193 85% 65%; --background: 0 0% 100%; --foreground: 0 0% 3.9%; --card-subtle: 31 57% 96%; /* crema para tarjetas alternativas */ --destructive: 0 84.2% 60.2%; --success: 141 71% 48%; --warning: 36 100% 50%; --border: 0 0% 89.8%; --radius: 0.5rem;}Uso en Tailwind
Sección titulada «Uso en Tailwind»// Colores en clases Tailwind<div className="bg-primary text-primary-foreground"> // violeta<div className="bg-secondary text-secondary-foreground"> // cyan<div className="bg-accent text-accent-foreground"> // cyan medio<div className="bg-card-subtle"> // crema
// Gradiente de texto TalentBricks<span className="text-gradient-primary">Texto degradado cyan</span><span className="text-gradient-primary-diagonal">Diagonal</span>Modo Oscuro
Sección titulada «Modo Oscuro»El modo oscuro se activa con la clase dark en el <html>:
import { DarkModeSwitcher } from "../client/components/DarkModeSwitcher";
// El componente ya está integrado en el NavBar<DarkModeSwitcher />;Agregar Nuevos Componentes
Sección titulada «Agregar Nuevos Componentes»Para agregar un componente de ShadCN:
cd appnpx shadcn-ui@latest add [component-name]Ejemplo:
npx shadcn-ui@latest add calendarnpx shadcn-ui@latest add sliderComponentes Custom de Navegación
Sección titulada «Componentes Custom de Navegación»Breadcrumb
Sección titulada «Breadcrumb»Componente de migas de pan para navegación contextual:
import { Breadcrumb } from "../client/components/Breadcrumb";
<Breadcrumb items={[ { label: "Cursos", href: "/courses" }, { label: "Python Basics" }, // último item sin href ]}/>;PageHeader
Sección titulada «PageHeader»Encabezado de página consistente con breadcrumb y acciones opcionales:
import { PageHeader } from "../client/components/PageHeader";
<PageHeader title="Mi Perfil" subtitle="Gestiona tu información personal" breadcrumb={[{ label: "Cuenta", href: "/account" }, { label: "Perfil" }]} actions={<Button>Editar</Button>}/>;AccountLayout
Sección titulada «AccountLayout»Layout unificado para páginas de cuenta con sidebar en desktop y sheet en mobile:
import { AccountLayout } from "../user/layout";
export default function MyPage({ user }) { return ( <AccountLayout> <h1>Contenido de la página</h1> {/* El layout maneja el contenedor y navegación */} </AccountLayout> );}ResourceList
Sección titulada «ResourceList»Lista de recursos descargables para estudiantes en la página de aprendizaje.
Ubicación: app/src/courses/components/ResourceList.tsx
Props:
lessonId: number- ID de la lección
Características:
- Muestra recursos ordenados por
order - Íconos por tipo de archivo (PDF, ZIP, PowerPoint, código, etc.)
- Tamaño de archivo formateado (KB/MB)
- Botón de descarga con loading state
- Genera URL firmada al hacer click
- Estados: loading, error, empty state
- Requiere enrollment o lección preview
Ejemplo:
import { ResourceList } from "../courses/components/ResourceList";
<ResourceList lessonId={currentLesson.id} />;ResourceUploadSection (Admin)
Sección titulada «ResourceUploadSection (Admin)»Componente admin para gestionar recursos de lección (upload, edición, eliminación).
Ubicación: app/src/admin/components/ResourceUploadSection.tsx
Props:
lessonId?: number- ID de la lección (undefined si nueva lección)resources: Partial<LessonResource>[]- Lista de recursosonResourcesChange: (resources: Partial<LessonResource>[]) => void- Callback al cambiar recursos
Características:
- Upload directo a S3 con presigned URLs
- Validación de tipo y tamaño de archivo
- Edición inline de título y descripción
- Eliminación con confirmación
- Loading states durante upload/delete
- Muestra nombre de archivo, tipo y tamaño
- Deshabilita upload si lección no está guardada
Ejemplo:
import { ResourceUploadSection } from "../../admin/components/ResourceUploadSection";
<ResourceUploadSection lessonId={formData.id} resources={formData.resources || []} onResourcesChange={resources => setFormData({ ...formData, resources })}/>;Componentes de Dashboard de Estudiante
Sección titulada «Componentes de Dashboard de Estudiante»Componentes del panel de progreso del estudiante. Ubicados en app/src/user/components/.
UpcomingLessonsCard
Sección titulada «UpcomingLessonsCard»Tarjeta que muestra hasta 3 próximas lecciones pendientes del estudiante, con barra de progreso por curso y botón “Continuar”.
Props:
| Prop | Tipo | Descripción |
|---|---|---|
data | UpcomingLessonItem[] | undefined | Lista de próximas lecciones |
isLoading | boolean | Muestra skeleton mientras carga |
Estados:
- Con lecciones: Lista de hasta 3 ítems con título del curso, título de la lección, progreso
(
X de Y clases) y botón “Continuar” → navega a/courses/:slug/learn. - Sin inscripciones: Mensaje vacío con enlace “Ver cursos” →
/courses.
Ejemplo:
import { UpcomingLessonsCard } from "./components/UpcomingLessonsCard";
<UpcomingLessonsCard data={upcomingLessons} isLoading={upcomingLoading} />;Componentes de Equipo (B2B)
Sección titulada «Componentes de Equipo (B2B)»Componentes específicos para el módulo de organizaciones. Ubicados en
app/src/organizations/components/.
TeamNavigation
Sección titulada «TeamNavigation»Barra de navegación compartida para todas las páginas del panel de equipo. Muestra 5 tabs con el activo resaltado.
Props:
| Prop | Tipo | Descripción |
|---|---|---|
organizationId | string | ID de la organización |
activeTab | 'dashboard' | 'members' | 'courses' | 'analytics' | 'billing' | Tab activo actual |
Ejemplo:
import { TeamNavigation } from "../components/TeamNavigation";
<TeamNavigation organizationId={organizationId!} activeTab="members" />;Marcadores y Notas de Lección
Sección titulada «Marcadores y Notas de Lección»Componentes del sidebar de aprendizaje para marcadores de video y notas. Ubicados en
app/src/courses/components/.
BookmarksPanel
Sección titulada «BookmarksPanel»Panel para gestionar marcadores de momentos del video. Solo visible para usuarios inscritos.
Props:
| Prop | Tipo | Descripción |
|---|---|---|
lessonId | number | ID de la lección actual |
getCurrentVideoTime | () => number | Devuelve el tiempo actual del video en segundos |
seekToTimestamp | (seconds: number) => void | Salta a un segundo específico del video |
isYouTube | boolean? | Muestra aviso de limitación de YouTube |
Funcionalidad: formulario inline para crear marcadores, lista con badge de tiempo clicable, eliminar con hover reveal.
NotesPanel
Sección titulada «NotesPanel»Panel para tomar y gestionar notas de lección con auto-guardado.
Props:
| Prop | Tipo | Descripción |
|---|---|---|
lessonId | number | ID de la lección actual |
getCurrentVideoTime | () => number | Devuelve el tiempo actual del video en segundos |
seekToTimestamp | (seconds: number) => void | Salta a un segundo específico del video |
Funcionalidad: búsqueda client-side, auto-guardado debounced (1500ms), vinculación a timestamp de video, edición in-place, confirmación al eliminar.
Helper exportado: formatTimestamp(seconds: number): string en BookmarksPanel.tsx — convierte
segundos a "M:SS".