npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

next-recomponents

v2.0.104

Published

description nueva

Readme

next-recomponents

Componentes y hooks de React para aplicaciones Next.js: tablas, formularios, modales, popups, calendario, visor de documentos y utilidades para consumir APIs, fechas y Excel.

Contenido


Instalación y configuración

npm install next-recomponents

Dependencias que debe tener tu proyecto

La librería no las incluye; tu aplicación debe tenerlas instaladas:

| Paquete | Para qué se usa | | ------------------ | ------------------------------------------------ | | react, react-dom | Todo | | next (13 o superior, App Router) | Container (next/link, usePathname) | | axios | useResources | | moment | useDates | | react-datepicker | MyCalendar |

npm install axios moment react-datepicker

Tailwind CSS

Los componentes usan clases de Tailwind. Agrega la librería a content en tailwind.config para que Tailwind genere esas clases:

// tailwind.config.js
module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}",
    "./node_modules/next-recomponents/dist/**/*.{js,mjs}",
  ],
};

Estilos del calendario

MyCalendar necesita la hoja de estilos del paquete. Impórtala una vez, por ejemplo en app/layout.tsx:

import "next-recomponents/dist/index.css";

Componentes de cliente

Los componentes usan estado y efectos, así que deben usarse desde un componente de cliente. Agrega "use client" al inicio del archivo que los importa:

"use client";
import { Button, Table } from "next-recomponents";

Escala de colores compartida

Button, Alert, Modal y usePopup aceptan la misma propiedad color:

"primary" (por defecto en la mayoría) · "secondary" · "info" · "danger" · "warning" · "success" · "white"


Table / TableAdvanced

Table y TableAdvanced son el mismo componente. Según lo que reciba en data muestra:

  • Un arreglo de filas → tabla con encabezados, orden, filtros, búsqueda, edición, selección, totales, paginación y modal por fila. Cada fila debe tener id.
  • Un solo objeto { campo: valor } → ficha vertical de dos columnas (campo | valor).
  • Un arreglo vacío u otro valor → no muestra nada.
import { Table } from "next-recomponents";

const usuarios = [
  { id: 1, nombre: "Ana López", correo: "[email protected]", saldo: 1250.5 },
  { id: 2, nombre: "Juan Pérez", correo: "[email protected]", saldo: 980 },
];

<Table data={usuarios} header="Usuarios" searchable exportName="usuarios" />;

Comportamiento automático

  • Columnas: se toman de las claves de todas las filas. Las que empiezan con _ no se muestran (útil para datos internos como _meta).
  • Ordenar: clic en el nombre de la columna alterna ascendente / descendente. Los valores vacíos siempre quedan al final.
  • Menú de columna (ícono junto al encabezado): ordenar de A a Z / Z a A y filtrar por valores. Solo puede haber un filtro activo a la vez.
  • Redimensionar: arrastra el borde derecho de un encabezado.
  • Paginación: con más de 100 filas visibles aparece una barra de páginas de 100 en 100.
  • Vista de tarjetas: si el contenedor de la tabla mide menos de 640 px de ancho (celular o panel angosto), cada fila se muestra como una tarjeta, con selector de orden y "seleccionar todo".
  • Formato de valores:
    • Fechas AAAA-MM-DD (con o sin hora) se muestran como DD/MM/AAAA.
    • Números con decimales se muestran con 2 decimales y separador de miles (1,250.50), precedidos de currentCoin si se indica.
    • Elementos de React se muestran tal cual; objetos y arreglos, como JSON.

Props de la tabla (arreglo de filas)

| Prop | Tipo | Por defecto | Descripción | | --- | --- | --- | --- | | data | object[] | — | Filas. Todas deben tener id. | | header | ReactNode | — | Título sobre la tabla. | | searchable | boolean | false | Muestra el buscador. Busca por palabras: una fila coincide si cada palabra aparece en alguno de sus campos ("juan perez" encuentra nombre y apellido en columnas distintas). Resalta las celdas que coinciden. | | exportName | string | — | Muestra el botón Exportar y descarga exportName.xlsx con todas las filas (sin columnas ocultas ni campos que empiezan con _). | | hideColumns | string[] | [] | Columnas que no se muestran. | | editableFields | string[] | — | Columnas editables en línea. El cambio se aplica al salir del campo. Si el valor original era número y lo escrito es un número válido, se guarda como número. | | onSave | (rows) => void \| Promise | — | Muestra el botón Guardar cambios (con el número de filas editadas). Recibe todas las filas con sus ediciones. Mientras la promesa no se resuelve, la tabla muestra "Guardando…". | | onSaveLabel | string | "Guardar cambios" | Texto del botón de guardar. | | onSelect | (rows) => void \| Promise | — | Agrega una columna de casillas y el botón Guardar selección; recibe las filas marcadas. No se puede usar junto con onSave. | | onSelectLabel | string | "Guardar selección" | Texto del botón de selección. | | buttons | Record<columna, ReactElement> | — | Reemplaza el contenido de una columna por un control (botón, input, select, componente propio). Ver Controles en celdas. | | modal | ReactElement | — | Agrega una columna con botón de edición que abre este contenido a pantalla completa. Ver Modal por fila. | | modalButton | ReactElement | ícono de lápiz | Botón propio para abrir el modal. Su onClick se sigue ejecutando. | | onCloseModal | (row) => boolean \| Promise<boolean> | — | Se llama al intentar cerrar el modal; devolver false lo mantiene abierto. | | footer | Record<columna, "sum" \| "avg" \| "count"> | — | Fila de totales fija al pie. Se calcula sobre las filas visibles (con filtros y búsqueda). Los vacíos cuentan como 0 y los valores no numéricos se ignoran; count cuenta los valores numéricos. | | currentCoin | string | "" | Símbolo antepuesto a los números con decimales ("$" → $ 1,250.50). | | sortBy | { [columna]: "ASC" \| "DESC" } | — | Orden inicial. Solo se usa la primera clave. | | height | number | 560 | Altura máxima (px) del área con scroll. Encabezados y totales quedan fijos. | | autoHeight | boolean | false | Sin altura máxima: la tabla crece con sus filas. | | width | number \| string | "100%" | Ancho de la tabla. | | colSize | Record<columna, number> | — | Ancho fijo (px) por columna. Las demás se reparten el espacio restante. | | density | "compact" \| "standard" \| "comfortable" | "standard" | Alto de fila: 30, 53 u 80 px. | | rowHeight | number | — | Alto de fila en px (tiene prioridad sobre density). | | wrapText | boolean | false | Permite que el texto ocupe varias líneas en lugar de cortarse con "…". | | fontSize | string | "12px" | Tamaño de letra de las celdas. | | className | string | — | Clases extra para el contenedor. | | disabled | boolean | false | Desactiva ordenar, filtrar y redimensionar desde los encabezados. |

Edición y guardado

<Table
  data={productos}
  editableFields={["precio", "existencias"]}
  onSave={async (rows) => {
    await api.guardar(rows); // todas las filas, con las ediciones aplicadas
  }}
/>

Las filas editadas siguen visibles aunque un filtro de columna las excluya, para que no "desaparezcan" mientras se trabaja.

Selección

<Table
  data={pedidos}
  onSelect={async (seleccionadas) => {
    await api.autorizar(seleccionadas.map((p) => p.id));
  }}
  onSelectLabel="Autorizar"
/>

Totales

<Table
  data={ventas}
  currentCoin="$"
  footer={{ importe: "sum", descuento: "avg", folio: "count" }}
/>

Controles en celdas (buttons)

Cada clave de buttons es el nombre de una columna; el valor es el control que se pinta en cada celda de esa columna. La tabla clona el control por fila y le pasa:

  • value y children con el valor de la celda (salvo a <input>, <select> y <textarea>, que conservan sus propios hijos);
  • row con la fila completa, si el control es un componente propio (no a etiquetas HTML como <button>);
  • onClick y onChange envueltos (ver abajo).

Lo que reciben onClick y onChange. Reciben el evento original con la fila agregada. Funcionan las dos formas:

import { Button, Table, TableButtonProps } from "next-recomponents";

<Table
  data={facturas}
  buttons={{
    // Forma 1: el evento con la fila (e.row, e.currentTarget, e.preventDefault()).
    folio: (
      <Button onClick={(e: TableButtonProps) => abrirFactura(e.row.id)} />
    ),

    // Forma 2: desestructurar { e, row }.
    estado: (
      <Button
        color="success"
        onClick={async ({ row }: TableButtonProps) => {
          await api.pagar(row.id);
          return { estado: "Pagada" }; // se aplica a la fila
        }}
      />
    ),

    // Input editable: onChange también recibe e.row; e.target.value es el valor escrito.
    cantidad: (
      <input
        type="number"
        onChange={(e: any) => {
          const cantidad = Number(e.target.value);
          return { total: cantidad * e.row.precio }; // actualiza otra columna de la fila
        }}
      />
    ),
  }}
/>;

Lo que devuelve el handler.

  • En onClick: si devuelve (o resuelve) un objeto, sus campos se aplican a la fila: return { estado: "Pagada" }. Cualquier otro valor se ignora.
  • En onChange: la columna del control se actualiza con e.target.value. Además, si devuelve un objeto, se aplican los campos que ya existen en la fila.
  • Si el control es Button y el handler devuelve una promesa, el botón muestra un indicador de carga hasta que termine.

TableEventProps es un alias de TableButtonProps.

Modal por fila

modal recibe un elemento; la tabla lo clona con dos props:

  • row: la fila abierta.
  • hide(): cierra el modal sin pasar por onCloseModal.
function DetalleCliente({ row, hide }: { row?: any; hide?: () => void }) {
  return (
    <div>
      <h2>{row?.nombre}</h2>
      <Button onClick={hide}>Listo</Button>
    </div>
  );
}

<Table
  data={clientes}
  modal={<DetalleCliente />}
  onCloseModal={async (row) => confirm(`¿Cerrar ${row.nombre}?`)}
/>;
  • Los botones Anterior / Siguiente recorren las filas visibles (con filtros, orden y búsqueda), no solo la página actual.
  • Al cambiar de fila, el contenido del modal se monta de nuevo: no arrastra el estado de la fila anterior.
  • El modal sigue la fila por su id, aunque cambie de posición por un orden o una edición. Si un filtro la oculta, el modal lo indica.
  • En la vista de tarjetas, tocar una tarjeta abre su modal.

Cuándo se reinician los datos

La tabla copia data a su estado interno para aplicar ediciones. Ese estado (ediciones, cambios de buttons) se reemplaza solo cuando el contenido de data cambia. Pasar un arreglo nuevo con el mismo contenido en cada render, como data={rows.map(...)}, no borra las ediciones.

Ficha vertical (un solo objeto)

<Table
  data={{ nombre: "Ana López", correo: "[email protected]", activo: true, saldo: 1250.5 }}
  header="Detalle del cliente"
  editableFields={["correo"]}
  onSave={async (valores) => api.actualizar(valores)}
/>

Acepta header, hideColumns (y oculta los campos que empiezan con _), editableFields, buttons, searchable (busca por campo o valor), exportName (exporta pares Campo/Valor), onSave, onSaveLabel, currentCoin, fontSize y className.

  • onSave recibe el objeto con sus ediciones (no un arreglo).
  • Los booleanos se muestran como "Sí" / "No".
  • Los objetos y arreglos anidados se muestran con Pre.

Form y useFormValues

Formulario que conecta automáticamente sus campos con un estado de valores, por su name. Los campos pueden estar a cualquier profundidad (dentro de div, grillas, etc.).

import { Form, Input, Select, TextArea, Button, regularExpresions } from "next-recomponents";

<Form
  onSubmit={async (e) => {
    await api.crearUsuario(e.values); // { nombre, correo, rol, acepto }
  }}
>
  <Input name="nombre" label="Nombre" required />
  <Input name="correo" label="Correo" regex={regularExpresions.email} />
  <Select name="rol" label="Rol">
    <option value="admin">Administrador</option>
    <option value="user">Usuario</option>
  </Select>
  <Input name="acepto" type="checkbox" label="Acepto los términos" />
  <Button type="submit">Guardar</Button>
</Form>;

Props

| Prop | Tipo | Por defecto | Descripción | | --- | --- | --- | --- | | onSubmit | (e) => void \| Promise | — | Recibe el evento con e.values (los valores del formulario). Mientras la promesa no se resuelve se muestra loader a pantalla completa. | | state | [valores, setValores] | estado interno | Estado externo, por ejemplo de useFormValues o useState. Permite leer o cambiar los valores desde fuera. | | variant | "card" \| "plain" | "card" | card: tarjeta con franja de color y etiqueta. plain: sin estilos (para filtros, barras, menús). | | badge | ReactNode \| false | "Formulario" | Texto de la etiqueta en la variante card; false la oculta. | | invalidMessage | string | "Existen valores inválidos en el formulario" | Mensaje (alert) si se intenta enviar con campos inválidos. | | loader | ReactNode | "Loading..." | Contenido mostrado mientras se ejecuta onSubmit. | | className y demás | — | — | Se pasan al <form>. |

Cómo se conectan los campos

  • Texto, select, calendario, textarea: value y onChange → guarda e.target.value.
  • type="checkbox": guarda true / false.
  • type="radio": varios radios con el mismo name; guarda el value del elegido.
  • Los campos sin name no se conectan.
  • Si el onChange propio de un campo existe, también se ejecuta.

Validación

Un Input con regex que no se cumple muestra su mensaje de error. Mientras haya alguno, el formulario no se envía y muestra invalidMessage.

useFormValues

Estado de formulario que fusiona cada actualización con la anterior. Úsalo para valores iniciales o para leer y modificar los valores desde fuera del formulario:

import { Form, Input, useFormValues } from "next-recomponents";

const [valores, setValores] = useFormValues<{ nombre: string; edad: number }>({
  nombre: "Ana",
});

<Form state={[valores, setValores]} onSubmit={(e) => console.log(e.values)}>
  <Input name="nombre" label="Nombre" />
  <Input name="edad" type="number" label="Edad" />
</Form>;

setValores({ edad: 30 }); // conserva `nombre`

Input

Campo de texto con etiqueta sobre el borde, validación por expresión regular y botón para mostrar contraseñas. Con type="checkbox" o type="radio" se muestra como casilla con la etiqueta al lado.

import { Input, regularExpresions } from "next-recomponents";

<Input
  label="Correo"
  name="correo"
  type="email"
  regex={regularExpresions.email}
  invalidMessage="Escribe un correo válido"
  required
/>

<Input label="Contraseña" type="password" regex={regularExpresions.password} />

<Input label="Recordarme" type="checkbox" name="recordar" />

<Input label="Mensual" type="radio" name="plan" value="mensual" />
<Input label="Anual" type="radio" name="plan" value="anual" />

| Prop | Tipo | Descripción | | --- | --- | --- | | label | ReactNode | Obligatoria. Etiqueta del campo. Con required agrega un *. | | regex | RegExp | El valor debe cumplirla. Con valor válido el borde se pone verde; con inválido, rojo y aparece el mensaje. Un campo vacío no se valida. | | invalidMessage | ReactNode | Mensaje si no cumple regex. Por defecto "Valor no válido". | | icon | ReactNode | Ícono decorativo a la derecha (se ignora en contraseñas). | | Atributos de <input> | — | name, type, value, defaultValue, onChange, placeholder, disabled, required, etc. |

Funciona controlado (value + onChange) o no controlado (defaultValue).


TextArea

Área de texto con etiqueta y contador opcional de caracteres.

import { TextArea } from "next-recomponents";

<TextArea label="Comentarios" name="comentarios" maxLength={500} rows={6} />;

| Prop | Tipo | Por defecto | Descripción | | --- | --- | --- | --- | | label | ReactNode | — | Etiqueta del campo. | | maxLength | number | — | Límite de caracteres. Muestra un contador que cambia a ámbar al 90 % y a rojo al llegar al límite. | | rows | number | 4 | Alto inicial en líneas (se puede estirar). | | Atributos de <textarea> | — | — | name, value, defaultValue, onChange, disabled, required, etc. |


Select

Selector con búsqueda: se escribe para filtrar las opciones (sin importar acentos ni mayúsculas) y se navega con el teclado (↑ ↓ Enter Esc).

import { Select } from "next-recomponents";

<Select
  label="Estado"
  name="estado"
  value={estado}
  onChange={(e) => setEstado(e.target.value)}
>
  <option value="AGS">Aguascalientes</option>
  <option value="JAL">Jalisco</option>
  <option value="NL">Nuevo León</option>
</Select>;

| Prop | Tipo | Por defecto | Descripción | | --- | --- | --- | --- | | children | <option> | — | Opciones. Si una opción no tiene value, se usa su texto. Pueden venir de un .map(...). | | label | string | — | Etiqueta del campo. | | placeholder | string | "Selecciona..." | Texto con el campo vacío. | | value | string | — | Valor (el value de la opción) seleccionado. | | onChange | (e) => void | — | Recibe { target: { value } } con el value de la opción elegida (o "" al borrar). | | strictMode | boolean | true | Solo acepta opciones de la lista: si al salir del campo el texto no coincide con una, se borra y el campo queda en rojo. | | Atributos de <input> | — | — | name, disabled, required, onBlur, onFocus, etc. |

  • Mientras no haya opciones, muestra "Cargando…" (útil cuando las opciones llegan de una API).
  • La lista se abre hacia arriba si no cabe abajo y se muestra por encima de modales.

MyCalendar

Selector de fecha en el que solo se pueden elegir las fechas indicadas. Muestra la fecha en formato legible (Mié, 7 oct 2026) y entrega AAAA-MM-DD.

Requiere importar next-recomponents/dist/index.css (ver Instalación).

import { MyCalendar } from "next-recomponents";

<MyCalendar
  label="Fecha de la cita"
  name="fecha"
  enabledDates={["2026-10-20", "2026-10-21", "2026-10-23"]}
  value={fecha}
  onChange={(e) => setFecha(e.target.value)} // "2026-10-21"
/>;

| Prop | Tipo | Descripción | | --- | --- | --- | | label | ReactNode | Obligatoria. Etiqueta del campo. | | enabledDates | (string \| Date)[] | Obligatoria. Únicas fechas elegibles ("AAAA-MM-DD" o Date). Si la lista está vacía, el componente no se muestra. | | value / defaultValue | string \| Date | Fecha seleccionada. | | onChange | (e) => void | Recibe { target: { value: "AAAA-MM-DD" } }. | | name | string | Nombre del campo oculto que envía AAAA-MM-DD en el formulario. | | placeholder | string | Por defecto "Selecciona una fecha". | | disabled, required | boolean | Como en un input. |

Al abrir, el calendario muestra el mes de la fecha elegida o, si no hay, de la próxima fecha disponible.


Button

Botón con colores predefinidos, ícono y estado de carga automático.

import { Button } from "next-recomponents";

<Button onClick={() => console.log("clic")}>Aceptar</Button>

<Button color="danger" icon={<TrashIcon />} onClick={async () => await api.borrar(id)}>
  Eliminar
</Button>

<Button color="white" full>Cancelar</Button>

| Prop | Tipo | Por defecto | Descripción | | --- | --- | --- | --- | | color | ver colores | "primary" | Color del botón. | | icon | ReactNode | — | Ícono antes del texto. Sin children, el botón queda cuadrado (solo ícono). | | size | "small" \| "full" | "small" | full ocupa todo el ancho. | | full | boolean | false | Atajo de size="full". | | loading | boolean | false | Fuerza el estado de carga. | | onClick | (e) => unknown | — | Si devuelve una promesa, el botón se deshabilita y muestra un indicador hasta que se resuelva (mínimo 400 ms para que se alcance a ver). | | ref | Ref<HTMLButtonElement> | — | Soportado. | | Atributos de <button> | — | — | type, disabled, className, etc. |

Dentro de buttons de Table, onClick recibe además la fila (ver Controles en celdas).


Modal

Ventana modal que se abre al hacer clic en un botón.

import { Modal, Button } from "next-recomponents";

<Modal
  button={<Button>Nuevo cliente</Button>}
  title="Nuevo cliente"
  size="md"
  onClose={async () => confirm("¿Descartar los cambios?")}
>
  <FormularioCliente />
</Modal>;

// Dentro del contenido, `hide` cierra el modal sin pasar por onClose:
function FormularioCliente({ hide }: { hide?: () => void }) {
  return <Button onClick={async () => { await guardar(); hide?.(); }}>Guardar</Button>;
}

| Prop | Tipo | Por defecto | Descripción | | --- | --- | --- | --- | | button | ReactElement | — | Obligatoria. Elemento que abre el modal. Su propio onClick también se ejecuta. | | children | ReactNode | — | Contenido. Si es un componente propio, recibe hide(). A etiquetas HTML y fragmentos no se les pasa. | | title | ReactNode | — | Título en la barra superior. Sin título, solo aparece la X. | | color | ver colores | "primary" | Color de la barra del título. white la deja sin color. | | size | "sm" \| "md" \| "lg" \| "xl" \| "full" | "lg" | Ancho máximo. full ocupa toda la ventana. | | onClose | () => boolean \| void \| Promise | — | Se llama al intentar cerrar (X, clic fuera o Escape). Devolver false lo mantiene abierto. |

El modal bloquea el scroll de la página, mantiene el foco del teclado dentro y lo devuelve al botón al cerrar.


Alert

Mensaje destacado en la página.

import { Alert } from "next-recomponents";

<Alert color="success" title="Guardado">Los cambios se guardaron correctamente.</Alert>

<Alert color="danger" onClose={() => setError(null)}>{error}</Alert>

<Alert color="info" icon={false}>Sin ícono.</Alert>

| Prop | Tipo | Por defecto | Descripción | | --- | --- | --- | --- | | children | ReactNode | — | Mensaje. | | color | ver colores | "primary" | Color e ícono. | | title | ReactNode | — | Título en negrita. | | icon | ReactNode \| false | según el color | Ícono propio; false lo oculta. | | onClose | () => void | — | Muestra una X para cerrar. El componente no se oculta solo: hazlo desde tu estado. | | Atributos de <div> | — | — | className, role, etc. Con danger y warning el role por defecto es alert (los lectores de pantalla lo anuncian); con el resto, status. |


Container

Estructura de página: menú lateral, barra superior con migas de pan, panel izquierdo opcional y pie plegable. Requiere Next.js con App Router.

"use client";
import { Container } from "next-recomponents";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <Container
      appName="Sistema QC"
      menuList={[
        { location: "/", name: "Inicio", icon: <HomeIcon /> },
        { location: "/clientes", name: "Clientes", icon: <UsersIcon /> },
      ]}
      navItems={[{ location: "/clientes", name: "Clientes" }]}
      leftPanel={<Filtros />}
      footPanel={<Resumen />}
      expandedMenu
    >
      {children}
    </Container>
  );
}

| Prop | Tipo | Por defecto | Descripción | | --- | --- | --- | --- | | children | ReactNode | — | Contenido principal. | | appName | ReactNode | — | Nombre de la aplicación (marca del menú y título en celular). Si es texto, su inicial se usa como logo. | | menuList | { location, name, icon? }[] | [] | Opciones del menú lateral. La opción de la ruta actual (o de una subruta) queda resaltada. Sin ícono, se muestra la inicial del nombre. | | navItems | { location, name, icon? }[] | [] | Migas de pan. El último elemento es la página actual y define el título de la pestaña del navegador y, si su ícono es un SVG, el favicon. | | leftPanel | ReactNode | — | Panel a la izquierda del contenido (arriba, en celular). | | footPanel | ReactNode | — | Pie plegable con "Mostrar / Ocultar detalles". | | expandedMenu | boolean | false | En escritorio, el menú inicia expandido (con nombres). Si no, muestra solo íconos y se expande al pasar el mouse. | | expandedFooter | boolean | false | El pie inicia abierto. |

En celular, el menú se abre como cajón lateral con el botón ☰ y se cierra al navegar, con Escape o tocando fuera.


TabContainer y TabItem

Pestañas.

import { TabContainer, TabItem } from "next-recomponents";

<TabContainer currentIndex={0} labelMaxWidth={180}>
  <TabItem label="General" icon={<InfoIcon />}>
    <DatosGenerales />
  </TabItem>
  <TabItem label="Documentos">
    <Documentos />
  </TabItem>
  <TabItem label="Historial" disabled>
    <Historial />
  </TabItem>
</TabContainer>;

TabContainer

| Prop | Tipo | Por defecto | Descripción | | --- | --- | --- | --- | | children | TabItem[] | — | Pestañas. | | currentIndex | number | 0 | Pestaña activa. Si cambia desde fuera, la pestaña activa cambia. | | labelMaxWidth | number | — | Ancho máximo (px) de cada etiqueta; el texto largo se corta con "…". |

TabItem

| Prop | Tipo | Descripción | | --- | --- | --- | | label | string | Obligatoria. Texto de la pestaña. Usa etiquetas distintas en cada pestaña. | | icon | ReactNode | Ícono junto al texto. | | disabled | boolean | No se puede abrir con clic. | | children | ReactNode | Contenido. |

Con el foco en una pestaña, las flechas ← → cambian de pestaña.


Pre

Visor de JSON con colores, números de línea y nodos plegables. Acepta cualquier valor y no se rompe con referencias circulares.

import { Pre } from "next-recomponents";

<Pre data={respuestaApi} title="Respuesta" defaultExpandDepth={1} maxHeight={400} />;

| Prop | Tipo | Por defecto | Descripción | | --- | --- | --- | --- | | data | any | — | Valor a mostrar. | | defaultExpandDepth | number | todo abierto | Niveles abiertos al inicio; 0 = todo plegado. | | maxHeight | number \| string | crece con el contenido | Alto máximo con scroll (400, "60vh"). | | title | ReactNode | — | Texto en la barra superior. |


DocumentViewer

Miniatura de un archivo (imagen, PDF, Word/Excel/PowerPoint u otro) con acciones de descargar, abrir y eliminar. Al abrirlo se muestra a pantalla completa.

import { DocumentViewer } from "next-recomponents";

const archivos = [
  { url: "https://cdn.ejemplo.com/foto.jpg", name: "foto.jpg", contentType: "image/jpeg" },
  { url: "https://cdn.ejemplo.com/contrato.pdf", name: "contrato.pdf", contentType: "application/pdf" },
];

<div className="flex gap-3">
  {archivos.map((a) => (
    <DocumentViewer
      key={a.url}
      item={a}
      width="200px"
      height="200px"
      groupId={1}
      onDelete={(item) => quitar(item.url)}
    />
  ))}
</div>;

| Prop | Tipo | Por defecto | Descripción | | --- | --- | --- | --- | | item | { url, name, contentType } | — | Obligatoria. Archivo. contentType es el tipo MIME y decide cómo se muestra. | | width / height | string | "100%" | Tamaño de la miniatura. | | onDelete | (item) => void | — | Muestra el botón de eliminar. | | groupId | number | 1 | Los visores con el mismo groupId se recorren juntos (← →) en pantalla completa, en el orden en que aparecen en la página. |

Cómo se muestra cada tipo:

  • Imágenes (image/*) y PDF: directamente.
  • Office (Word, Excel, PowerPoint): con el visor de Google Docs, así que la url debe ser pública.
  • Otros: ícono de archivo con su extensión ("Este tipo de archivo no tiene vista previa"); se pueden descargar.

Table3 (heredada)

Tabla anterior basada en <table>. Se mantiene por compatibilidad; para código nuevo usa Table.

import { Table3 } from "next-recomponents";

<Table3
  data={filas}           // cada fila con id o _id
  header="Inventario"
  selectItems
  maxItems={20}          // filas por página
  footer={{ cantidad: "sum" }}
  symbols={{ importe: "$" }}
  exportName="inventario"
  onSave={(rows) => console.log(rows)}
/>;

Props: data (con id o _id), selectItems, maxItems, onSave, exportName, colSizes, modal, footer (sum / count / avg), header, onChange, symbols (prefijo por columna en totales), sortBy ({ columna: "asc" | "desc" }), más los atributos de <table>.


usePopup

Reemplazo de alert, confirm y prompt del navegador, con promesas. También abre contenido propio en un modal.

Importante: renderiza PopupComponent en el componente que usa el hook.

import { usePopup, Button } from "next-recomponents";

function Acciones() {
  const popup = usePopup();

  const borrar = async () => {
    if (!(await popup.confirm("¿Eliminar el registro?", "danger"))) return;
    await api.borrar();
    await popup.alert("Registro eliminado", "success");
  };

  const renombrar = async () => {
    const nombre = await popup.prompt("Nuevo nombre"); // null si se cancela
    if (nombre) await api.renombrar(nombre);
  };

  return (
    <>
      <Button color="danger" onClick={borrar}>Eliminar</Button>
      <Button onClick={renombrar}>Renombrar</Button>
      {popup.PopupComponent}
    </>
  );
}

| Función / valor | Devuelve | Descripción | | --- | --- | --- | | alert(mensaje, color?) | Promise<void> | Mensaje con botón Aceptar. | | confirm(mensaje, color?) | Promise<boolean> | true con Aceptar; false con Cancelar o Escape. | | prompt(mensaje, color?) | Promise<string \| null> | Texto escrito con Aceptar (Enter también acepta); null con Cancelar. | | modal(contenido, color?, icons?, full?, onClose?) | Promise<void> | Abre contenido propio en un modal. full ocupa toda la ventana. onClose debe resolver true para permitir el cierre. Con icons = true se muestra el ícono del color en lugar de la X (se cierra con Escape). | | close(confirmado, valor?) | — | Cierra el popup abierto desde código. | | updateMessage(contenido) | — | Cambia el contenido del popup abierto (por ejemplo, si el contenido del modal depende de un estado). | | isOpen | boolean | Si hay un popup abierto. | | PopupComponent | ReactNode | Debe renderizarse para que el popup se vea. |

  • El popup aparece por encima de cualquier Modal abierto.
  • Si se abre un popup mientras otro está abierto, el anterior se resuelve como cancelado.
  • Hacer clic en el fondo no lo cierra.

useResources

Hook para consumir una API REST con un recurso por endpoint: listar con paginación, buscar por id, crear, actualizar y eliminar. Mantiene en estado los datos, la página actual y el estado de cada petición.

import { useResources } from "next-recomponents";

type Cliente = { id: number; nombre: string; correo: string };

const recursos = useResources({
  baseURI: "https://api.ejemplo.com",
  endpoints: {
    clientes: { typeof: {} as Cliente, defaultParams: { limit: 20, page: 1 } },
    perfil: { typeof: {} as { id: number; nombre: string }, id: "me" },
  },
  onError: ({ errorMessage }) => alert(errorMessage), // solo errores 403
});

const { clientes } = recursos;

clientes.data;                               // filas cargadas
clientes.state;                              // "loading" | "success" | "error"
await clientes.show({ page: 2, limit: 20, activo: true });
await clientes.find(15);                     // GET /clientes/15 → clientes.selectedItem
await clientes.create({ nombre: "Ana" });    // POST /clientes
await clientes.update(15, { correo: "[email protected]" }); // PATCH /clientes/15
await clientes.remove(15);                   // DELETE /clientes/15

Configuración

| Opción | Descripción | | --- | --- | | baseURI | URL base. Cada endpoint se consulta en baseURI/<nombre>. Sin baseURI no se hacen peticiones (respuestas vacías, útil para pruebas). | | endpoints | Objeto { nombre: { typeof, id?, defaultParams? } }. typeof solo da el tipo de las filas. | | endpoints.x.id | Al montar se llama find(id, defaultParams). | | endpoints.x.defaultParams | Al montar (si no hay id) se llama show(defaultParams). | | onError | Se llama con { error, errorMessage } cuando la API responde 403. |

Autenticación. Todas las peticiones envían Authorization: <token>, con el valor guardado en localStorage bajo la clave "token". Se lee al montar el hook.

Por cada endpoint

| Miembro | Descripción | | --- | --- | | show({ page?, limit?, merge?, ...filtros }) | GET /nombre con page (1), limit (10) y los filtros como query. Espera { data, totalItems, totalPages, currentPage }. Con merge (por defecto true), las páginas posteriores a la 1 se agregan a los datos ya cargados. | | find(id, query?) | GET /nombre/id. Guarda el resultado en selectedItem y lo reemplaza (o agrega) en data. | | create(datos \| datos[]) | POST /nombre. Si algún valor es un File, se envía como multipart/form-data. El resultado se agrega a data. | | update(id, datos) | PATCH /nombre/id. Fusiona la respuesta con la fila en data. | | remove(id) | DELETE /nombre/id. Quita la fila de data. | | getAllPages(limit?) | Recorre todas las páginas con show y devuelve todas las filas. | | data, selectedItem | Datos cargados y último elemento consultado o creado. | | state, errorMessage | Estado de la última petición y mensaje de error. | | currentPage, totalPages, params | Paginación y filtros de la última consulta. | | loaded, setLoaded() | Si el recurso ya se cargó. |

Si una petición falla, la función devuelve el error (no lo lanza) y deja state = "error".


useDates

Utilidades para fechas de calendario (AAAA-MM-DD) sin errores de zona horaria: "2026-10-07T00:00:00Z" es el 7 de octubre, aunque en México ese instante aún sea el día 6.

import { useDates } from "next-recomponents";

const { now, dateAddDays, dateSubDays, isValid, addMinutes } = useDates();

now();                                // "2026-10-14" (hoy)
now("07/10/2026");                    // "2026-10-07"
dateAddDays(30, "2026-10-07");        // "2026-11-06"
dateSubDays(1);                       // ayer
isValid("2026-10-20");                // true si el 20 de octubre aún no termina
isValid("2026-10-20", "2026-10-21");  // false
addMinutes("23:50", 20);              // "00:10"

| Función | Descripción | | --- | --- | | now(fecha?) | Hoy, o la fecha recibida normalizada, como "AAAA-MM-DD". | | dateAddDays(días, fecha?) | Suma días (desde hoy si no se indica fecha). | | dateSubDays(días, fecha?) | Resta días. | | isValid(fecha, referencia?) | true si el final de ese día (23:59:59) es posterior a referencia (por defecto, ahora). Útil para vigencias. | | addMinutes("HH:mm", minutos) | Suma minutos a una hora y da la vuelta al pasar la medianoche. |

Formatos aceptados: "AAAA-MM-DD" (con o sin hora, que se ignora), "DD/MM/AAAA", "D-M-AAAA", Date y Moment. Una fecha no válida devuelve "" (o false en isValid).


useExcel

Importar y exportar archivos de Excel.

import { useExcel } from "next-recomponents";

const excel = useExcel();

// Exportar
await excel.export(clientes, "clientes.xlsx");
await excel.exportAllPages({ Clientes: clientes, Pedidos: pedidos }, "reporte.xlsx");

// Importar desde un <input type="file">
const filas = await excel.import(file);         // filas de la primera hoja
const hojas = await excel.importAllPages(file); // { NombreHoja: filas[] }

| Función | Descripción | | --- | --- | | export(filas, nombre?) | Descarga un .xlsx de una hoja. Los encabezados son las claves de la primera fila. Nombre por defecto: "archivo.xlsx". | | exportAllPages({ hoja: filas }, nombre?) | Descarga un .xlsx con una hoja por clave. | | import(file) | Lee la primera hoja; cada fila es un objeto con los encabezados como claves. | | importAllPages(file) | Lee todas las hojas. |


regularExpresions

Expresiones regulares listas para usar con Input (regex={regularExpresions.email}). Validan el formato, no la existencia (un RFC con formato correcto puede no existir).

| Clave | Valida | Ejemplo válido | | --- | --- | --- | | email | Correo | [email protected] | | phone | 10 dígitos | 4611234567 | | phoneIntl | Teléfono internacional (E.164) | +524611234567 | | url | URL http(s) | https://sitio.com/ruta?x=1 | | number | Solo dígitos | 007 | | integer | Entero con signo | -15 | | decimal | Decimal con punto | -0.5 | | currency | Importe, hasta 2 decimales, comas de miles opcionales | 1,234.50 | | percent | 0 a 100, hasta 2 decimales | 99.9 | | alpha | Letras (con acentos y ñ) y espacios | José Peña | | alphanumeric | Letras, números y espacios | Calle 5 Sur | | name | Nombre de persona (2 a 60) | María-José O'Neil | | username | 3 a 20, empieza con letra | ana.lopez_1 | | slug | Slug para URL | mi-articulo-2026 | | password | 8+ con mayúscula, minúscula y número | Clave2026 | | strongPassword | 10+ y además un símbolo | Clave#2026x | | date | AAAA-MM-DD | 2026-10-07 | | dateDMY | DD/MM/AAAA | 07/10/2026 | | time | HH:MM 24 h | 23:59 | | zipMX | Código postal (5 dígitos) | 38000 | | rfc | RFC persona física o moral | GODE561231GR8 | | curp | CURP | GODE561231HDFRRN09 | | clabe | CLABE (18 dígitos) | 012180001234567891 | | rut | RUT chileno | 12.345.678-5 | | creditCard | Tarjeta (13 a 19 dígitos) | 4111 1111 1111 1111 | | hexColor | Color hexadecimal | #4F8CFF | | ipv4 | Dirección IPv4 | 192.168.0.1 | | uuid | UUID | 123e4567-e89b-12d3-a456-426614174000 |

Ninguna lleva la bandera g, así que se pueden reutilizar con .test() sin resultados alternados.