diff --git a/src/app/input/page.tsx b/src/app/input/page.tsx
index 9b98d51..77f1452 100644
--- a/src/app/input/page.tsx
+++ b/src/app/input/page.tsx
@@ -6,54 +6,117 @@ import Input from "@/components/input";
export default function Page() {
return (
-
- Componente: Input
+
+ 📦 Componente: Input
-
- Este componente encapsula un {` `} con su respectiva
- etiqueta y funcionalidad adicional para mostrar u ocultar contraseñas si
- el tipo es password. Puedes personalizar las clases de cada
- parte del input.
+
+ El componente {` `} encapsula un campo de entrada
+ HTML estándar con mejoras en accesibilidad, estilo y experiencia de
+ usuario. Soporta etiquetas, clases personalizadas, inputs
+ deshabilitados, y funcionalidad para mostrar u ocultar contraseñas.
-
🔹 Uso básico
-
+
-
🔹 Campo de tipo contraseña
-
- Incluye un botón para mostrar u ocultar el contenido del campo.
-
-
-
-
🔹 Input deshabilitado
-
-
-
🔹 Personalización de clases
-
-
-
+
+ {` `}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/app/table/page.tsx b/src/app/table/page.tsx
index 0375257..5eec72e 100644
--- a/src/app/table/page.tsx
+++ b/src/app/table/page.tsx
@@ -1,19 +1,15 @@
"use client";
-import Table, { TablaSkeleton } from "@/components/table";
+import Table, { Header, TablaSkeleton } from "@/components/table";
import React from "react";
-/**
- * Página de demostración del componente Table.
- * Incluye explicación de uso y ejemplos con distintas configuraciones.
- */
export default function Page() {
- const headers: { key: "name" | "age" | "email"; label: string }[] = [
- { key: "name", label: "Nombre" },
- { key: "age", label: "Edad" },
- { key: "email", label: "Correo" },
+ const headers: Header<{ name: string; age: number; email: string; acciones?: null }>[] = [
+ { key: "name", label: "Nombre", width: "30%" },
+ { key: "age", label: "Edad", width: "15%" },
+ { key: "email", label: "Correo", width: "40%" },
];
-
+
const data = [
{ name: "Juan", age: 20, email: "juan@example.com" },
{ name: "María", age: 25, email: "maria@example.com" },
@@ -23,21 +19,27 @@ export default function Page() {
return (
-
Componente: Table
+
+ 📊 Componente: Table
+
-
- El componente Table permite renderizar tablas dinámicas con
- paginación y ordenamiento por columna. Acepta un arreglo de objetos como
- datos, y una lista de headers que indican qué propiedades mostrar.
+
+ El componente <Table /> permite mostrar datos
+ tabulares con soporte para ordenamiento, paginación, renderizado
+ personalizado, columnas clicables y estado de carga.
-
🔹 Uso básico
+
+
+
🔹 Uso básico
-
🔹 Con filas clicables
+
+
+
🖱️ Filas clicables
- Puedes pasar una función onRowClick para manejar clics en
- filas.
+ Usa la propiedad onRowClick para manejar interacciones por
+ fila.
alert(`Seleccionaste a ${row.name}`)}
/>
- 🔹 Con paginación personalizada
+
+
+ 📄 Paginación personalizada
- Puedes controlar cuántas filas se muestran por página usando{" "}
- rowsPerPage.
+ Usa rowsPerPage para definir cuántas filas mostrar por
+ página.
- 🔹 Tabla en estado de carga
-
- El componente TablaSkeleton muestra una tabla con
- placeholders mientras se cargan los datos reales. Puedes personalizar el
- número de filas ycolumnas usando las
- props rows y columns.
-
+
-
+ 🎨 Render personalizado por celda
+
+ Puedes usar la propiedad render dentro de cada{" "}
+ header para personalizar cómo se muestra una columna.
+
+ (
+ alert(row.email)}
+ >
+ Ver correo
+
+ ),
+ },
+ ]}
+ data={data.map((d) => ({ ...d, acciones: null as null }))}
+ />
+
+
+
+ ⏳ Tabla en estado de carga
+
+ El componente TablaSkeleton muestra una tabla de ejemplo
+ mientras se cargan los datos. Puedes personalizar rows y{" "}
+ columns.
+
+
);
}
diff --git a/src/components/input.tsx b/src/components/input.tsx
index bfa4ff8..87bb9d3 100644
--- a/src/components/input.tsx
+++ b/src/components/input.tsx
@@ -1,7 +1,7 @@
-import React, { useState } from 'react';
+import React, { useState } from "react";
export interface InputProps
- extends Omit, 'className'> {
+ extends Omit, "className"> {
label: string;
className?: {
container?: string;
@@ -9,62 +9,88 @@ export interface InputProps
input?: string;
};
}
+
/**
* Componente de entrada (`Input`) reutilizable en React.
*
- * Este componente renderiza un campo de entrada (` `) con una etiqueta opcional y
+ * Este componente renderiza un campo de entrada (` `) con una etiqueta y
* funcionalidad para mostrar u ocultar contraseñas cuando el tipo es `password`.
+ * También permite personalizar clases CSS y es accesible mediante `aria-label` y `htmlFor`.
*
* @component
* @param {Object} props - Propiedades del componente.
- * @param {string} props.label - Etiqueta del campo de entrada.
- * @param {Object} [props.className] - Clases CSS personalizables.
+ * @param {string} props.label - Etiqueta visible asociada al campo de entrada.
+ * @param {Object} [props.className] - Clases CSS personalizables para distintos elementos.
* @param {string} [props.className.container] - Clases para el contenedor del input.
* @param {string} [props.className.label] - Clases para la etiqueta del input.
* @param {string} [props.className.input] - Clases para el input en sí.
- * @param {string} [props.type] - Tipo del input (`text`, `password`, etc.).
- * @param {string} [props.name] - Nombre del input (para formularios).
- * @param {React.InputHTMLAttributes} [rest] - Atributos adicionales para el input.
+ * @param {string} [props.type] - Tipo del input (`text`, `password`, `email`, etc.).
+ * @param {string} [props.name] - Nombre del input (útil para formularios).
+ * @param {string} [props.id] - ID personalizado para el input (para accesibilidad).
+ * @param {string} [props.value] - Valor del input (para uso controlado).
+ * @param {function} [props.onChange] - Manejador del cambio de valor del input.
+ * @param {React.InputHTMLAttributes} [rest] - Atributos adicionales compatibles con ` `.
*
* @example
- * // Ejemplo de uso
+ * // Ejemplo de uso con contraseña:
*
+ *
+ * @example
+ * // Ejemplo de input controlado:
+ * setEmail(e.target.value)}
* />
*/
export default function Input(props: InputProps) {
- const { className, label, type, ...rest } = props;
+ const { className, label, type, name, id, ...rest } = props;
const [showPassword, setShowPassword] = useState(false);
- const togglePasswordVisibility = () => {
- setShowPassword(!showPassword);
- };
+ const isPassword = type === "password";
+ const inputType = isPassword && showPassword ? "text" : type;
+ const inputId = id || name || `input-${label.replace(/\s+/g, "-")}`;
+
+ const togglePasswordVisibility = () => setShowPassword(!showPassword);
return (
-
-
+
+
{label}
- {props.type === 'password' && (
+ {isPassword && (
- {showPassword ? (
-
- ) : (
-
- )}
+
)}
diff --git a/src/components/table.tsx b/src/components/table.tsx
index d89d9e7..7acc404 100644
--- a/src/components/table.tsx
+++ b/src/components/table.tsx
@@ -1,59 +1,49 @@
import React, { useState, useMemo } from "react";
import Pagination from "./pagination";
-/**
- * Representa la definición de una columna para la tabla.
- *
- * @template T - Tipo genérico de los datos.
- */
export interface Header
{
- /** Clave del objeto a mostrar en esta columna. */
key: keyof T;
- /** Etiqueta que se mostrará como encabezado. */
label: string;
+ render?: (value: T[keyof T], row: T) => React.ReactNode;
+ width?: string;
+ className?: string;
}
-/**
- * Propiedades del componente Table.
- *
- * @template T - Tipo genérico de los datos.
- */
interface TableProps {
- /** Arreglo de encabezados de columna. */
headers: Header[];
- /** Datos que se mostrarán en la tabla. */
data: T[];
- /** Número de filas por página (por defecto: 10). */
rowsPerPage?: number;
- /** Callback que se ejecuta al hacer clic en una fila. */
onRowClick?: (rowData: T) => void;
+ rowKey?: (row: T) => string | number;
+ className?: {
+ table?: string;
+ row?: string;
+ cell?: string;
+ header?: string;
+ };
+ controlledPage?: number;
+ onPageChange?: (page: number) => void;
}
-/**
- * Componente genérico de tabla con paginación y ordenamiento.
- *
- * @template T - Tipo de los objetos a renderizar en las filas.
- *
- * @param {TableProps} props - Propiedades del componente.
- * @returns {JSX.Element} Tabla renderizada.
- */
export default function Table({
headers,
data,
rowsPerPage = 10,
onRowClick,
+ rowKey,
+ className,
+ controlledPage,
+ onPageChange,
}: TableProps) {
const [sortConfig, setSortConfig] = useState<{
key: keyof T;
direction: "asc" | "desc";
} | null>(null);
- const [currentPage, setCurrentPage] = useState(1);
- /**
- * Maneja el ordenamiento al hacer clic en un encabezado.
- *
- * @param {keyof T} key - Clave del dato a ordenar.
- */
+ const [internalPage, setInternalPage] = useState(1);
+ const currentPage = controlledPage ?? internalPage;
+ const setPage = onPageChange ?? setInternalPage;
+
const handleSort = (key: keyof T) => {
let direction: "asc" | "desc" = "asc";
if (
@@ -66,24 +56,17 @@ export default function Table({
setSortConfig({ key, direction });
};
- /**
- * Datos ordenados de acuerdo con `sortConfig`.
- */
const sortedData = useMemo(() => {
if (!sortConfig) return data;
-
return [...data].sort((a, b) => {
- if (a[sortConfig.key] < b[sortConfig.key]) {
+ if (a[sortConfig.key]! < b[sortConfig.key]!)
return sortConfig.direction === "asc" ? -1 : 1;
- }
- if (a[sortConfig.key] > b[sortConfig.key]) {
+ if (a[sortConfig.key]! > b[sortConfig.key]!)
return sortConfig.direction === "asc" ? 1 : -1;
- }
return 0;
});
}, [data, sortConfig]);
- // Paginación
const totalPages = Math.ceil(sortedData.length / rowsPerPage);
const paginatedData = sortedData.slice(
(currentPage - 1) * rowsPerPage,
@@ -92,52 +75,88 @@ export default function Table({
return (
-
-
+
+
+ {headers.map((header, index) => (
+
+ ))}
+
+
{headers.map((header, index) => (
handleSort(header.key)}
- className={
+ role="button"
+ tabIndex={0}
+ aria-sort={
+ sortConfig?.key === header.key
+ ? sortConfig.direction === "asc"
+ ? "ascending"
+ : "descending"
+ : "none"
+ }
+ style={{ width: header.width }}
+ className={`sortable ${className?.header || ""} ${
+ header.className || ""
+ } ${
sortConfig?.key === header.key
? `sorted-${sortConfig.direction}`
: ""
- }
+ }`}
>
{header.label}
- {sortConfig?.key === header.key &&
- (sortConfig.direction === "asc" ? (
-
- ) : (
-
- ))}
+ {sortConfig?.key === header.key && (
+
+ )}
))}
- {paginatedData.map((row, rowIndex) => (
- onRowClick && onRowClick(row)}
- className={onRowClick ? "clickable-row" : ""}
- >
- {headers.map((header, cellIndex) => (
-
- {row[header.key] as string}
-
- ))}
+ {paginatedData.length === 0 ? (
+
+
+ No hay datos disponibles
+
- ))}
+ ) : (
+ paginatedData.map((row, rowIndex) => (
+ onRowClick?.(row)}
+ className={`${onRowClick ? "clickable-row" : ""} ${
+ className?.row || ""
+ }`}
+ >
+ {headers.map((header, cellIndex) => (
+
+ {header.render
+ ? header.render(row[header.key], row)
+ : (row[header.key] as React.ReactNode)}
+
+ ))}
+
+ ))
+ )}
- {/* Paginación */}
{totalPages > 1 && (
)}
@@ -146,20 +165,11 @@ export default function Table({
}
interface TablaSkeletonProps {
- /** Número de renglones que se desean renderizar */
rows?: number;
- /** Número de columnas por fila (default: 5) */
columns?: number;
}
-/**
- * Componente de tabla en estado de carga con placeholders.
- *
- * @param rows - Número de filas a mostrar (default: 5)
- * @param columns - Número de columnas por fila (default: 5)
- * @returns JSX.Element
- */
-function TablaSkeleton({ rows = 5, columns = 5 }: TablaSkeletonProps) {
+export function TablaSkeleton({ rows = 5, columns = 5 }: TablaSkeletonProps) {
return (
@@ -185,5 +195,3 @@ function TablaSkeleton({ rows = 5, columns = 5 }: TablaSkeletonProps) {
);
}
-
-export { TablaSkeleton };