Ir al contenido

TalentBricksAI usa ShadCN UI (estilo New York) con Radix UI primitives.

Los componentes estan en app/src/client/components/ui/.

ComponenteArchivodescripción
Buttonbutton.tsxBotones con variantes
Inputinput.tsxCampo de texto
Textareatextarea.tsxÁrea de texto
Selectselect.tsxSelector dropdown
Checkboxcheckbox.tsxCasilla de verificación
Switchswitch.tsxToggle switch
Labellabel.tsxEtiqueta para inputs
Formform.tsxWrapper de formulario
ComponenteArchivodescripción
Cardcard.tsxContenedor con borde
Dialogdialog.tsxModal dialog
Sheetsheet.tsxPanel deslizante
Separatorseparator.tsxlínea divisora
Tabstabs.tsxnavegación por tabs
ComponenteArchivodescripción
Alertalert.tsxMensaje de alerta
Toasttoaster.tsxNotificaciones toast
Progressprogress.tsxBarra de progreso
Skeletonskeleton.tsxPlaceholder de carga
ComponenteArchivodescripción
DropdownMenudropdown-menu.tsxMenu desplegable
NavigationMenunavigation-menu.tsxMenu de navegación
BreadcrumbBreadcrumb.tsxMigas de pan (custom, no ShadCN)
PageHeaderPageHeader.tsxEncabezado de página con breadcrumb
ComponenteArchivodescripción
AccountLayoutuser/layout/AccountLayout.tsxLayout para páginas de cuenta
AccountSidebaruser/layout/AccountSidebar.tsxSidebar de navegación de cuenta
ComponenteArchivodescripción
Tabletable.tsxTabla de datos
Avataravatar.tsxAvatar de usuario
Badgebadge.tsxEtiqueta/badge
ComponenteArchivodescripción
ScrollToTopButtonScrollToTopButton.tsxBotón flotante para volver al inicio de la página

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áginas

Props: 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íaUsoUbicación
Lucide ReactÍconos de UI (65+ archivos)import { Eye } from 'lucide-react'
IconifyLogos de marca únicamenteapp/src/client/components/icons/

Componentes disponibles:

ComponenteArchivoDescripción
IconIcon.tsxWrapper genérico para Iconify
BrandIconBrandIcon.tsxLogos 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

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

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 mustChangePassword del usuario es true

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:

  1. Usuario inicia sesión con contraseña establecida por admin
  2. Modal aparece bloqueando la aplicación
  3. Usuario ingresa contraseña actual y nueva contraseña
  4. Al enviar, se llama a la operación changePassword
  5. Si es exitoso, mustChangePassword se limpia a false
  6. Página se recarga mostrando la app sin el modal

Ver guía completa de Force Password Change para más detalles.

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>
);
}
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>;

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.

TokenClaroOscuroUso
--primary287 100% 44% — violeta287 90% 75% — violeta claroCTAs, botones principales
--secondary193 100% 53% — cyan193 95% 60% — cyanAcentos, links, highlights
--secondary-muted193 75% 78% — cyan suave193 70% 85%Gradientes, fondos suaves
--accent193 85% 65% — cyan medio193 85% 70%Hover states, chips
--background0 0% 100% — blanco210 50% 5% — azul muy oscuroFondo de página
--card-subtle31 57% 96% — crema233 24% 15% — azul oscuroFondos de tarjetas alternativas
--destructive0 84.2% 60.2% — rojo0 62.8% 63% — rojo suaveEliminar, errores
--success141 71% 48% — verdeigualConfirmaciones, éxito
--warning36 100% 50% — naranjaigualAdvertencias
app/src/client/Main.css
: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;
}
// 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>

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 />;

Para agregar un componente de ShadCN:

Ventana de terminal
cd app
npx shadcn-ui@latest add [component-name]

Ejemplo:

Ventana de terminal
npx shadcn-ui@latest add calendar
npx shadcn-ui@latest add slider

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
]}
/>;

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>}
/>;

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>
);
}

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} />;

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 recursos
  • onResourcesChange: (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 del panel de progreso del estudiante. Ubicados en app/src/user/components/.

Tarjeta que muestra hasta 3 próximas lecciones pendientes del estudiante, con barra de progreso por curso y botón “Continuar”.

Props:

PropTipoDescripción
dataUpcomingLessonItem[] | undefinedLista de próximas lecciones
isLoadingbooleanMuestra 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 específicos para el módulo de organizaciones. Ubicados en app/src/organizations/components/.

Barra de navegación compartida para todas las páginas del panel de equipo. Muestra 5 tabs con el activo resaltado.

Props:

PropTipoDescripción
organizationIdstringID de la organización
activeTab'dashboard' | 'members' | 'courses' | 'analytics' | 'billing'Tab activo actual

Ejemplo:

import { TeamNavigation } from "../components/TeamNavigation";
<TeamNavigation organizationId={organizationId!} activeTab="members" />;

Componentes del sidebar de aprendizaje para marcadores de video y notas. Ubicados en app/src/courses/components/.

Panel para gestionar marcadores de momentos del video. Solo visible para usuarios inscritos.

Props:

PropTipoDescripción
lessonIdnumberID de la lección actual
getCurrentVideoTime() => numberDevuelve el tiempo actual del video en segundos
seekToTimestamp(seconds: number) => voidSalta a un segundo específico del video
isYouTubeboolean?Muestra aviso de limitación de YouTube

Funcionalidad: formulario inline para crear marcadores, lista con badge de tiempo clicable, eliminar con hover reveal.

Panel para tomar y gestionar notas de lección con auto-guardado.

Props:

PropTipoDescripción
lessonIdnumberID de la lección actual
getCurrentVideoTime() => numberDevuelve el tiempo actual del video en segundos
seekToTimestamp(seconds: number) => voidSalta 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".