Biblioteca de componentes de UI
packages/ui (@repo/ui) es la biblioteca de componentes de React compartida de Closegram. Es la única fuente de verdad para la apariencia de botones, controles de formulario, avatares, badges, spinners y demás primitivas usadas por la app principal (apps/frontend-nextjs) y el panel de administración, de modo que ambos consumen los mismos componentes en lugar de redeclarar las mismas cadenas de Tailwind en cada archivo.
El paquete se desarrolla y previsualiza de forma aislada con Storybook (pnpm --filter @repo/ui dev, puerto 6006), lo que permite que cada componente se renderice sin depender de ninguna app. Los componentes se agrupan bajo los títulos Primitives/ y Foundations/ en Storybook.
Importación
El nombre del paquete es @repo/ui. Todo lo de la superficie pública se reexporta desde src/index.ts:
import { Button, Badge, Avatar, Spinner, Tooltip } from '@repo/ui';
import { fieldClass, TextInput, Switch, Select } from '@repo/ui';
Hay tres puntos de entrada definidos en package.json:
| Entrada | Importación | Contenido |
|---|---|---|
| Main | @repo/ui | Todos los componentes, las constantes de cadenas de clase y el helper cx |
| Icons | @repo/ui/icons | El conjunto completo de íconos (genéricos + de marca) |
| Styles | @repo/ui/styles.css | Hoja de estilos de Tailwind compilada (importar una sola vez en la raíz de la app) |
react y react-dom (^18.3.1) son dependencias entre pares (peer dependencies) — la app consumidora las provee.
El helper cx
cx(...parts) es un pequeño combinador de className que descarta valores falsy, usado internamente por cada componente y reexportado para los sitios de llamada:
import { cx } from '@repo/ui';
cx('rounded-lg', isActive && 'bg-cg-blue', className);
Componentes
Button
Botón de acción canónico — un superconjunto de las variantes usadas en toda la app principal y el panel de administración. Extiende los atributos nativos de <button> (mediante React.ButtonHTMLAttributes) y reenvía una ref.
| Prop | Tipo | Por defecto | Notas |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'ghost' | 'danger' | 'success' | 'warning' | 'outline-danger' | 'outline-warning' | 'secondary' | Estilo visual |
size | 'sm' | 'md' | 'md' | |
icon | React.ReactNode | — | Ícono inicial opcional renderizado antes de children |
| ...resto | props nativas de <button> | — | onClick, disabled, type (por defecto 'button'), etc. |
<Button variant="primary" size="md">Guardar</Button>
Para el puñado de páginas de configuración/pagos que aún declaran el estilo del botón en línea, las cadenas de clase sin procesar también se exportan: primaryButtonClass, secondaryButtonClass, dangerButtonClass, linkButtonClass.
IconButton
Botón de ícono redondo y ghost que consolida el patrón repetido w-9 h-9 flex items-center justify-center rounded-full hover:bg-gray-100 … usado en headers, modales y barras de herramientas. Extiende los atributos nativos de <button> y reenvía una ref.
| Prop | Tipo | Por defecto |
|---|---|---|
size | 'sm' | 'md' | 'lg' (w-8/9/10) | 'md' |
| ...resto | props nativas de <button> | — |
<IconButton size="md" aria-label="More"><MoreHorizontal /></IconButton>
Badge
Etiqueta/píldora pequeña. Superconjunto usado por la app principal y el panel de administración.
| Prop | Tipo | Por defecto |
|---|---|---|
tone | 'neutral' | 'accent' | 'info' | 'success' | 'warning' | 'danger' | 'neutral' |
icon | React.ReactNode | — |
children | React.ReactNode | — (requerido) |
className | string | — |
<Badge tone="success">Verified</Badge>
Avatar
Avatar circular con un respaldo de inicial en gradiente morado→rosa cuando no hay imagen configurada.
| Prop | Tipo | Por defecto | Notas |
|---|---|---|---|
src | string | null | — | Recurre a una inicial cuando está ausente |
alt | string | '' | |
name | string | — | Primer carácter mostrado como la inicial de respaldo |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | |
className | string | — |
<Avatar src={user.photo} name={user.name} size="lg" />
StoryRingAvatar
Anillo de historia estilo Instagram alrededor de un avatar, coloreado según la audiencia/visibilidad de la historia. visibility proviene de User.activeStoryVisibility; null/undefined significa que no hay historia activa, lo que renderiza un avatar simple sin anillo.
| Prop | Tipo | Por defecto | Notas |
|---|---|---|---|
src | string | null | — | |
alt | string | '' | |
visibility | string | null | — | public, followers, close_friends, subscribers, private reciben cada uno un gradiente distinto |
sizeClass | string | — (requerido) | Clases de tamaño de Tailwind para el avatar en sí, p. ej. "w-10 h-10" |
fallbackSrc | string | '/default-avatar.png' | Imagen usada cuando src está vacío |
className | string | — |
<StoryRingAvatar src={user.photo} sizeClass="w-14 h-14" visibility="close_friends" />
El helper storyRingClass(visibility) también se exporta y devuelve las clases del gradiente del anillo (o null cuando no hay historia activa).
Spinner
Spinner de carga en línea que hereda el color de texto actual (border-current). Renderiza un elemento role="status" con aria-label="Loading".
| Prop | Tipo | Por defecto |
|---|---|---|
className | string | 'w-4 h-4' |
<Spinner className="w-5 h-5" />
Skeleton
Bloque de skeleton base — un marcador de posición gris pulsante. Se compone el ancho/alto en el sitio de la llamada. Si el className ya contiene una utilidad rounded-*, se omite el rounded-md por defecto para que gane el rounded-full de quien llama.
| Prop | Tipo | Por defecto |
|---|---|---|
className | string | '' |
<Skeleton className="h-3 w-32" />
Tooltip
Tooltip ligero: envuelve un elemento y muestra una etiqueta al pasar el mouse o al enfocar. No renderiza nada adicional cuando label está vacío.
| Prop | Tipo | Por defecto |
|---|---|---|
label | string | — (requerido) |
side | 'top' | 'bottom' | 'left' | 'right' | 'top' |
children | React.ReactNode | — |
className | string | '' |
<Tooltip label="Copy link" side="bottom"><IconButton><Copy /></IconButton></Tooltip>
VerifiedBadge
Sello de verificación estilo Instagram/Twitter con una marca de verificación blanca. También se reexporta desde @repo/ui/icons.
| Prop | Tipo | Por defecto |
|---|---|---|
className | string | 'w-3.5 h-3.5 text-cg-blue flex-shrink-0' |
<span>{user.name}{user.verified && <VerifiedBadge />}</span>
AnimatedCount
Contador animado que hace un pop y se desplaza hacia arriba/abajo cada vez que value cambia (usando la Web Animations API). Omite la animación en el primer montaje.
| Prop | Tipo | Por defecto | Notas |
|---|---|---|---|
value | number | — (requerido) | |
format | boolean | true | Activa/desactiva la abreviación compacta K/M |
className | string | — |
<AnimatedCount value={likeCount} />
El helper puro formatCount(n) (p. ej. 1200 → "1.2K", 3_400_000 → "3.4M") también se exporta.
RollingNumber
Rueda/cuenta desde su valor anterior hasta el nuevo, mostrando cada número intermedio mediante requestAnimationFrame. Omite el giro en el primer montaje y comienza cada giro desde lo que esté actualmente en pantalla para que los cambios rápidos se mantengan fluidos.
| Prop | Tipo | Por defecto |
|---|---|---|
value | number | — (requerido) |
className | string | — |
<RollingNumber value={followerCount} />
DateTimePicker
Selector de fecha y hora personalizado sin calendario nativo del navegador y sin dependencia externa — una cuadrícula de mes más selectores de hora/minuto. Devuelve la misma cadena local 'YYYY-MM-DDTHH:mm' que usa un <input type="datetime-local">, de modo que quienes lo llaman no cambian. Marcado como 'use client'.
| Prop | Tipo | Por defecto | Notas |
|---|---|---|---|
value | string | — (requerido) | Cadena local 'YYYY-MM-DDTHH:mm', o '' |
onChange | (value: string) => void | — (requerido) | |
min | Date | — | Fecha y hora seleccionable más temprana |
placeholder | string | 'Fecha y hora' | |
panel | boolean | false | Panel en línea siempre visible (la cuadrícula se desplaza, fila de hora + "Done" fijos) en lugar de un botón disparador + popover flotante |
onDone | () => void | — | Llamado por el botón "Done" del panel (p. ej. para cerrar un overlay) |
className | string | '' |
<DateTimePicker value={when} onChange={setWhen} min={new Date()} />
Primitivas de formulario
Form.tsx es la única fuente de verdad para la apariencia de inputs, textareas, selects, labels y switches. Exporta tanto componentes listos para usar como las constantes de cadenas de clase sin procesar a partir de las cuales se construyen.
Constantes de clase: fieldClass (campo con borde de ancho completo), labelClass, inlineInputClass, inlineSelectClass — importa estas en lugar de redeclarar las mismas cadenas.
Componentes:
| Componente | Props clave | Notas |
|---|---|---|
FieldLabel | htmlFor, className, children | <label> estilizado con labelClass |
TextInput | props nativas de <input> (ref reenviada) | Estilizado con fieldClass |
TextArea | props nativas de <textarea> | fieldClass + resize-none |
SearchInput | value, onChange(value), placeholder, onClear, inputClassName (SearchInputProps) | Campo de búsqueda redondeado con ícono de lupa y un botón de limpiar opcional (se muestra cuando onClear está configurado y hay texto) |
Switch | checked, onChange(), disabled, ariaLabel | Interruptor; role="switch", API canónica checked/onChange |
Select | value, onChange(value), options? (SelectOption[]), children?, variant ('field' | 'inline'), disabled | Pasar ya sea options o hijos <option> |
SelectOption es { value: string; label: string }.
<FieldLabel>Idioma</FieldLabel>
<Select
value={lang}
onChange={setLang}
options={[
{ value: 'es', label: 'Español' },
{ value: 'en', label: 'English' },
]}
/>
<SearchInput value={q} onChange={setQ} onClear={() => setQ('')} placeholder="Buscar" />
<Switch checked={on} onChange={() => setOn((v) => !v)} ariaLabel="toggle" />
Estilos de callout / caja
styles/boxes.ts exporta cadenas de clase de callout en línea reutilizadas en los formularios de configuración y pagos, de modo que las mismas cadenas no se redeclaran en cada archivo:
successBoxClass(verde)errorBoxClass(rojo)neutralBoxClass(gris)warningBoxClass(ámbar)
<div className={successBoxClass}>Saved.</div>
Íconos
Se importan desde @repo/ui/icons. Hay dos grupos.
Conjunto de íconos genéricos (icons/lucide.tsx)
Una biblioteca de íconos SVG personalizada generada automáticamente que es un reemplazo directo de lucide-react — los datos de trazado se derivan de lucide (licencia ISC) y se renderizan mediante un wrapper propio, de modo que la app ya no depende del paquete lucide-react. Cada ícono acepta IconProps (props nativas de SVG más):
| Prop | Tipo | Por defecto |
|---|---|---|
size | number | string | 24 |
strokeWidth | number | string | 2 |
absoluteStrokeWidth | boolean | — |
color | string | 'currentColor' |
import { Bell, Heart, Search } from '@repo/ui/icons';
<Heart size={20} className="text-cg-red" />
El conjunto incluye (entre otros) Activity, Archive, ArrowLeft, Bell, Bookmark, Calendar, Check, CheckCircle2, ChevronDown/Left/Right/Up, Circle, Clapperboard, Clock, Coins, Compass, Copy, CreditCard, Crown, DollarSign, Download, ExternalLink, Eye, Facebook, FileText, Gift, Globe, Grid3x3, Hand, Hash, Heart, Image, Languages, Link, Link2, Loader2, Lock, LogOut, Mail, Maximize2, Megaphone, MapPin, MessageCircle, Mic, MicOff, Minus, Monitor, Moon, MoreHorizontal, Package, Pencil, Phone, Pin, Play, Plus, Radio, Receipt, Repeat2, RotateCcw, Search, Send, Settings, Share2, ShieldAlert/Check/Off, ShoppingBag, Smile, Square, SquarePen, Star, Store, Sun, Ticket, Trash2, TrendingUp, Trophy, Truck, Twitter, Upload, UploadCloud, UserPlus, UserSquare2, Users, Video, VideoOff, X, XCircle, además de un RepostSquare de esquinas cuadradas personalizado.
Íconos de marca
Íconos elaborados a mano que coinciden con el diseño de navegación y acciones de publicación de la app. La mayoría toman { className?: string; filled?: boolean } (una variante rellena para estados activos/seleccionados); unos pocos son solo de contorno (solo className):
| Ícono | Variantes |
|---|---|
HomeIcon | contorno / filled |
SearchIcon | contorno / filled |
ExploreIcon | contorno / filled |
ClipIcon | contorno / filled |
MessagesIcon | contorno / filled |
NotificationIcon | contorno / filled |
HeartIcon | contorno / filled |
ProfileIcon | contorno / filled |
CoinIcon | contorno / filled |
LanguageIcon | contorno / filled |
CommentIcon | solo contorno |
CreateIcon | solo contorno |
EditIcon | solo contorno |
RepostIcon | solo contorno |
ShareIcon | solo contorno |
VerifiedBadge también se reexporta desde @repo/ui/icons.
import { HomeIcon } from '@repo/ui/icons';
<HomeIcon filled={isActive} className="w-6 h-6" />
Tokens de diseño y Tailwind
La biblioteca incluye su propio tailwind.config.ts para que se renderice correctamente de forma aislada (Storybook) sin depender de ninguna app. El modo oscuro usa la estrategia darkMode: 'class' — cada componente está estilizado tanto para variantes claras como dark:. Consulta Theming para ver cómo se alterna la clase de tema en tiempo de ejecución.
La paleta está definida bajo el espacio de nombres de color cg (Closegram) y refleja los tokens usados por apps/frontend-nextjs. Tokens clave:
| Token | Valor | Uso |
|---|---|---|
cg-blue | #3797f0 | Azul de marca primario (+ -dark, -hover, -light, -action #0095f6) |
cg-green | #58c322 | Éxito |
cg-red | #ed4956 | Peligro |
cg-bg | #ffffff / dark #0c1014 | Fondo de página |
cg-dark-* | primary #121212, secondary, elevated #262626, modal, border, hover, input, … | Superficies del modo oscuro |
cg-text-* | primary, secondary #a8a8a8, tertiary, placeholder | Colores de texto |
cg-separator / cg-muted | #262626 / #555555 | Divisores, elementos atenuados |
El archivo de configuración señala una tarea pendiente para llevar esta paleta a un
@repo/tailwind-configcompartido, de modo que las apps y este paquete compartan una única fuente de verdad en lugar de duplicarla.
Las apps consumidoras que no compilan por sí mismas las clases de Tailwind del paquete pueden importar la hoja de estilos precompilada una sola vez en la raíz:
import '@repo/ui/styles.css';