lib-kit-components
v0.1.0
Published
Componentes atómicos + moléculas PWA para Next.js + React + Tailwind v4 + Framer Motion
Maintainers
Readme
Scaffolding · Atomic Components
67 componentes + 39 hooks para Next.js + React + Tailwind v4 + Framer Motion, con soporte de tema claro/oscuro vía la clase .dark (compatible con next-themes).
📖 Guía completa de uso de cada componente y hook → — cuándo usar cada uno, todas sus props, ejemplos y comportamiento no obvio. Pensada para vos o para que la lea una IA antes de implementar.
🧪 Demo en vivo → — el playground de
dev/deployado en Vercel, los componentes reales corriendo.
🧭 ¿Qué vas a construir? Empezá por acá
Este README es el catálogo de piezas. Para arrancar un proyecto no leas de arriba a abajo: buscá tu caso en esta tabla y andá directo a la guía, que te dice qué componentes montar, en qué archivo va cada uno y en qué orden.
| Si estás armando… | Andá a | Qué vas a encontrar |
|---|---|---|
| 🛒 Una tienda / ecommerce | docs/guides/ecommerce.md | Las 12 piezas obligatorias de una tienda, el mapa de rutas, el carrito global, el catálogo con skeleton y el checkout — con server render obligatorio por SEO como Regla 0 (metadata, JSON-LD, sitemap) |
| 👥 Una red social / app de comunidad | docs/guides/red-social.md | Las 9 piezas de una app social: feed con SocialPost y comentarios, campana + drawer de notificaciones, BottomNav de feed/chat/explorar/perfil, compartir e instalación |
| 📱 Una app mobile / PWA | docs/guides/app-base.md | Las 6 capas del shell: safe areas, splash, instalación, offline, navegación, FAB y avisos |
| 🏗️ Un proyecto Next.js nuevo (cualquiera de los dos) | Setup de una app nueva desde cero (acá abajo) | Crear el proyecto, instalar, conectar estilos y tema, y la arquitectura atómica SSR-first del consumidor |
| 🔎 Un componente puntual | docs/README.md | La guía "Necesito… → Usá…", más las props completas, ejemplos y gotchas de cada componente y hook |
| 👀 Ver algo funcionando | Demo en vivo · dev/ | El playground con todos los componentes reales corriendo |
Orden recomendado para un ecommerce: setup del proyecto → Regla 0 de la guía de ecommerce (el SEO condiciona la arquitectura, leelo antes de elegir componentes) → sus lineamientos obligatorios → el shell listo para copiar.
Las dos guías están escritas para que una IA las siga sin interpretar: props leídas del código real, defaults explícitos, checklist de integración y los gotchas que no se deducen mirando el componente.
📁 Estructura
app/
globals.css # Tokens de tema + base PWA
components/
Button.tsx
Input.tsx
Textarea.tsx
Select.tsx
Dropdown.tsx
Spinner.tsx
Toast.tsx # incluye <ToastProvider> + hook useToast()
Checkbox.tsx # + <CheckboxGroup/>
Switch.tsx # interruptor on/off con thumb animado por spring
CodeOTP.tsx # código OTP/2FA en casillas segmentadas, auto-avance y pegado multi-dígito
Modal.tsx
BottomSheet.tsx # 7 alturas + snapPoints arrastrables
Tooltip.tsx # globo informativo con hover/focus y auto-flip
Popover.tsx # panel anclado con contenido arbitrario, abierto con click
CoachMark.tsx # tour guiado con spotlight sobre elementos reales de la UI
Breadcrumbs.tsx
FlipCard.tsx # + <CreditCard/> y <CreditCardStack/>
FloatingButton.tsx # FAB + speed dial
AddButton.tsx # control de cantidad (+/−) con loading por botón
AddToCartButton.tsx # botón "agregar" con estados idle → loading → hecho
Progress.tsx # <ProgressBar/> <ProgressRing/> <StepsProgress/>
Skeleton.tsx # placeholders animados: primitivo + Text/Avatar/Card/List/Table
NotificationPanel.tsx # historial agrupado por fecha + <NotificationBell/> (popover) y <NotificationSidebar/> (drawer)
ThemeConfigurator.tsx # editor en vivo de los tokens de color del tema, con export CSS/JSON
TenantTheme.tsx # paleta multi-tenant por dominio/sesión: <TenantThemeProvider/> + useTenantTheme()
Card.tsx # Card base + StatCard · MediaCard · ProfileCard · PricingCard
Carousel.tsx # carrusel de imágenes: drag, dots, thumbs, autoplay, zoom
CardCarousel.tsx # carrusel de cards: track · peek · coverflow · snap
ImageZoom.tsx # visor pan + zoom a pantalla completa (bloquea el resto) + <ZoomableImage/>
Tabs.tsx # 5 estilos: underline · pill · segmented · enclosed · vertical
TabsGlow.tsx # pastilla flotante con overshoot elástico + glow primario
TabsCarousel.tsx # línea corta centrada + panel que se desliza como carrusel
TabsDock.tsx # iconos con rebote elástico estilo dock + punto indicador
ScrollArea.tsx # scroll con barra propia arrastrable — 4 variantes de grosor/animación
Footer.tsx # pie de página: marca, columnas de links, redes y newsletter
VideoPlayer.tsx # scrub, marcadores, atajos de teclado y modo portrait tipo reels
Hero.tsx # HeroSearch · HeroImage · HeroTabs · HeroWelcome — cabeceras de pantalla completa
ChipCarousel.tsx # fila de chips con drag, snap y flechas — 4 variantes
Keypad.tsx # teclado numérico táctil 3×4, tecla extra + borrado long-press
PinLock.tsx # pantalla de bloqueo por PIN o contraseña
AmountPad.tsx # carga de montos a pantalla completa, estilo billetera
RedirectTimer.tsx # cuenta atrás con redirección a WhatsApp/Telegram/SMS/mail/URL
ShareButton.tsx # compartir con hoja nativa del sistema o sheet propio
CardGrid.tsx # grilla de cards con columnas ajustables en tiempo real
DataTable.tsx # orden, búsqueda, selección, paginado, sticky header
AnimatedTable.tsx # orden con reacomodo animado (FLIP) + resalte de celdas que cambian
ExpandableTable.tsx # fila con panel de detalle desplegable animado
Spreadsheet.tsx # hoja de cálculo editable con fórmulas y atajos
CalendarGrid.tsx # grilla mensual con eventos
Navbar.tsx # usa next/link + next/navigation
SideBar.tsx # usa next/link + next/navigation
BottomNav.tsx # usa next/link + next/navigation
ImageCounter.tsx # galería de una imagen con contador «03 / 12» + zoom
Snackbar.tsx # <SnackbarProvider> + useSnackbar() (cola + deshacer)
DatePicker.tsx # fecha simple o rango, popover o embebido
TimePicker.tsx # horario (h/m/s), 12h o 24h, popover o embebido
Pagination.tsx # paginado con elipsis, resumen y tamaño de página
PullToRefresh.tsx # gesto nativo de refresco
ProductCard.tsx # tarjeta de producto: grid · list · compact · offer · editorial
ProductGrid.tsx # grilla de ProductCard: columnas y variante automáticas
Cart.tsx # CartButton (badge animado) + CartPanel + useCart()
PromoPopup.tsx # interstitial de ofertas + captura de email
CouponCode.tsx # cupón copiable con timer y/o cupos
CountdownBanner.tsx # cuenta regresiva de campaña
CountdownHero.tsx # cuenta regresiva gigante: blocks · flip · minimal
Chatbot.tsx # chat conversacional + quick replies + lanzador flotante
BookReader.tsx # lector paginado tipo Google Books (columnas CSS)
SocialPost.tsx # post de red social con media, reacciones y comentarios incluidos
CommentBox.tsx # comentarios con hilos, likes y paginado
Poll.tsx # encuestas: lista o carrusel de imágenes/videos, anónima y voto modificable
Confetti.tsx # confeti en canvas (burst · rain · center)
SuccessPage.tsx # pantalla de éxito + confeti + resumen de operación
PwaInstallPrompt.tsx # banner Android + sheet iOS
InstallButton.tsx # botón de instalación embebible
OfflineBanner.tsx # offline / reconectado / conexión lenta
UpdatePrompt.tsx # "nueva versión disponible" (service worker)
NotificationOptIn.tsx # opt-in de notificaciones
PwaStatus.tsx # panel de diagnóstico PWA
SplashScreen.tsx # 6 estilos de animación · icono · versión
SafeArea.tsx # + <SafeAreaSpacer/>: notch, island, home indicator
NativeShell.tsx # raíz todo-en-uno para experiencia nativa
ViewportLock.tsx # bloquea zoom/overscroll/long-press (sin UI)
AppHeader.tsx # header de app: volver, título grande colapsable, acciones, buscador
AppHeaderIsland.tsx # cápsula flotante desprendida de los bordes, estilo dynamic island
AppHeaderTabs.tsx # header + fila de tabs scrolables (underline o pastilla) con panels
AppHeaderWave.tsx # hero con degradado y esquina inferior muy redondeada
AppHeaderCard.tsx # tarjeta flotante con sombra que se eleva al scrollear
AppHeaderNotch.tsx # muesca circular con botón flotante centrado
AppHeaderPill.tsx # barra minimalista + píldora de búsqueda siempre visible
AppHeaderCardSlot.tsx # hero con título/logo (chico y grande) y card flotante vacía centrada
CountdownBannerHeader.tsx # hero de campaña con la cuenta regresiva en la card flotante
AppIdentityConfig.tsx # editor en vivo de nombre/colores/íconos de instalación + export manifest.json
SyncStatus.tsx # estado visual de una cola offline (chip o panel)
OfflineFallback.tsx # pantalla de "sin conexión" cuando un fetch falla sin caché
PermissionGate.tsx # pide un permiso del navegador con contexto
CameraCapture.tsx # foto a pantalla completa: preview, cambio de cámara, revisión
LocationPicker.tsx # GPS actual + dirección con sugerencias (sin mapa)
BiometricGate.tsx # desbloqueo por Face ID / huella / Windows Hello (WebAuthn)
ActivityTimeline.tsx # línea de tiempo vertical de eventos con estado
BranchingTimeline.tsx # línea de tiempo con ramificaciones/nodos hijos
TrackingStepper.tsx # pasos de seguimiento tipo envío/pedido
TimelineComments.tsx # línea de tiempo de eventos con notas/comentarios
GroupedActivityFeed.tsx # feed de actividad agrupado por fecha
AuditLog.tsx # historial de auditoría con cambios de campo (from → to)
Roadmap.tsx # roadmap de producto por trimestre
HowItWorksTimeline.tsx # pasos numerados "cómo funciona", horizontal o vertical
KanbanBoard.tsx # tablero Kanban de escritorio con drag & drop nativo
KanbanBoardMobile.tsx # Kanban táctil, una columna visible a la vez
ItineraryTimeline.tsx # itinerario día por día: tira de días + timeline de actividades
TripRouteMap.tsx # resumen de ruta: destinos encadenados con fechas y noches
TripBudgetSummary.tsx # presupuesto de viaje: anillo total + categorías
TripChecklist.tsx # checklist simple con progreso (equipaje, pendientes)
GroupedTaskList.tsx # tareas agrupadas por día/categoría, colapsables
TaskCard.tsx # tarea con subtareas, prioridad y fecha límite
KpiCard.tsx # tarjeta de KPI con sparkline y variación
WalletBalanceCard.tsx # saldo multi-moneda con enviar/recibir/convertir
CurrencySelector.tsx # selector de moneda con tasa de cambio
RateComparator.tsx # comparador de cotizaciones entre proveedores
ValueHistoryChart.tsx # gráfico de evolución de un valor por período
JsonChartViewer.tsx # visor de datos JSON como tabla o gráfico
TransactionList.tsx # lista de transacciones agrupadas por categoría
SendMoneyFlow.tsx # flujo de envío de dinero a un contacto
PaymentQrCard.tsx # tarjeta de cobro con QR y monto editable
BillSplitter.tsx # divisor de cuenta entre participantes
BudgetCategoryProgress.tsx # progreso de gasto por categoría de presupuesto
PaymentMethodPicker.tsx # selector de tarjetas guardadas + alta de tarjeta nueva
SearchFilters.tsx # filtros de búsqueda agrupados + resultados en vivo
ProductFilterBar.tsx # orden asc/desc + filtros de faceta + rango de precio (tienda online)
BookingCalendar.tsx # calendario de reservas con horarios disponibles por día
ProfileEditor.tsx # editor de perfil: avatar, datos de contacto y bio
LanguagePicker.tsx # selector de idioma/región
DateRangePicker.tsx # selector de rango de fechas con presets
TagInput.tsx # input de etiquetas con sugerencias
CollapsibleFormSections.tsx # formulario largo dividido en secciones colapsables
DualRangeSlider.tsx # slider de rango doble (mínimo–máximo)
ColorPicker.tsx # selector de color con paleta + color personalizado
RichTextEditor.tsx # editor de texto enriquecido básico
BeforeAfterSlider.tsx # comparador de imágenes antes/después con slider
StarRatingWidget.tsx # calificación por estrellas, con promedio y distribución
OnboardingWizard.tsx # wizard multi-paso con validación y pasos opcionales
UnitConverter.tsx # conversor de unidades por grupo
PricingTable.tsx # tabla comparativa de planes, precio mensual/anual
ShippingMethodPicker.tsx # selector de método de envío con precio y ETA
ProductComparisonTable.tsx # tabla comparativa de especificaciones entre productos
StockLimitedStepper.tsx # stepper de cantidad limitado por stock disponible
ReferralProgram.tsx # panel de programa de referidos
ApprovalChecklist.tsx # checklist de aprobación/rechazo por ítem
RolePermissionsTable.tsx # matriz de permisos por rol, editable
SecurityAlertBanner.tsx # banner de alerta de seguridad de cuenta
IdentityVerification.tsx # flujo de verificación de identidad (KYC) por pasos
BranchSelector.tsx # selector de sucursal, con distancia y estado
PageStatusScreen.tsx # pantalla de estado: 404, 403, 500 o vacío
MaintenancePage.tsx # pantalla de mantenimiento o "próximamente"
CardFan.tsx # abanico de cartas interactivo
SwipeableCardStack.tsx # pila de tarjetas swipeable, tipo Tinder
FlipRevealGrid.tsx # grilla de cartas que se voltean para revelar/emparejar
AnimatedCounter.tsx # contador numérico animado hacia un valor
SkeletonMorph.tsx # transición morph entre skeleton y contenido real
ParallaxScrollCards.tsx # cards con efecto parallax al scrollear
TiltHoverCard.tsx # tarjeta con inclinación 3D al mover el mouse + glare
AnimatedProgressRing.tsx # anillo de progreso animado hacia un valor
DragReorderList.tsx # lista reordenable por drag & drop
VideoCallGrid.tsx # grilla de participantes de videollamada
FabActionSheets.tsx # FAB con speed dial: cada acción abre su propio BottomSheet
QuickNotePad.tsx # FAB + bloc de notas rápido (viñetas, numeración, emojis)
DocumentEditor.tsx # escritor a pantalla completa: tradicional o Markdown
DiceRoller.tsx # lanzador de dados 3D, cantidad elegible
RouletteWheel.tsx # ruleta con opciones editables, elige una por giro
CoinFlip.tsx # moneda 3D: cara o cruz al azar
NumberGenerator.tsx # número al azar en un rango editable, con historial
RaffleDraw.tsx # sorteo de N ganadores con reel animado, sin repetir
TeamShuffler.tsx # reparte una lista en N equipos parejos al azar
TallyCounter.tsx # anotador de palitos, marcas en grupos de 5
Flashcard.tsx # tarjeta de memorización con flip 3D
FlashcardDeck.tsx # mazo con progreso y calificación por tarjeta
QuizCard.tsx # opción múltiple con feedback y explicación
StudyTimer.tsx # Pomodoro: foco/descanso + ciclos completados
StreakTracker.tsx # racha de estudio + grilla de constancia
ProgressByTopic.tsx # dominio por tema/materia, ordenado por avance
MatchingPairs.tsx # ejercicio de emparejar término/definición
index.ts # barrel export
hooks/
useSpreadsheet.ts # motor de fórmulas + selección + undo/redo
usePwaInstall.ts # beforeinstallprompt + standalone + snooze
useOnlineStatus.ts # online/offline + conexión lenta
useServiceWorker.ts # registro + detección de actualización
useNotificationPermission.ts # permiso + notificación local
usePlatform.ts # OS, navegador, form factor, display mode, safe areas
useNativeFeel.ts # bloqueos para experiencia nativa
useAppIdentity.ts # nombre/colores/íconos de instalación, persistidos + export manifest.json
useSplash.ts # duración mínima + espera de recursos + progreso
useSafeArea.ts # insets reactivas + CSS vars --sa-*
useImmersive.ts # esconde la barra del navegador, fullscreen, wake lock
useKeyboardInset.ts # altura del teclado virtual (--kb-inset)
useHaptics.ts # feedback táctil con nombres semánticos
useStatusBarColor.ts # tiñe la barra de estado (theme-color)
idb.ts # wrapper mínimo de IndexedDB + fallback a localStorage
useOfflineQueue.ts # cola de mutaciones offline con reintentos y backoff
useCachedFetch.ts # stale-while-revalidate con persistencia en IndexedDB
usePersistentState.ts # useState que sobrevive recargas y cierres de la app
usePushSubscription.ts # suscripción a Web Push (VAPID)
useAppBadge.ts # contador en el ícono de la app instalada
useAppLifecycle.ts # foreground/background + último gancho fiable para persistir
useBackButton.ts # captura el botón atrás de Android para overlays propios
useStorageEstimate.ts # espacio usado, persistencia y limpieza de Cache Storage
usePermission.ts # estado reactivo de un permiso del navegador
useCamera.ts # stream de cámara, cambio de cámara y captura a Blob
useBarcodeScanner.ts # lectura de QR/códigos de barra sobre un <video> en vivo
useGeolocation.ts # ubicación del dispositivo, una vez o en vivo
useWebAuthn.ts # biometría vía WebAuthn (Face ID / huella / Windows Hello)
useClipboard.ts # copiar/leer el portapapeles con feedback temporizado
useFilePicker.ts # elegir/guardar archivos (File System Access + fallback)
useContactPicker.ts # selector de contactos del sistema (Android/Chrome)
useNfc.ts # lectura y escritura de tags NFC (Web NFC)
useWebOTP.ts # autocompleta el código de un SMS de verificación
usePeriodicSync.ts # actualización en segundo plano (PWA instalada)
useLongPress.ts # gesto de mantener presionado, listo para pegar como props
useSwipe.ts # gestos de swipe en las 4 direcciones
useDebounce.ts # useDebounce + useDebouncedCallback + useThrottledCallback
useIdle.ts # inactividad del usuario, con aviso previo
useMediaQuery.ts # media query reactiva + useIsMobile/usePrefersDark/etc.
useNetworkQuality.ts # calidad de conexión para carga adaptativa
useViewTransition.ts # View Transitions API + useScreenStack
useVirtualList.ts # virtualización de listas largas
docs/ # guía de uso de cada componente y hook (ver enlace arriba)
dev/ # playground Next.js que importa components/ real (ver sección Preview)🔧 Instalación
Asume Next.js 15 + Tailwind v4. Se distribuye desde npm:
npm i lib-kit-componentsTambién se puede instalar directo desde GitHub (por ejemplo para probar un commit sin publicar):
npm i github:EmaDev/kit-componentes
# o fijando una versión/commit concreto:
npm i github:EmaDev/kit-componentes#v0.1.0npm install corre automáticamente el script prepare (tsup), que compila dist/ a partir del código fuente — no hace falta commitear el build.
También necesitás los peer dependencies en el proyecto consumidor:
npm i react react-dom framer-motion next-themes(next es peer dependency opcional: sólo hace falta si usás Navbar, SideBar o BottomNav, que usan next/link y next/navigation.)
Estilos y tokens de tema
El paquete expone su CSS de tokens en lib-kit-components/styles.css. Importalo una vez en tu app/globals.css (o equivalente) antes de tus propios estilos, y decile a Tailwind v4 que escanee las clases usadas dentro del paquete (viven compiladas en dist/, fuera de tu content habitual):
@import "tailwindcss";
@import "lib-kit-components/styles.css";
@source "../node_modules/lib-kit-components/dist";Si ya tenés tus propios tokens de tema (--color-primary, etc.) podés omitir el @import del paquete y definir vos las variables — los componentes sólo necesitan que existan.
Sin zoom al enfocar un input (iOS)
styles.css incluye el fix del zoom automático de Safari en iPhone/iPad: al tocar un campo cuyo font-size computado es menor a 16px, iOS hace zoom sobre el campo (y no se puede cancelar por JS ni con user-scalable=no). El paquete impone un piso de 16px para input, textarea, select y [contenteditable] sólo en dispositivos táctiles (@media (hover: none) and (pointer: coarse)), así que en escritorio los campos conservan su text-sm/text-xs de diseño.
- Aplica a los componentes del kit y a los inputs propios de tu app — es CSS global, no hace falta tocar nada.
- Si un campo necesita a propósito una tipografía mayor a 16px (montos, OTP), agregale la clase
text-keep-sizejunto a sutext-*para que conserve el tamaño del diseño:<input className="text-4xl text-keep-size" inputMode="decimal" /> - Como red de seguridad para inputs con
font-sizeinline o!importantde terceros,useNativeFeel/ViewportLock/NativeShelltraen ademáspreventInputZoom(activo por default, y no se desactiva cononlyWhenInstalled: no le saca el pinch-zoom a nadie).
En app/layout.tsx:
import "./globals.css";
import { ThemeProvider } from "next-themes";
import { ToastProvider } from "lib-kit-components";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ToastProvider>{children}</ToastProvider>
</ThemeProvider>
</body>
</html>
);
}Todos los imports de ejemplo en este README (@/components/...) pasan a importarse directo desde "lib-kit-components" una vez instalado como paquete.
🏗️ Guía: setup de una app nueva desde cero (SSR-first)
Checklist completa para arrancar un proyecto nuevo con Next.js + lib-kit-components, con arquitectura atómica en el consumidor y Server Components por default — el cliente se reserva sólo para lo que de verdad necesita interactividad (el shell de la app, formularios, hooks de estado).
Para el contenido del shell (qué componentes montar y en qué orden: safe areas, splash, capa PWA, navegación y FAB), la receta completa está en docs/guides/app-base.md.
Si lo que estás armando es una tienda, la arquitectura completa está en docs/guides/ecommerce.md: server render obligatorio por SEO (metadata, JSON-LD, sitemap), rutas, carrito global, catálogo con skeleton, promo y pantalla de éxito.
Y si es una red social, docs/guides/red-social.md arma el shell completo (splash, safe areas, instalador, campana + drawer, BottomNav de cuatro rutas) y el feed con SocialPost y su caja de comentarios.
1. Crear el proyecto
npx create-next-app@latest mi-app --typescript --tailwind --app --src-dir --import-alias "@/*"
cd mi-appcreate-next-app ya scaffoldea Tailwind v4 (sin tailwind.config.js/postcss.config.js manual, todo vía @import "tailwindcss" en CSS) — verificá "tailwindcss": "^4" en package.json. App Router (--app) es obligatorio: la librería y esta guía asumen Server Components, que no existen en Pages Router.
2. Instalar la librería y sus peer dependencies
npm i github:EmaDev/kit-componentes
npm i framer-motion next-themes(react/react-dom ya vienen con create-next-app; next sólo hace falta si usás Navbar/SideBar/BottomNav, y ya está.)
3. Conectar estilos, tema y providers globales
src/app/globals.css:
@import "tailwindcss";
@import "lib-kit-components/styles.css";
@source "../node_modules/lib-kit-components/dist";src/app/layout.tsx — se queda como Server Component (no lleva "use client"): ThemeProvider y ToastProvider son client components por dentro, pero envolver children con ellos no obliga a que el layout raíz lo sea también.
// src/app/layout.tsx
import "./globals.css";
import { ThemeProvider } from "next-themes";
import { ToastProvider } from "lib-kit-components";
export const metadata = { title: "Mi App", description: "…" };
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ToastProvider>{children}</ToastProvider>
</ThemeProvider>
</body>
</html>
);
}Mantener el RootLayout como Server Component es lo que permite exportar metadata/generateMetadata (SEO) desde acá y desde cada page.tsx — un layout con "use client" no puede exportarlos.
4. Arquitectura atómica del proyecto consumidor
La librería ya te da los átomos y moléculas de UI (Button, Input, Card, Hero, …). Lo que arma el proyecto consumidor son las capas que combinan esos átomos con datos y navegación reales, separando claramente qué corre en el servidor y qué necesita el cliente:
src/
app/
layout.tsx # Server · providers globales (paso 3)
(app)/
layout.tsx # Server · sólo arma <AppShell>{children}</AppShell>
page.tsx # Server · fetch de datos + <HomeTemplate data={...}/>
productos/[id]/page.tsx # Server · fetch por id + <ProductTemplate/>
(app)/AppShell.tsx # Client · único punto donde se monta el shell (splash + PWA + nav + FAB)
components/
atoms/ # primitivos 100% propios que no existen en la librería
molecules/ # combinación de átomos con un propósito (ProductPrice, RatingStars)
organisms/ # bloques de pantalla con estado/interacción (ProductGrid, FiltersBar)
templates/ # arman una pantalla completa a partir de organisms; reciben datos ya
# resueltos por props — NO hacen fetch, así pueden ser Server Components
lib/
data/ # funciones de acceso a datos (fetch/DB/ORM) — se importan sólo desde
# Server Components (page.tsx, layout.tsx)- atoms/molecules: úsalos sólo para lo que la librería no cubre. Todo lo que ya exporta
lib-kit-components(ver Estructura) es tu capa de átomos — no la reimplementes. - organisms: llevan
"use client"en cuanto usan un hook (useState,useCart,useDebounce, etc.) o un componente client de la librería (DataTable,Carousel,Chatbot...). Reciben datos ya cargados por props, no hacen su propio fetch. - templates: pura composición de layout (grid, secciones, orden de organisms). Si no usan hooks, quedan como Server Components — eso es lo que le permite a Next.js streamear el HTML de la pantalla completa antes de que hidrate un solo organism.
- pages (
page.tsx): siempre Server Componentsasync. Son el único lugar autorizado paraawait fetch(...)/queries a la base de datos — nunca en unorganism/templateclient.
5. El shell de la app (el único límite cliente/servidor que importa)
Las piezas del shell (SplashScreen, PwaInstallPrompt, BottomNav, FabActionSheets, …) son client components — pero no hace falta que tu layout.tsx también lo sea. El patrón es aislarlas en un componente cliente propio (AppShell) que recibe children como prop: ese children lo sigue resolviendo el Server Component que lo llama, así que las páginas debajo del shell pueden seguir siendo 100% Server Components con fetch en el servidor.
// src/app/(app)/AppShell.tsx
"use client";
import type { ReactNode } from "react";
import {
NativeShell, SafeArea, SplashScreen, OfflineBanner, PwaInstallPrompt,
BottomNav, FabActionSheets, SnackbarProvider, useSplash,
} from "lib-kit-components";
import { NAV, ACTIONS } from "./shell-config";
export function AppShell({ children }: { children: ReactNode }) {
const { visible, progress } = useSplash({ minDuration: 1500, oncePerSession: true });
return (
<SnackbarProvider position="bottom-center" gap={80}>
<NativeShell onlyWhenInstalled>
<SplashScreen visible={visible} progress={progress} appName="Mi App" variant="bars" background="brand" />
<OfflineBanner position="top" />
<PwaInstallPrompt appName="Mi App" />
<SafeArea edges={["left", "right"]} fillViewport className="flex flex-col bg-surface text-foreground">
<main className="min-w-0 flex-1 md:pb-8">{children}</main>
</SafeArea>
<BottomNav items={NAV} />
<FabActionSheets actions={ACTIONS} mainLabel="Acciones" className="pb-[4.5rem] md:pb-0" />
</NativeShell>
</SnackbarProvider>
);
}Cada capa de ese shell, sus alternativas y sus gotchas (z-index, paddings sobre el BottomNav, safe areas, HeroTabs como nav de pantalla): docs/guides/app-base.md.
// src/app/(app)/layout.tsx — Server Component: no hace fetch, sólo delega
import { AppShell } from "./AppShell";
export default function AppLayout({ children }: { children: React.ReactNode }) {
return <AppShell>{children}</AppShell>;
}// src/app/(app)/page.tsx — Server Component: fetch en el servidor, cero JS de datos al cliente
import { getHomeData } from "@/lib/data/home";
import { HomeTemplate } from "@/components/templates/HomeTemplate";
export default async function HomePage() {
const data = await getHomeData(); // DB/fetch directo, sin API route intermedia
return <HomeTemplate data={data} />;
}// src/components/templates/HomeTemplate.tsx — Server Component (sin "use client")
import { HeroWelcome } from "lib-kit-components";
import { ProductGrid } from "@/components/organisms/ProductGrid";
import type { HomeData } from "@/lib/data/home";
export function HomeTemplate({ data }: { data: HomeData }) {
return (
<>
<HeroWelcome name={data.user.name} highlight={data.highlight} tone="brand" />
<ProductGrid products={data.products} />
</>
);
}ProductGrid (un organism) recién ahí lleva "use client" si necesita useCart(), useState para filtros, etc. — pero ya recibe products resuelto, sin volver a pedirlo al cliente.
6. Por qué este patrón mantiene SSR real
- El HTML de
HomeTemplate(hero + grilla con datos reales) se renderiza en el servidor y llega completo en la respuesta inicial — no depende de que hidrateAppShell. - El único JS que el cliente hidrata "de arriba" es el shell (header, bottom nav, splash, sheet global); el contenido de cada pantalla hidrata sólo los organisms puntuales que lo necesitan (islas de interactividad), no la página entera.
- Evitá el error común de poner
"use client"enlayout.tsx/page.tsx"porque el shell lo pide" — el límite cliente va enAppShell, no en la ruta. - Para confirmarlo:
view-source:sobre la página en el navegador debe mostrar el contenido deHomeTemplateya resuelto en el HTML crudo (sin JS), no un<div id="__next">vacío.
7. PWA mínima (si vas a montar PwaInstallPrompt/UpdatePrompt)
public/manifest.jsonconname,icons,start_url,display: "standalone".public/sw.jscon al menos el handler que necesitaUpdatePrompt:
self.addEventListener("message", (e) => {
if (e.data?.type === "SKIP_WAITING") self.skipWaiting();
});- Registrarlo con
useServiceWorker()(ver docs/hooks y la sección 📱 PWA más abajo).
8. Checklist final
- [ ]
npx tsc --noEmitsin errores. - [ ]
RootLayouty(app)/layout.tsxsin"use client". - [ ] Todo
await fetch/DBvive en unpage.tsx(o una función enlib/data/), nunca en unorganism. - [ ]
view-sourcede cada pantalla muestra el contenido real, no sólo el shell. - [ ]
manifest.json+sw.jssi activasteinstallPrompt/updatePrompt.
🎨 Tokens
Todos los colores usan CSS variables (--color-primary, --color-foreground, …) definidas en globals.css. Cambia los valores allí — los componentes se adaptan solos en claro/oscuro.
Para editarlos en vivo (paleta de marca, superficie, texto y estado) desde la propia UI en vez de tocar el CSS a mano, usá ThemeConfigurator:
// Reskinea toda la app en vivo mientras se ajusta cada color
<ThemeConfigurator applyToDocument />
// …con presets de marca y export a CSS/JSON
<ThemeConfigurator
presets={[{ name: "Océano", tokens: { ...DEFAULT_THEME_TOKENS, primary: "#0891b2", accent: "#06b6d4" } }]}
onChange={(tokens) => console.log(tokens)}
/>Multi-tenant (white-label)
Un mismo deploy, una paleta por cliente — resuelta por dominio o por sesión. TenantThemeProvider inyecta las CSS vars del tenant activo, así todos los componentes se reskinean solos:
// lib/tenants.ts
export const TENANTS: TenantTheme[] = [
{ id: "acme", name: "Acme", domains: ["acme.com", "*.acme.com"],
tokens: { primary: "#e11d48", primaryHover: "#be123c", accent: "#fb7185" } },
{ id: "globex", name: "Globex", domains: ["globex.io", "*.globex.io"],
tokens: { primary: "#0891b2", primaryHover: "#0e7490", accent: "#06b6d4" },
dark: { primary: "#22d3ee", surface: "#082f49" } },
];// app/layout.tsx — Server Component: resolver en el servidor evita el flash de marca
const host = (await headers()).get("host") ?? undefined;
<TenantThemeProvider themes={TENANTS} host={host}> {/* por dominio */}
<TenantThemeProvider themes={TENANTS} tenantId={session?.tenantId ?? null}> {/* por auth */}// …y desde cualquier client component
const { tenant, themes, setTenant, tokens, setTokens, css } = useTenantTheme();Detalles (precedencia de resolución, herencia claro→oscuro, tenants desde la base de datos): TenantThemeProvider.
🖼 Superficies & media
// Cards — variant: elevated | outline | flat | gradient | glass
<Card variant="elevated" padding="md" interactive>…</Card>
<StatCard label="MRR" value="$48.2k" delta={12.4} tone="primary" spark={[8,10,9,13]}/>
<MediaCard src="/casa.jpg" badge="Nuevo" title="Casa Aldama" horizontal
description="Reforma integral de 140 m²." actions={<Button size="sm">Ver</Button>}/>
<ProfileCard name="Lucía Marín" role="Product designer" cover
stats={[{ label: "Proyectos", value: 12 }]}/>
<PricingCard plan="Pro" price="$29" highlight badge="Popular"
features={["Proyectos ilimitados", "Soporte prioritario"]} cta={<Button/>}/>
// Carrusel — drag, flechas, dots, miniaturas, autoplay, varias por vista
<Carousel images={[{ src, alt, caption }]} perView={2} peek={56}
aspect={16/9} loop autoplay={2600} thumbs zoomable/>
// Carrusel de CARDS (cualquier contenido) — track | peek | coverflow | snap
<CardCarousel perView={4}>{productos.map(p => <ProductCard key={p.id} {...p}/>)}</CardCarousel>
<CardCarousel variant="peek" peek={0.14} autoplay={5000} loop>{promos}</CardCarousel>
<CardCarousel variant="coverflow" focusRatio={0.55}>{portadas}</CardCarousel>
<CardCarousel variant="snap" itemWidth={220} index={i} onIndexChange={setI}>{articulos}</CardCarousel>
// Imagen con pan y zoom — sólo la imagen; bloquea scroll, pinch del navegador,
// ctrl+scroll, pull-to-refresh, long-press y clicks fuera del visor
<ZoomableImage src="/plano.png" caption="A-01 · 1:50"/>
<ImageZoom open={open} onClose={close} src={img.src} maxScale={6}
onPrev={prev} onNext={next}/>
// Tabs
<Tabs items={items} value={tab} onChange={setTab}
variant="segmented" size="md" fitted scrollable
panels={{ resumen: <Resumen/>, actividad: <Actividad/> }}/>
// TabsGlow — pastilla flotante con overshoot elástico + glow
<TabsGlow items={items} value={tab} onChange={setTab} size="md" panels={panels}/>
// TabsCarousel — línea corta centrada, panel se desliza según la dirección
<TabsCarousel items={items} value={tab} onChange={setTab} panels={panels}/>
// TabsDock — iconos con rebote elástico y punto indicador, estilo dock
<TabsDock
items={[{ id: "inicio", label: "Inicio", icon: <HomeIcon/> }]}
value={tab} onChange={setTab} panels={panels}
/>
// VideoPlayer — scrub con buffer y marcadores, atajos de teclado, doble-tap para saltar
<VideoPlayer
src="/videos/keynote.mp4" poster="/videos/keynote-poster.jpg"
title="Keynote 2025" resumeKey="video-keynote-2025"
markers={[{ at: 120, label: "Intro" }]}
/>
// …o modo portrait, feed vertical tipo reels
<VideoPlayer src={clip.url} orientation="portrait" loop autoPlay muted
onNext={nextClip} onPrev={prevClip}
overlay={<ClipActions clip={clip} />}
/>Gestos del visor: arrastrar = pan · rueda o pinch = zoom hacia el puntero · doble click = 250% ↔ reset · + / − / 0 · ← → para recorrer la galería · Esc cierra. El pan está limitado para que la imagen nunca se escape de la pantalla.
🦸 Heroes
// Cabecera con buscador — sugerencias frecuentes + resultados en vivo
<HeroSearch
title="Encontrá tu próximo lugar" eyebrow="1.284 propiedades activas"
placeholder="Barrio, calle o código…" cta="Buscar"
suggestions={["Palermo", "Belgrano", "2 ambientes"]}
results={liveResults} onSubmit={(q) => router.push(`/buscar?q=${q}`)}
/>
// Cabecera con imagen a sangre — overlay, metadatos y acciones
<HeroImage
src="/casa.jpg" eyebrow="Obra terminada" title="Casa Aldama"
description="Reforma integral de 140 m² en dos plantas."
meta={[{ label: "Superficie", value: "140 m²" }, { label: "Año", value: "2025" }]}
actions={<Button>Ver proyecto</Button>} height={420} overlay="gradient"
/>
// Cabecera con pestañas horizontales scrolables
<HeroTabs
title="Bandeja de entrada"
tabs={[{ id: "todo", label: "Todo", count: 128 }, { id: "hoy", label: "Hoy", count: 12 }]}
value={tab} onChange={setTab} variant="underline"
panels={{ todo: <Todo />, hoy: <Hoy /> }}
/>
// Saludo de bienvenida — home de la app instalada
<HeroWelcome
name="Lucía Marín" avatar={user.photo} subtitle="Cuenta personal · **** 4417"
highlight={{ label: "Saldo disponible", value: "$248.320", delta: "+4,2%" }}
quickActions={[{ id: "enviar", label: "Enviar", icon: <SendIcon /> }]}
onQuickAction={(id) => go(id)} tone="brand"
/>🧱 Bloques de app
// Fila de chips — categorías, filtros (multi), personas, o "cover" con imagen de fondo
<ChipCarousel chips={categorias} value={cat} onChange={setCat} variant="soft" size="md" />
<ChipCarousel chips={filtros} value={tags} onChange={setTags} multi variant="outline" />
// Bloqueo por PIN o contraseña al abrir la app
<PinLock
open={locked} mode="pin" length={4} appName="Mi App"
onUnlock={async (code) => await verifyPin(code)}
onSuccess={() => setLocked(false)}
maxAttempts={5} onBiometric={() => webauthnLogin()}
/>
// Carga de montos a pantalla completa, estilo billetera
<AmountPad
open={open} onClose={() => setOpen(false)}
balance={saldo} min={100} max={500000}
quickAmounts={[1000, 5000, 10000]}
onConfirm={async (amount) => await recargar(amount)}
/>
// Redirección con cuenta atrás — WhatsApp | Telegram | SMS | mail | URL
<RedirectTimer
target="whatsapp" phone="5491122334455"
message="Hola 👋 quiero consultar por el plan Pro."
seconds={8} onRedirect={(href) => track("wa_redirect", href)}
/>
// Compartir — hoja nativa del sistema, o sheet propio de fallback
<ShareButton title="Casa Aldama" text="Mirá esta propiedad" onShared={(m) => track("share", m)} />
// Grilla de cards con columnas ajustables en tiempo real
<CardGrid
items={propiedades} renderItem={(p) => <PropertyCard key={p.id} {...p} />}
defaultColumns={3} min={1} max={5} minCardWidth={190} storageKey="grid.cols"
/>Keypad es la pieza de bajo nivel detrás de AmountPad y PinLock (teclado numérico 3×4 con tecla extra y borrado en long-press) — usalo directo sólo si necesitás armar un flujo numérico propio.
🧪 Preview y desarrollo local
dev/ es un playground real con Next.js (App Router) que importa los componentes directamente desde components/ (mismo Tailwind v4 + Framer Motion que consume cualquier proyecto) — no un mock.
cd dev
npm install
npm run dev
# abrí http://localhost:3000Es una sola pantalla: un catálogo con un preview por componente, ordenado en las 6 categorías del inventario (Átomos · Moléculas · PWA & nativo · Config & hooks · Nicho · Otros), con índice lateral y buscador por nombre. Cada preview tiene su ancla propia (/#datatable, /#bottomsheet, …).
Al ser una app Next.js de verdad, Navbar, SideBar y BottomNav (que usan next/link/next/navigation) también corren en vivo, sin mocks.
Para agregar un componente nuevo al playground (y el resto de los pasos obligatorios al crear uno): ver CLAUDE.md.
Variables de entorno del playground
dev/ lee estas variables (ver dev/.env.example) para armar los links del header/hero/footer — copiá el archivo a dev/.env.local para desarrollo local:
| Variable | Uso | Default |
| --- | --- | --- |
| NEXT_PUBLIC_REPO_URL | Botones "GitHub" / "Ver código" | https://github.com/EmaDev/kit-componentes |
| NEXT_PUBLIC_DEMO_URL | URL pública del propio deploy (referencia, ej. para compartir) | https://lib-kit-components.vercel.app |
| NEXT_PUBLIC_LIB_VERSION | Versión mostrada en el footer | 0.1.0 |
| NEXT_PUBLIC_DONATE_URL | Link de donaciones en el footer (se oculta si no está seteada) | (vacío) |
Todas son NEXT_PUBLIC_* porque solo arman URLs en la UI, no hay nada sensible.
Deploy del playground en Vercel
El código fuente (components/, hooks/, docs/) vive en un repo de GitHub; el playground de dev/ se deploya aparte, como demo pública:
- En Vercel, New Project → importar este repo.
- Root Directory:
dev(el playground tiene su propiopackage.json). - Cargar las variables de la tabla de arriba en Settings → Environment Variables (con la URL real del repo y, una vez asignado, el dominio que te dio Vercel para
NEXT_PUBLIC_DEMO_URL). - Deploy. Framework preset: Next.js (autodetectado).
El resto del repo (paquete instalable) no necesita deploy — se consume vía npm i lib-kit-components como se explica en Instalación.
📚 Uso rápido
// Button
<Button variant="primary" size="md" loading={false}>Guardar</Button>
// Input
<Input label="Email" type="email" leftIcon={<MailIcon />} error="..." />
// Select
<Select options={[{value:"a",label:"A"}]} onChange={v => …} />
// Toast
const { toast } = useToast();
toast({ title: "¡Listo!", variant: "success" });
// Modal
<Modal open={open} onClose={…} title="Hola" footer={<Button>OK</Button>}>
…
</Modal>
// BottomSheet — auto | xs | sm | md | lg | xl | full
<BottomSheet open={open} onClose={…} size="md" title="Elegí una opción"
footer={<Button>Confirmar</Button>}>
…
</BottomSheet>
// …o con alturas arrastrables
<BottomSheet snapPoints={[0.35, 0.65, 0.92]} defaultSnap={0} … />
// Tooltip
<Tooltip content="Eliminar producto">
<Button variant="ghost" size="icon"><TrashIcon /></Button>
</Tooltip>
// Popover
<Popover trigger={<Button variant="secondary">Filtros</Button>}>
<div className="flex flex-col gap-3 w-56">…</div>
</Popover>
// CoachMark — tour guiado con spotlight
<CoachMark
open={tourOpen}
onClose={() => setTourOpen(false)}
steps={[
{ target: "#nav-search", title: "Buscá lo que necesites" },
{ target: "#cart-button", title: "Tu carrito", side: "left" },
]}
/>
// Switch
<Switch checked={notifications} onChange={setNotifications} label="Notificaciones push" />
// CodeOTP
<CodeOTP length={6} label="Código de verificación" onComplete={(code) => verifyCode(code)} />
// ScrollArea — variant: thin | pill | glow | debounce
<ScrollArea variant="pill" maxHeight={280}>
<div className="flex flex-col gap-3 p-1">…</div>
</ScrollArea>
// Footer
<Footer
brand={<span>Mi Tienda</span>}
groups={[{ title: "Producto", links: [{ label: "Catálogo", href: "/catalogo" }] }]}
bottomLinks={[{ label: "Privacidad", href: "/privacidad" }]}
/>
// NotificationPanel / NotificationBell — historial agrupado por fecha
<NotificationBell
items={notifications}
onRead={markAsRead}
onReadAll={markAllAsRead}
onDismiss={dismiss}
/>
// …el mismo panel como centro de notificaciones lateral, con backdrop y Escape
<NotificationSidebar
open={open} onClose={() => setOpen(false)} side="right" width={420}
items={notifications} onRead={markAsRead} onReadAll={markAllAsRead}
/>📊 Datos y grillas
// Tabla profesional
const columns: Column<Person>[] = [
{ key: "name", header: "Persona", width: "minmax(200px,1.4fr)",
render: r => <PersonCell row={r} /> },
{ key: "mrr", header: "MRR", align: "right", sortValue: r => r.mrr },
];
<DataTable
columns={columns} rows={people} rowKey={r => r.id}
selectable searchable pageSize={6}
density="normal" stickyHeader maxHeight="420px"
onRowClick={openDetail}
rowActions={r => <RowMenu row={r} />}
/>
// Hoja de cálculo editable
<Spreadsheet
rows={24} cols={8} height="420px" headerRow
initial={{ A1: "Mes", B1: "Ingresos", B7: "=SUM(B2:B6)" }}
onChange={grid => save(grid)}
/>
// Grilla de calendario
<CalendarGrid
events={events} weekStartsOn={1} maxPerDay={3}
onDayClick={openDay} onEventClick={openEvent}
/>
// Orden con reacomodo animado + celdas que se resaltan al cambiar de valor
<AnimatedTable
columns={columns} rows={liveStats} rowKey={s => s.id}
sortable highlightChanges density="compact"
/>
// Fila con panel de detalle desplegable
<ExpandableTable
columns={columns} rows={pedidos} rowKey={o => o.id}
renderDetail={o => <DetallePedido pedido={o}/>}
multiple
/>AnimatedTable y ExpandableTable reutilizan el mismo tipo Column<T> que DataTable, así que podés pasarles las mismas definiciones de columna. AnimatedTable usa key, header, width, align, sortable, render y sortValue; ExpandableTable usa key, header, width, align y render. Ninguna de las dos aplica hideOnMobile.
Ninguna trae búsqueda, selección ni paginado a propósito — para eso está DataTable. Dos detalles: highlightChanges sólo resalta columnas sin render (el resalte se dispara cuando cambia el string del valor crudo), y en ExpandableTable toda la fila es el disparador, así que un control interactivo dentro de una celda necesita e.stopPropagation() para no desplegar el panel.
Atajos de la hoja de cálculo: flechas · ⇧+flechas (rango) · ⌘/Ctrl+flechas (extremos) · Tab/⇧Tab · Enter/F2 (editar) · escribir (reemplazar) · Esc · Delete · ⌘/Ctrl+C/X/V (TSV, compatible con Excel y Sheets) · ⌘/Ctrl+Z/⇧Z · ⌘/Ctrl+A · Home/End.
Fórmulas (evaluador propio, sin eval()): operadores + - * / ^ ( ), referencias, rangos, y SUM AVERAGE/AVG MIN MAX COUNT ABS ROUND. Errores tipados: #DIV/0!, #NAME?, #REF!, #CIRC!.
🔁 Listas, pickers & feedback
// Imagen con contador — 03 / 12 superpuesto, arrastre, teclado, zoom
<ImageCounter
images={fotos} // [{ src, alt, caption }]
counter="pill" // pill | bar | dots
position="top-right" // …-left | bottom-* | bottom-center
aspect={4/3} pad badge="Destacada" thumbs zoomable
onIndexChange={setIndex}
/>
// Snackbar — uno a la vez, cola FIFO, acción inline, swipe para descartar
<SnackbarProvider position="bottom-center">{children}</SnackbarProvider>
const { snack, undo, dismiss } = useSnackbar();
snack({ message: "Cambios publicados", variant: "success" });
undo("«Factura #1042» eliminada", () => restore(row));
// Date picker — simple o rango, atajos, límites, días bloqueados
<DatePicker
value={date} onChange={setDate}
label="Fecha de la visita" min={hoy} max={enDosMeses}
disabledDate={d => d.getDay() === 0 || d.getDay() === 6}
presets={[{ label: "Hoy", value: () => new Date() }]}
weekStartsOn={1} locale="es-AR"
/>
<DatePicker mode="range" months={2} value={range} onChange={setRange}/>
// Time picker — horas/minutos (y segundos opcional), 12h o 24h, atajos, límites
<TimePicker
value={time} onChange={setTime}
label="Hora de la reserva" step={15}
min="09:00" max="18:00"
disabledTime={h => h === 13}
/>
<TimePicker hour12 value={time} onChange={setTime}/>
// Paginado — elipsis, extremos, resumen y tamaño de página
<Pagination
page={page} total={248} pageSize={pageSize}
onPageChange={setPage} onPageSizeChange={setPageSize}
siblings={1} edges summary
/>
// Pull to refresh — sólo con el scroll arriba del todo
<PullToRefresh onRefresh={() => mutate()} threshold={72} height="100%">
<Feed items={items}/>
</PullToRefresh>Snackbar es para una acción y uno a la vez (confirmaciones, «deshacer»); Toast es para notificaciones que se acumulan. PullToRefresh usa overscroll-behavior-y: contain para no pelearse con el gesto del navegador.
🛒 Comercio & conversión
🧭 Si estás armando una tienda entera y no una pantalla suelta, arrancá por docs/guides/ecommerce.md: rutas, server render por SEO, carrito global y cómo se combinan todas estas piezas.
// Tarjeta de producto — el mismo set de props en 5 layouts
<ProductCard // grid | list | compact | offer | editorial
brand="Aurora Home" title="Sillón Nórdico Roble"
price={189900} compareAtPrice={249900} // tachado + badge "24% OFF" automático
rating={4.6} reviewCount={128}
colors={[{ id: "roble", label: "Roble", hex: "#c8a06a" }]}
installments="3 cuotas sin interés de $63.300"
onClick={verDetalle} onAddToCart={() => cart.add(p)}
/>
<ProductCard variant="list" title="Lámpara Halo" price={74500}
stock={3} quantity={qty} onQuantityChange={setQty} // stepper con tope de stock
shippingNote="Envío gratis" deliveryNote="Llega el jueves" onAddToCart={agregar}/>
<ProductCard variant="compact" title="Vela Cedro" price={12900} onAddToCart={agregar}/>
<ProductCard variant="offer" title="Mesa ratona Linden" price={129900} compareAtPrice={215000}
until={finDeLaPromo} onExpire={refrescarPrecios} sold={68} total={100} onAddToCart={agregar}/>
<ProductCard variant="editorial" brand="Verano en el sur" title="Set de exterior Dune"
description="Teca y cuerda náutica." price={412000} image="/dune.jpg" onAddToCart={agregar}/>
// Grilla que se acomoda sola: columnas por ancho, recortadas por cantidad,
// y la variante de card la elige según el ancho de columna que quedó
<ProductGrid
products={productos} // props de ProductCard + id
minCardWidth={220} maxCardWidth={340} maxColumns={5}
onProductClick={verDetalle} onProductAdd={agregarAlCarrito}
onLayoutChange={({ columns, variant }) => console.log(columns, variant)}
/>
const cart = useCart();
<CartButton count={cart.count} onClick={openSheet} variant="ghost"/>
<CartButton count={cart.count} bump="count"/> // icon | count | none
<CartPanel
lines={cart.lines} onQtyChange={cart.setQty}
onRemove={cart.remove} onClear={cart.clear} // el vaciado se anima antes de llamar
shipping={0} discount={cupon}
footer={<Button fullWidth>Finalizar compra</Button>}
/>
// Popup de promociones
<PromoPopup
open={open} onClose={close}
eyebrow="Sólo por hoy" highlight="30% OFF"
title="Llevate el 30% en toda la colección"
image="/promo.jpg"
layout="center" // center | side-image | bottom-sheet
delay={4000} snoozeDays={7}
emailCapture={{ onSubmit: sendCoupon, note: "Sin spam." }}
cta={{ label: "Ver ofertas", onClick: go }}
/>
// Cupón temporal — timer, cupos, o los dos
<CouponCode code="HOTSALE30" label="30% OFF en toda la tienda"
expiresAt={endOfSale} onExpire={refreshOffers} onCopy={track}/>
<CouponCode code="ENVIOGRATIS" uses={{ used: 37, total: 50 }} tone="success"/>
// Banner de cuenta regresiva
<CountdownBanner
until={endOfSale} eyebrow="Hot Sale" title="La oferta termina en"
variant="boxes" // boxes | flip | bar
tone="danger" sticky="top"
cta={{ label: "Ver ofertas", onClick: go }}
dismissible snoozeDays={1}
expiredMessage="La promoción terminó." onExpire={refreshPrices}
/>
// Cuenta regresiva gigante — sólo números
<CountdownHero until={launchDate} variant="minimal" size="xl" tone="dark"/>
// …o con título y bajada
<CountdownHero
until={endOfSale} from={startOfSale}
eyebrow="Hot Sale"
title="Todo hasta 50% OFF"
subtitle="La liquidación termina el domingo a las 23:59."
variant="flip" // blocks | flip | minimal
size="lg" // md | lg | xl
tone="gradient" // surface | primary | danger | accent | dark | gradient
cta={{ label: "Ver ofertas", onClick: go }}
expired="La promo terminó" onExpire={refreshPrices}
/>ProductCard maneja el favorito, el color y la cantidad sola si no le pasás las props controladas, y sólo dibuja el botón de agregar si le pasás onAddToCart. El badge de CartButton entra con spring y salta en cada incremento; con bump="count" el icono queda quieto y se agranda sólo el número; CartPanel anima cada línea y despide todas en cascada al vaciar. CouponCode pulsa en rojo en el último minuto y se tacha al vencer o agotarse. CountdownHero es el hermano grande de CountdownBanner: dibuja la cabecera sólo si le pasás eyebrow/title/subtitle, así que sin ellas queda un contador de números puros.
💬 Social, lectura & chat
🧭 Si estás armando una red social entera y no una pantalla suelta, arrancá por docs/guides/red-social.md: shell, navegación, notificaciones, compartir y cómo se combinan todas estas piezas.
// Chatbot — burbujas, «escribiendo…», quick replies
const [msgs, setMsgs] = useState<ChatMessage[]>([]);
<Chatbot
messages={msgs}
onSend={async text => {
setMsgs(m => [...m, { id: uid(), role: "user", text, at: Date.now() }]);
const answer = await askBot(text); // el input queda bloqueado
setMsgs(m => [...m, { id: uid(), role: "bot", ...answer }]);
}}
botName="Asistente" starters={["Envíos", "Pagos", "Devoluciones"]}
variant="floating" // floating | inline
unread={2} footnote="Respuestas automáticas."
/>
// Lector paginado tipo Google Books
<BookReader
title="Las sillas de la calle Aldama" author="Irene Costa"
chapters={[{ id: "c1", title: "I · El taller", paragraphs: […] }]}
spread="auto" // auto | single | double
theme="sepia" // light | sepia | dark
fontSize={19} height={560}
storageKey="reader.aldama" // recuerda capítulo + página
onProgress={pct => save(pct)}
/>
// Post de red social — con su caja de comentarios básica incluida
<SocialPost
author={{ name: "Estudio Aldama", handle: "@aldama", verified: true }}
time="hace 2 h" text={post.body} counts={{ likes: 1284, comments: 96 }}
currentUser={{ name: "Lucía Marín", avatar: user.photo }}
comments={[{ id: "c1", author: "Martín Paz", text: "Buenísimo 👏", at: Date.now() - 480_000, likes: 4 }]}
visibleComments={2} // el resto, detrás de «ver los anteriores»
onLikeComment={(id, liked) => reactToComment(id, liked)}
/>
// …y cualquier otra cosa (encuesta, CommentBox completo) se cuelga abajo como children
<SocialPost
author={{ name: "Estudio Aldama", handle: "@aldama", verified: true }}
time="hace 2 h" text={post.body} media={[{ src, alt }]}
counts={{ likes: 1284, comments: 96, shares: 34 }}
onLike={liked => react(post.id, liked)} onSave={bookmark}
onComment={openThread} onShare={share} onMedia={i => openGallery(i)}
>
<Poll question="¿Con qué madera armamos la próxima serie?"
options={[{ id: "a", label: "Roble", votes: 412 }]}
kind="single" onVote={async ids => await api.vote(pollId, ids)}/>
</SocialPost>
// Caja de comentarios — hilos de una respuesta
<CommentBox
comments={comments} // planos, con parentId en las respuestas
currentUser={{ name: "Lucía", avatar: user.photo }}
onSubmit={async (text, parentId) => await api.comment({ text, parentId })}
onLike={(id, liked) => api.likeComment(id, liked)}
maxLength={280} pageSize={4} // fijados primero, después los más recientes
/>
// Pantalla de éxito con confeti
<SuccessPage
title="¡Pago confirmado!" headline="$248.320"
description="Te mandamos el comprobante por mail."
details={[{ label: "Operación", value: "#A-10428" }]}
primary={{ label: "Ver mi pedido", href: "/pedidos/A-10428" }}
confetti="burst" // burst | rain | center | false
tone="success" variant="full" // full (100dvh) | card
redirectIn={10} onRedirect={() => router.push("/")}
/>
// Encuesta anónima — voto secreto, sin resultados ni totales
<Poll
question="¿Cómo calificás el clima del equipo este trimestre?"
description="Tu respuesta no queda asociada a vos."
kind="single" anonymous
anonymousNote="Votación secreta. Los resultados se publican el viernes."
options={[{ id: "a", label: "Muy bien", votes: 0 }, { id: "b", label: "Regular", votes: 0 }]}
onVote={async ids => await api.voteAnonymous(pollId, ids)}
/>
// …y con voto modificable: onVote recibe el voto que se reemplaza
<Poll
question="¿Con qué madera armamos la próxima serie?"
options={options} kind="single" allowChangeVote
onVote={async (ids, previous) => await api.vote(pollId, ids, { replaces: previous })}
/>
// Encuesta visual — se vota una imagen o un video desde un carrusel
<Poll
question="¿Qué spot publicamos esta semana?"
kind="single" layout="media"
mediaVariant="peek" // peek | track | coverflow | snap
mediaAspect="16 / 9"
mediaSelector // listado de opciones clásico debajo del carrusel (default true)
options={[
{ id: "a", label: "Spot «Taller»", video: "/spots/taller.mp4", poster: "/spots/taller.jpg", votes: 0 },
{ id: "b", label: "Portada B", image: "/portadas/b.jpg", votes: 0 },
]}
onVote={send}
/>
// …o el confeti suelto, sobre cualquier contenedor relative
<Confetti fire={shot} mode="center" count={160}/>Chatbot bloquea el input mientras onSend esté pendiente. BookReader pagina con column-count + column-fill: auto y repagina solo al cambiar tipografía o rotar el dispositivo, sin perder el capítulo. SocialPost recorta el texto a 240 caracteres con «ver más», y su caja de comentarios incluida se activa sola con comments, commentBox o onAddComment: sin onAddComment administra la lista por su cuenta, con él sólo avisa. CommentBox no anida más de un nivel a propósito. Poll con anonymous no muestra resultados ni totales en ningún momento —tampoco le devuelve al usuario lo que votó—, pero es anonimidad de pantalla: lo que mandás en onVote lo decidís vos. Con allowChangeVote la encuesta vuelve a estado votable y onVote te pasa el voto anterior para que el backend reemplace en vez de sumar. Con layout="media" las opciones se votan desde un carrusel (CardCarousel por dentro): los videos van muteados y en loop, sólo suena el de la tarjeta a la vista, y deslizar no cuenta como voto. Confetti es canvas puro, sin dependencias, y respeta prefers-reduced-motion.
📱 PWA
// Prompt de instalación (Android nativo + instrucciones iOS)
<PwaInstallPrompt appName="Mi App" snoozeDays={14} />
// Botón embebible (header / ajustes)
<InstallButton size="sm" variant="outline" onIosClick={() => setShowIosHelp(true)} />
// Conectividad — se muestra solo cuando hace falta
<OfflineBanner position="top" />
// Nueva versión lista (requiere sw.js con SKIP_WAITING)
<UpdatePrompt swUrl="/sw.js" />
// Opt-in de notificaciones
<NotificationOptIn onResult={(s) => console.log(s)} />
// Diagnóstico en Ajustes
<PwaStatus />En tu public/sw.js agregá el handler que UpdatePrompt necesita:
self.addEventListener("message", (e) => {
if (e.data?.type === "SKIP_WAITING") self.skipWaiting();
});📲 Plataforma y experiencia nativa
// Qué dispositivo es — SSR-safe (hydrating: true en el primer render)
const {
os, // ios | ipados | android | macos | windows | linux
browser, // safari | chrome | firefox | edge | samsung | webview
formFactor, // mobile | tablet | desktop
displayMode, // browser | standalone | minimal-ui | fullscreen | twa
isStandalone, // corre instalada
isIos, isAndroid, isMobileOs, isTouch, isWebView,
safeArea, // { top, right, bottom, left } en px reales
prefersReducedMotion, pixelRatio,
} = usePlatform();
// Bloquear zoom y gestos del navegador — declarativo
<ViewportLock onlyWhenInstalled /> // recomendado: sólo con la PWA instalada
<ViewportLock onlyOnMobile />
// …o imperativo, con control fino
useNativeFeel({
blockZoom: true, // pinch · doble-tap · ctrl+scroll · ctrl +/- · viewport meta
blockOverscroll: true, // pull-to-refresh y rebote del body
blockContextMenu: true, // long-press / click derecho (respeta inputs)
blockTextSelection: true, // fuera de inputs y textareas
preventInputZoom: true, // iOS: sin zoom al enfocar un campo (piso de 16px)
});Detalles: en iOS el pinch-zoom sólo se frena reescribiendo el <meta name="viewport">, así que useNativeFeel lo parchea (y lo restaura al desmontar). usePlatform detecta el iPad que se hace pasar por Mac y los WebViews embebidos (Instagram, Facebook, TikTok…).
preventInputZoom es aparte del resto: no es un bloqueo (el usuario conserva su pinch-zoom), sólo evita el salto de zoom al tocar un input. Por eso se aplica siempre, aunque montes <ViewportLock onlyWhenInstalled />, y no incumple WCAG. Ver Sin zoom al enfocar un input (iOS).
⚠️ Accesibilidad: bloquear el zoom incumple WCAG 1.4.4. Usá
onlyWhenInstalledpara que en el navegador el usuario conserve su zoom, y ofrecé un control propio de tamaño de texto.
🚀 Splash screen
const { visible, progress } = useSplash({
minDuration: 1400, // nunca menos que esto (evita el flash)
until: () => loadSession(), // …y esperá esta promesa
waitForFonts: true,
oncePerSession: true, // opcional
});
<SplashScreen
visible={visible}
progress={progress} // lo usa variant="bars"
variant="zoom" // fade | pulse | orbit | bars | zoom | wipe
background="brand" // surface | brand | dark | cualquier CSS
appName="Mi App"
tagline="Tu frase corta"
version="1.4.0"
footnote="build 2f9a1c"
icon={<img src="/icon.svg" alt="" />}
/>Las 6 variantes definen entrada y salida: fade (sobrio), pulse (anillos concéntricos), orbit (punto orbitando), bars (barra de progreso real), zoom (spring in + escala al salir, estilo iOS) y wipe (dos paneles que se abren revelando la app).
📐 Safe areas y shell nativo
// Raíz: bloqueos + barra del navegador escondida + vars publicadas
<NativeShell onlyWhenInstalled>{children}</NativeShell>
// Por pantalla, elegí qué bordes respetar
<SafeArea edges={["top"]} gutter={12} as="header">…</SafeArea>
<SafeArea edges={["bottom"]} avoidKeyboard as="footer">…</SafeArea>
<SafeAreaSpacer edge="bottom" /> // cierre de una lista scrolleable
// Hooks sueltos
const sa = useSafeArea(); // { top, bottom, hasInsets, orientation, …