Utilidades
Errores, paginación, formato es-PA, contraste WCAG, sanitización, cache y puertos de infraestructura.
import { AppError, NotFoundError, cn, slugify, contrastRatio } from "@fesa/components/utils";Errores de dominio
Jerarquía AppError con statusCode y código estable — el contrato de error único del ecosistema:
NotFoundError (404) · ValidationError (400, con fieldErrors) · UnauthorizedError (401) · ForbiddenError (403) · AccountLockedError (423) · ConflictError (409) · TooManyRequestsError (429) · ServiceUnavailableError (503)
if (!order) throw new NotFoundError("La orden no existe");ActionState
El contrato estandarizado de useActionState:
interface ActionState { success: boolean; message: string; errors?: Record<string, string[]> }Paginación
const result = createPaginatedResult(items, total, { page: 1, pageSize: 20 });
// { items, totalItems, totalPages, page, pageSize, hasNext, hasPrev }Tipos PaginationParams, PaginatedResult<T>, SortParams + DEFAULT_PAGE_SIZE (20) y MAX_PAGE_SIZE (100) — el mismo techo que usa el datatable.
Formato FESA
formatCurrency(amount) (es-PA, USD) · formatDate(date, options?) (América/Panamá) · cn(...) (clsx + tailwind-merge) · slugify / toSlug (normaliza acentos por NFD) · stripUndefined(obj) · calendarDayRange(from, to) (rango de día local con fin exclusivo — evita el off-by-one de UTC) · toPlain(value) (convierte Decimal de Prisma a number para cruzar la frontera server→client).
Contraste WCAG 2.1
contrastRatio("#1e40af", "#ffffff") // 8.59
wcagLevel(ratio, isLargeText) // 'AAA' | 'AA' | 'AA Large' | 'Fail'
parseColor("rgb(30, 64, 175)") // parsea hex y rgb()Cero dependencias, con suite de tests. Lo usa el editor de tarjetas para avisar cuando un diseño no se lee.
Sanitización
sanitizeHtml(html) — DOMPurify con allowlist (prohíbe style/form/button) · sanitizeText · sanitizeAttr · sanitizeMarkdownHtml. Una sola política anti-XSS para todo el ecosistema.
Cache y rate limit en memoria
MemoryCache— TTL por clave con limpiezaunref()(interfaz estilo Redis: get/set/delete). Para producción distribuida, Redis en la app detrás deICache.MemoryRateLimitService— sliding window en memoria que implementaIRateLimitService.
Inyección de dependencias
const emailProvider = createProvider<IEmailService>(() => new ResendEmailService());
// emailProvider.get() — singleton lazy · .set(mock) — seam de tests · .reset()Puertos de infraestructura
Contratos para intercambiar proveedores sin tocar consumidores: IStorageService, IEmailService, ICache, IRateLimitService (con sus DTOs).