diff --git a/api-gateway/src/controllers/leads.controller.test.ts b/api-gateway/src/controllers/leads.controller.test.ts index d615160..fac98d0 100644 --- a/api-gateway/src/controllers/leads.controller.test.ts +++ b/api-gateway/src/controllers/leads.controller.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { + buscarLeads, + buscarLeadsEndpoint, cambiarEstadoLead, crearLead, descartarLead, @@ -131,3 +133,55 @@ describe('descartarLead (POST /api/leads/:itemId/descartar)', () => { expect(monday.motivos).toEqual([]) }) }) + +describe('buscarLeads (GET /api/leads/buscar)', () => { + const lead = (overrides: Partial): LeadMonday => ({ + id: 'lead-1', + nombre: 'Lead Prueba', + createdAt: '2026-08-01T00:00:00Z', + updatedAt: '2026-08-01T00:00:00Z', + estado: 'NUEVO', + origen: '', + telefono: '', + email: '', + vendedorIds: [], + ...overrides, + }) + + it('matchea por nombre, teléfono y email (case-insensitive)', () => { + const leads = [ + lead({ id: 'a', nombre: 'MARÍA FERNANDA LÓPEZ' }), + lead({ id: 'b', telefono: '477 123 4567' }), + lead({ id: 'c', email: 'Cliente@Correo.com' }), + lead({ id: 'd', nombre: 'Otro', telefono: '333', email: '' }), + ] + expect(buscarLeads(leads, 'fernanda').map((l) => l.id)).toEqual(['a']) + expect(buscarLeads(leads, '123').map((l) => l.id)).toEqual(['b']) + expect(buscarLeads(leads, 'cliente@').map((l) => l.id)).toEqual(['c']) + }) + + it('ignora acentos tanto en el lead como en la búsqueda', () => { + const leads = [lead({ id: 'a', nombre: 'MARÍA FERNANDA LÓPEZ', email: 'andres@correo.com' })] + expect(buscarLeads(leads, 'maria')).toHaveLength(1) + expect(buscarLeads(leads, 'andrés')).toHaveLength(1) + }) + + it('ordena por updatedAt descendente y limita a 15', () => { + const leads = Array.from({ length: 20 }, (_, i) => + lead({ id: `l-${i}`, nombre: `Cliente Común ${i}`, updatedAt: `2026-08-${String(i + 1).padStart(2, '0')}T00:00:00Z` }), + ) + const r = buscarLeads(leads, 'común') + expect(r).toHaveLength(15) + expect(r[0]?.id).toBe('l-19') // el más reciente primero + }) + + it('el endpoint responde 400 con q de menos de 2 caracteres', async () => { + const res = { json: vi.fn(), status: vi.fn() } as unknown as Parameters[1] + await expect( + buscarLeadsEndpoint({ query: { q: 'a' } } as unknown as Parameters[0], res), + ).rejects.toMatchObject({ status: 400 }) + await expect( + buscarLeadsEndpoint({ query: {} } as unknown as Parameters[0], res), + ).rejects.toMatchObject({ status: 400 }) + }) +}) diff --git a/api-gateway/src/controllers/leads.controller.ts b/api-gateway/src/controllers/leads.controller.ts index 4da9962..6bc7b81 100644 --- a/api-gateway/src/controllers/leads.controller.ts +++ b/api-gateway/src/controllers/leads.controller.ts @@ -35,6 +35,29 @@ const descartarSchema = z.object({ comentario: z.string().trim().min(1).optional(), }) +// ---------- Búsqueda de leads (prellenado del alta de clientes) ---------- + +const LIMITE_BUSQUEDA_LEADS = 15 + +/** Minúsculas y sin diacríticos, para comparar nombres/teléfonos con acentos */ +const sinAcentos = (s: string): string => + s + .normalize('NFD') + .replace(/[̀-ͯ]/g, '') // diacríticos U+0300–U+036F + .toLowerCase() + +/** + * Filtra leads por q en nombre/teléfono/email (case-insensitive, sin acentos), + * ordena por actualización reciente y limita a 15 resultados. + */ +export function buscarLeads(leads: LeadMonday[], q: string): LeadMonday[] { + const needle = sinAcentos(q) + return leads + .filter((l) => [l.nombre, l.telefono, l.email].some((campo) => sinAcentos(campo).includes(needle))) + .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) + .slice(0, LIMITE_BUSQUEDA_LEADS) +} + type MondayLeads = Pick< MondayService, | 'cambiarValorColumna' @@ -144,3 +167,23 @@ export async function descartarLeadEndpoint(req: Request, res: Response): Promis const motivoItemId = await descartarLead(mondayService, itemId, body.motivo, body.comentario) res.json({ itemId, estado: 'DESCARTADO', motivo: body.motivo, motivoItemId }) } + +/** + * GET /api/leads/buscar?q=texto — autocompletado para prellenar el alta de + * clientes desde un lead. q mínimo 2 caracteres; devuelve máximo 15. + */ +export async function buscarLeadsEndpoint(req: Request, res: Response): Promise { + const q = String(req.query['q'] ?? '').trim() + if (q.length < 2) throw new AppError(400, 'La búsqueda requiere al menos 2 caracteres') + const encontrados = buscarLeads(await mondayService.getLeads(), q) + res.json( + encontrados.map((l) => ({ + id: l.id, + nombre: l.nombre, + telefono: l.telefono, + email: l.email, + estado: l.estado, + origen: l.origen, + })), + ) +} diff --git a/api-gateway/src/controllers/usuarios.controller.test.ts b/api-gateway/src/controllers/usuarios.controller.test.ts new file mode 100644 index 0000000..dba4b22 --- /dev/null +++ b/api-gateway/src/controllers/usuarios.controller.test.ts @@ -0,0 +1,139 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Request, Response } from 'express' +import type { Usuario } from '@prisma/client' +import { + actualizarVinculoMondayEndpoint, + listarUsuariosMondayEndpoint, +} from './usuarios.controller.js' +import { AppError } from '../utils/errors.js' + +// ---------- Monday y Prisma mockeados (los tests no tocan red ni BD) ---------- + +const mondayMock = vi.hoisted(() => ({ getUsuariosWorkspace: vi.fn() })) +const prismaMock = vi.hoisted(() => ({ usuario: { update: vi.fn() } })) + +vi.mock('../services/monday.service.js', () => ({ mondayService: mondayMock })) +vi.mock('../services/prisma.js', () => ({ prisma: prismaMock })) + +const USUARIOS_WORKSPACE = [ + { id: 13676, nombre: 'Erick Peña', email: 'erick@frasa.mx' }, + { id: 20400, nombre: 'Cristina Barrera', email: 'cristina@frasa.mx' }, +] + +function usuarioFake(overrides: Partial = {}): Usuario { + return { + id: 'u-1', + username: 'erick israel pena ramos', + nombre: 'ERICK ISRAEL PEÑA RAMOS', + passwordHash: 'hash', + rol: 'vendedor', + asignacion: 'MONTEBELLO', + empleadoId: 13676, + mondayUserId: null, + activo: true, + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, + } +} + +function reqFake(overrides: Partial = {}): Request { + return { + body: {}, + usuarioToken: { sub: 'u-1', username: 'erick israel pena ramos', rol: 'vendedor' }, + ...overrides, + } as Request +} + +function resFake() { + const json = vi.fn() + const status = vi.fn().mockReturnValue({ json }) + return { res: { json, status } as unknown as Response, status, json } +} + +beforeEach(() => { + vi.clearAllMocks() + mondayMock.getUsuariosWorkspace.mockResolvedValue(USUARIOS_WORKSPACE) +}) + +describe('listarUsuariosMondayEndpoint (GET /api/monday/usuarios)', () => { + it('devuelve los usuarios del workspace con id numérico', async () => { + const { res, json } = resFake() + + await listarUsuariosMondayEndpoint(reqFake(), res) + + expect(json).toHaveBeenCalledWith([ + { id: 13676, nombre: 'Erick Peña', email: 'erick@frasa.mx' }, + { id: 20400, nombre: 'Cristina Barrera', email: 'cristina@frasa.mx' }, + ]) + }) + + it('propaga el error si Monday falla (el error handler responde 5xx)', async () => { + mondayMock.getUsuariosWorkspace.mockRejectedValue(new Error('Monday caído')) + const { res } = resFake() + await expect(listarUsuariosMondayEndpoint(reqFake(), res)).rejects.toThrow('Monday caído') + }) +}) + +describe('actualizarVinculoMondayEndpoint (PATCH /api/usuarios/me/monday)', () => { + it('con id válido guarda el vínculo y devuelve el perfil actualizado', async () => { + prismaMock.usuario.update.mockResolvedValue(usuarioFake({ mondayUserId: 13676 })) + const { res, json } = resFake() + + await actualizarVinculoMondayEndpoint(reqFake({ body: { mondayUserId: 13676 } }), res) + + expect(prismaMock.usuario.update).toHaveBeenCalledWith({ + where: { id: 'u-1' }, + data: { mondayUserId: 13676 }, + }) + expect(json).toHaveBeenCalledWith({ + nombre: 'ERICK ISRAEL PEÑA RAMOS', + rol: 'vendedor', + asignacion: 'MONTEBELLO', + mondayUserId: 13676, + }) + }) + + it('con id inexistente en el workspace responde 400 y no toca la BD', async () => { + const { res } = resFake() + + const error = await actualizarVinculoMondayEndpoint( + reqFake({ body: { mondayUserId: 99999 } }), + res, + ).catch((e: unknown) => e) + + expect(error).toBeInstanceOf(AppError) + expect(error).toMatchObject({ status: 400 }) + expect(prismaMock.usuario.update).not.toHaveBeenCalled() + }) + + it('con null desvincula sin consultar Monday', async () => { + prismaMock.usuario.update.mockResolvedValue(usuarioFake({ mondayUserId: null })) + const { res, json } = resFake() + + await actualizarVinculoMondayEndpoint(reqFake({ body: { mondayUserId: null } }), res) + + expect(mondayMock.getUsuariosWorkspace).not.toHaveBeenCalled() + expect(prismaMock.usuario.update).toHaveBeenCalledWith({ + where: { id: 'u-1' }, + data: { mondayUserId: null }, + }) + expect(json).toHaveBeenCalledWith(expect.objectContaining({ mondayUserId: null })) + }) + + it('sin token verificado: 400', async () => { + const { res } = resFake() + await expect( + actualizarVinculoMondayEndpoint(reqFake({ usuarioToken: undefined }), res), + ).rejects.toThrow('Falta el token verificado') + expect(prismaMock.usuario.update).not.toHaveBeenCalled() + }) + + it('body inválido (string): ZodError (el error handler lo traduce a 400)', async () => { + const { res } = resFake() + await expect( + actualizarVinculoMondayEndpoint(reqFake({ body: { mondayUserId: '13676' } }), res), + ).rejects.toThrow() + expect(prismaMock.usuario.update).not.toHaveBeenCalled() + }) +}) diff --git a/api-gateway/src/controllers/usuarios.controller.ts b/api-gateway/src/controllers/usuarios.controller.ts new file mode 100644 index 0000000..4b2fca9 --- /dev/null +++ b/api-gateway/src/controllers/usuarios.controller.ts @@ -0,0 +1,44 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' +import { mondayService } from '../services/monday.service.js' +import { prisma } from '../services/prisma.js' +import { aPublico } from '../services/auth.service.js' +import { BadRequestError } from '../utils/errors.js' + +/** + * Vínculo usuario del ecosistema ↔ usuario de Monday (people). Es el mapeo + * que alimenta round-robin, mis-leads, actividades y alertas; el usuario lo + * elige él mismo desde la landing (Configuración de cuenta). + */ + +/** GET /api/monday/usuarios (Bearer) → usuarios activos del workspace */ +export async function listarUsuariosMondayEndpoint(_req: Request, res: Response): Promise { + const usuarios = await mondayService.getUsuariosWorkspace() + res.json(usuarios.map((u) => ({ id: u.id, nombre: u.nombre, email: u.email }))) +} + +const vinculoMondaySchema = z.object({ + mondayUserId: z.number().int().positive().nullable(), +}) + +/** + * PATCH /api/usuarios/me/monday (Bearer) { mondayUserId: number | null } + * null desvincula. Un id que no existe en el workspace → 400. + */ +export async function actualizarVinculoMondayEndpoint(req: Request, res: Response): Promise { + if (!req.usuarioToken) throw new BadRequestError('Falta el token verificado') + const { mondayUserId } = vinculoMondaySchema.parse(req.body) + + if (mondayUserId !== null) { + const usuarios = await mondayService.getUsuariosWorkspace() + if (!usuarios.some((u) => u.id === mondayUserId)) { + throw new BadRequestError(`El usuario de Monday ${mondayUserId} no existe en el workspace`) + } + } + + const usuario = await prisma.usuario.update({ + where: { id: req.usuarioToken.sub }, + data: { mondayUserId }, + }) + res.json(aPublico(usuario)) +} diff --git a/api-gateway/src/routes/index.ts b/api-gateway/src/routes/index.ts index 009e348..2909789 100644 --- a/api-gateway/src/routes/index.ts +++ b/api-gateway/src/routes/index.ts @@ -45,8 +45,13 @@ import { correrRecordatorios, listarRecordatorios } from '../controllers/recorda import { cambiarEstadoLeadEndpoint, crearLeadEndpoint, + buscarLeadsEndpoint, descartarLeadEndpoint, } from '../controllers/leads.controller.js' +import { + actualizarVinculoMondayEndpoint, + listarUsuariosMondayEndpoint, +} from '../controllers/usuarios.controller.js' import { resumenMiDiaEndpoint } from '../controllers/mi-dia.controller.js' import { webhookMonday } from '../controllers/webhooks.controller.js' import { @@ -121,10 +126,15 @@ apiRouter.post('/oportunidades/:mondayItemId/perder', perderOportunidadEndpoint) apiRouter.get('/mi-dia/resumen', resumenMiDiaEndpoint) +// GET estático: va ANTES de /leads/:itemId para no colisionar con el parámetro +apiRouter.get('/leads/buscar', buscarLeadsEndpoint) apiRouter.post('/leads', crearLeadEndpoint) apiRouter.post('/leads/:itemId/estado', cambiarEstadoLeadEndpoint) apiRouter.post('/leads/:itemId/descartar', descartarLeadEndpoint) +apiRouter.get('/monday/usuarios', listarUsuariosMondayEndpoint) +apiRouter.patch('/usuarios/me/monday', actualizarVinculoMondayEndpoint) + apiRouter.get('/reportes/resumen', resumenReportes) apiRouter.post('/webhooks/monday', webhookMonday) diff --git a/api-gateway/src/services/auth.service.test.ts b/api-gateway/src/services/auth.service.test.ts index 41d0bdd..e1f3db6 100644 --- a/api-gateway/src/services/auth.service.test.ts +++ b/api-gateway/src/services/auth.service.test.ts @@ -23,6 +23,7 @@ function usuarioFake(overrides: Partial = {}): Usuario { rol: 'vendedor', asignacion: 'MONTEBELLO', empleadoId: 13676, + mondayUserId: null, activo: true, createdAt: new Date(), updatedAt: new Date(), @@ -52,6 +53,7 @@ describe('login', () => { nombre: 'ERICK ISRAEL PEÑA RAMOS', rol: 'vendedor', asignacion: 'MONTEBELLO', + mondayUserId: null, }) expect(verificarToken(resultado.token).username).toBe('erick israel pena ramos') }) diff --git a/api-gateway/src/services/auth.service.ts b/api-gateway/src/services/auth.service.ts index 8e33364..b163ee5 100644 --- a/api-gateway/src/services/auth.service.ts +++ b/api-gateway/src/services/auth.service.ts @@ -9,6 +9,8 @@ export interface UsuarioPublico { nombre: string rol: string asignacion: string | null + // User ID de Monday vinculado (null = sin vincular; ver PATCH /usuarios/me/monday) + mondayUserId: number | null } export interface LoginResult { @@ -30,7 +32,12 @@ export function normalizarUsername(nombre: string): string { } export function aPublico(usuario: Usuario): UsuarioPublico { - return { nombre: usuario.nombre, rol: usuario.rol, asignacion: usuario.asignacion } + return { + nombre: usuario.nombre, + rol: usuario.rol, + asignacion: usuario.asignacion, + mondayUserId: usuario.mondayUserId, + } } /** diff --git a/apps/alta-clientes/package-lock.json b/apps/alta-clientes/package-lock.json index 8c18196..38ed3f0 100644 --- a/apps/alta-clientes/package-lock.json +++ b/apps/alta-clientes/package-lock.json @@ -23,15 +23,39 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.3", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^26.1.0", "tailwindcss": "^4.0.0", "typescript": "~5.6.3", "vite": "^6.0.7", "vitest": "^2.1.8" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -323,6 +347,121 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1479,6 +1618,61 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1671,6 +1865,49 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1900,6 +2137,20 @@ "utrie": "^1.0.2" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1907,6 +2158,57 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1925,6 +2227,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -1935,6 +2244,16 @@ "node": ">=6" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1945,6 +2264,13 @@ "node": ">=8" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dompurify": { "version": "2.5.9", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz", @@ -1973,6 +2299,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2120,6 +2459,19 @@ "dev": true, "license": "ISC" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -2134,12 +2486,60 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/idb-keyval": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz", "integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==", "license": "Apache-2.0" }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-url": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", @@ -2162,6 +2562,83 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2505,6 +2982,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2571,6 +3058,13 @@ "node": ">=18" } }, + "node_modules/nwsapi": { + "version": "2.2.25", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.25.tgz", + "integrity": "sha512-kktSgNDIbyEIs/LrE6dtEMnfzxSDpXViFl6c4gFjz/CgtnL4GDCR3wyZXzjvAXsNB8dXU4dAfvIOgLMH2/vwLg==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2589,6 +3083,19 @@ "opencollective-postinstall": "index.js" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -2662,6 +3169,28 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -2673,6 +3202,16 @@ "react-is": "^16.13.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -2883,6 +3422,33 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2962,6 +3528,13 @@ "node": ">=12.0.0" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", @@ -3078,6 +3651,39 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -4322,6 +4928,19 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/wasm-feature-detect": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.9.0.tgz", @@ -4334,6 +4953,30 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -4361,6 +5004,45 @@ "node": ">=8" } }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/apps/alta-clientes/package.json b/apps/alta-clientes/package.json index 55b845a..7febb09 100644 --- a/apps/alta-clientes/package.json +++ b/apps/alta-clientes/package.json @@ -26,9 +26,12 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.3", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^26.1.0", "tailwindcss": "^4.0.0", "typescript": "~5.6.3", "vite": "^6.0.7", diff --git a/apps/alta-clientes/src/components/BusquedaLead.test.tsx b/apps/alta-clientes/src/components/BusquedaLead.test.tsx new file mode 100644 index 0000000..4512c62 --- /dev/null +++ b/apps/alta-clientes/src/components/BusquedaLead.test.tsx @@ -0,0 +1,69 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import BusquedaLead from './BusquedaLead' +import type { LeadResumen } from '../services/api' + +type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise + +const LEADS: LeadResumen[] = [ + { id: 'lead-1', nombre: 'MARÍA FERNANDA LÓPEZ', telefono: '4771234567', email: 'maria@correo.com', estado: 'NUEVO', origen: 'Web' }, + { id: 'lead-2', nombre: 'Juan Pérez', telefono: '', email: 'juan@correo.com', estado: 'CONTACTADO', origen: '' }, +] + +const respuestaOk = (data: unknown) => + new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } }) + +let fetchMock: ReturnType> + +beforeEach(() => { + fetchMock = vi.fn().mockResolvedValue(respuestaOk(LEADS)) + vi.stubGlobal('fetch', fetchMock) +}) + +afterEach(() => { + cleanup() + vi.unstubAllGlobals() +}) + +const llamadasBusqueda = () => fetchMock.mock.calls.filter(([url]) => String(url).includes('/leads/buscar')) + +describe('BusquedaLead', () => { + it('con menos de 2 caracteres no llama al API', async () => { + render( undefined} />) + fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'm' } }) + await new Promise((r) => setTimeout(r, 400)) // más que el debounce + expect(llamadasBusqueda()).toHaveLength(0) + }) + + it('renderiza los resultados de la búsqueda (nombre, teléfono, estado)', async () => { + render( undefined} />) + fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'maria' } }) + + expect(await screen.findByText('MARÍA FERNANDA LÓPEZ')).toBeTruthy() + expect(screen.getByText(/4771234567 · NUEVO/)).toBeTruthy() + expect(screen.getByText('Juan Pérez')).toBeTruthy() + const [url] = llamadasBusqueda()[0] as [string] + expect(url).toContain('/leads/buscar?q=maria') + }) + + it('al elegir un resultado avisa con el lead completo', async () => { + const onSeleccion = vi.fn() + render() + fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'maria' } }) + + fireEvent.click(await screen.findByText('MARÍA FERNANDA LÓPEZ')) + expect(onSeleccion).toHaveBeenCalledWith(LEADS[0]) + }) + + it('muestra "sin resultados" y errores de red', async () => { + render( undefined} />) + + fetchMock.mockResolvedValueOnce(respuestaOk([])) + fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'nadie' } }) + expect(await screen.findByText('Sin leads que coincidan.')).toBeTruthy() + + fetchMock.mockRejectedValueOnce(new Error('red caída')) + fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'otro' } }) + expect((await screen.findByRole('alert')).textContent).toContain('No se pudo buscar en Monday') + }) +}) diff --git a/apps/alta-clientes/src/components/BusquedaLead.tsx b/apps/alta-clientes/src/components/BusquedaLead.tsx new file mode 100644 index 0000000..8180b16 --- /dev/null +++ b/apps/alta-clientes/src/components/BusquedaLead.tsx @@ -0,0 +1,92 @@ +import { useEffect, useState } from 'react' +import { buscarLeads, type LeadResumen } from '../services/api' + +const DEBOUNCE_MS = 300 + +interface Props { + onSeleccion: (lead: LeadResumen) => void +} + +/** + * Buscador con debounce de leads de Monday (GET /leads/buscar). No llama al + * API con menos de 2 caracteres; al elegir un resultado avisa con onSeleccion. + */ +export default function BusquedaLead({ onSeleccion }: Props) { + const [q, setQ] = useState('') + const [resultados, setResultados] = useState(null) + const [buscando, setBuscando] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + const texto = q.trim() + if (texto.length < 2) { + setResultados(null) + setError('') + setBuscando(false) + return + } + setBuscando(true) + const timer = setTimeout(() => { + buscarLeads(texto) + .then((lista) => { + setResultados(lista) + setError('') + }) + .catch(() => { + setResultados(null) + setError('No se pudo buscar en Monday. Intenta de nuevo.') + }) + .finally(() => setBuscando(false)) + }, DEBOUNCE_MS) + return () => clearTimeout(timer) + }, [q]) + + return ( +
+ + setQ(e.target.value)} + placeholder="Escribe al menos 2 caracteres…" + className="w-full max-w-md border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 focus:border-neutral-900" + /> + + {buscando &&

Buscando…

} + {error && ( +

+ {error} +

+ )} + {!buscando && !error && resultados !== null && resultados.length === 0 && ( +

Sin leads que coincidan.

+ )} + + {resultados !== null && resultados.length > 0 && ( +
    + {resultados.map((lead) => ( +
  • + +
  • + ))} +
+ )} +
+ ) +} diff --git a/apps/alta-clientes/src/hooks/useAltaCliente.ts b/apps/alta-clientes/src/hooks/useAltaCliente.ts index 95244bb..da269e9 100644 --- a/apps/alta-clientes/src/hooks/useAltaCliente.ts +++ b/apps/alta-clientes/src/hooks/useAltaCliente.ts @@ -57,6 +57,7 @@ const economicosVacios = (): AltaCliente['economicos'] => ({ }) const estadoInicial = (): Omit => ({ + leadMondayId: null, datosGenerales: datosGeneralesVacios(), empleo: empleoVacio(), economicos: economicosVacios(), diff --git a/apps/alta-clientes/src/lib/fixture.ts b/apps/alta-clientes/src/lib/fixture.ts index 1f1cca4..2590217 100644 --- a/apps/alta-clientes/src/lib/fixture.ts +++ b/apps/alta-clientes/src/lib/fixture.ts @@ -9,6 +9,7 @@ export const FIRMA_PRUEBA = * (PASCUAL MARTIN TELLEZ CHIGUINDO — pdf_formatos.txt) */ export const fixtureAlta: AltaCliente = { + leadMondayId: null, datosGenerales: { campaniaVenta: '', fecha: '2026-07-19', diff --git a/apps/alta-clientes/src/lib/leadAlta.test.ts b/apps/alta-clientes/src/lib/leadAlta.test.ts new file mode 100644 index 0000000..568282a --- /dev/null +++ b/apps/alta-clientes/src/lib/leadAlta.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' +import { leadAPrellenado } from './leadAlta' + +describe('leadAPrellenado', () => { + it('prellena nombre (mayúsculas), email y celular cuando el lead los trae', () => { + const pre = leadAPrellenado({ nombre: ' María Fernanda López ', email: 'maria@correo.com', telefono: '4771234567' }) + expect(pre).toEqual({ + nombreCompleto: 'MARÍA FERNANDA LÓPEZ', + email: 'maria@correo.com', + celular: '4771234567', + }) + }) + + it('omite email y celular cuando el lead no los tiene (no inventa datos)', () => { + expect(leadAPrellenado({ nombre: 'Lead Sin Contacto', email: '', telefono: '' })).toEqual({ + nombreCompleto: 'LEAD SIN CONTACTO', + }) + }) + + it('prellena solo lo que haya (email sin teléfono)', () => { + const pre = leadAPrellenado({ nombre: 'Solo Email', email: 'solo@correo.com', telefono: '' }) + expect(pre).toEqual({ nombreCompleto: 'SOLO EMAIL', email: 'solo@correo.com' }) + expect(pre.celular).toBeUndefined() + }) +}) diff --git a/apps/alta-clientes/src/lib/leadAlta.ts b/apps/alta-clientes/src/lib/leadAlta.ts new file mode 100644 index 0000000..1013852 --- /dev/null +++ b/apps/alta-clientes/src/lib/leadAlta.ts @@ -0,0 +1,16 @@ +import type { DatosGenerales } from '../types' +import type { LeadResumen } from '../services/api' + +/** + * Transformación pura lead de Monday → campos del wizard de alta. + * Prellena solo lo que el lead tenga (nombre en mayúsculas, como el formato). + */ +export function leadAPrellenado( + lead: Pick, +): Partial { + return { + nombreCompleto: lead.nombre.trim().toUpperCase(), + ...(lead.email ? { email: lead.email.trim() } : {}), + ...(lead.telefono ? { celular: lead.telefono.trim() } : {}), + } +} diff --git a/apps/alta-clientes/src/pages/AltaClientePage.test.tsx b/apps/alta-clientes/src/pages/AltaClientePage.test.tsx new file mode 100644 index 0000000..524a690 --- /dev/null +++ b/apps/alta-clientes/src/pages/AltaClientePage.test.tsx @@ -0,0 +1,57 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import AltaClientePage from './AltaClientePage' +import type { LeadResumen } from '../services/api' + +type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise + +const LEAD: LeadResumen = { + id: 'lead-9', + nombre: 'María Fernanda López', + telefono: '4771234567', + email: 'maria@correo.com', + estado: 'NUEVO', + origen: 'Web', +} + +const respuestaOk = (data: unknown) => + new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } }) + +let fetchMock: ReturnType> + +beforeEach(() => { + fetchMock = vi.fn().mockImplementation((input) => { + const url = String(input) + if (url.includes('/borradores')) { + // Un borrador para que la pantalla inicial sea el picker + return Promise.resolve( + respuestaOk([{ id: 'b-1', nombre: 'Borrador previo', paso: 0, updatedAt: '2026-08-20T10:00:00Z' }]), + ) + } + if (url.includes('/leads/buscar')) return Promise.resolve(respuestaOk([LEAD])) + return Promise.reject(new Error(`fetch inesperado: ${url}`)) + }) + vi.stubGlobal('fetch', fetchMock) +}) + +afterEach(() => { + cleanup() + vi.unstubAllGlobals() +}) + +describe('AltaClientePage — alta desde lead de Monday', () => { + it('al elegir un lead el wizard arranca con datosGenerales prellenados', async () => { + render() + + // Picker visible con la sección de búsqueda + const busqueda = await screen.findByLabelText(/Buscar lead/) + fireEvent.change(busqueda, { target: { value: 'maria' } }) + fireEvent.click(await screen.findByText('María Fernanda López')) + + // Wizard paso 1 con los campos del lead ya capturados + const nombre = (await screen.findByLabelText(/Nombre completo/)) as HTMLInputElement + expect(nombre.value).toBe('MARÍA FERNANDA LÓPEZ') + expect((screen.getByLabelText(/Correo electrónico|Email/i) as HTMLInputElement).value).toBe('maria@correo.com') + expect((screen.getByLabelText(/Celular/i) as HTMLInputElement).value).toBe('4771234567') + }) +}) diff --git a/apps/alta-clientes/src/pages/AltaClientePage.tsx b/apps/alta-clientes/src/pages/AltaClientePage.tsx index b2c3ee8..4ab1465 100644 --- a/apps/alta-clientes/src/pages/AltaClientePage.tsx +++ b/apps/alta-clientes/src/pages/AltaClientePage.tsx @@ -19,8 +19,11 @@ import { obtenerCotizacion, subirDocumentos, type BorradorResumen, + type LeadResumen, } from '../services/api' import { cotizacionAPrellenado } from '../lib/cotizacionAlta' +import { leadAPrellenado } from '../lib/leadAlta' +import BusquedaLead from '../components/BusquedaLead' /** Lee ?cotizacion= del query string o del hash (#cotizacion=) */ function paramCotizacion(): string | null { @@ -82,7 +85,9 @@ export default function AltaClientePage() { listarBorradores() .then((lista) => { setBorradores(lista) - setPantalla(lista.length > 0 ? 'picker' : 'wizard') + // El picker es siempre la entrada: además de los borradores ofrece + // "Empezar nuevo" y la búsqueda de leads de Monday. + setPantalla('picker') }) .catch(() => setPantalla('wizard')) // sin conexión: el wizard funciona igual // eslint-disable-next-line react-hooks/exhaustive-deps @@ -103,11 +108,9 @@ export default function AltaClientePage() { const borrarBorrador = async (id: string) => { try { await eliminarBorrador(id) - setBorradores((prev) => { - const restantes = prev.filter((b) => b.id !== id) - if (restantes.length === 0) setPantalla('wizard') - return restantes - }) + // Al borrar el último borrador el picker sigue siendo la pantalla de + // entrada (ofrece "Empezar nuevo" y la búsqueda de leads). + setBorradores((prev) => prev.filter((b) => b.id !== id)) } catch { setErrorCarga('No se pudo eliminar el borrador.') } @@ -120,6 +123,16 @@ export default function AltaClientePage() { setPantalla('wizard') } + // Alta nueva prellenada desde un lead de Monday (nombre, email, celular) + const empezarDesdeLead = (lead: LeadResumen) => { + reiniciar() + setBorradorId(null) + setCargoDeBorrador(false) + actualizar('datosGenerales', { ...datos.datosGenerales, ...leadAPrellenado(lead) }) + actualizar('leadMondayId', lead.id) + setPantalla('wizard') + } + const guardar = async () => { try { const b = await guardarBorrador({ ...(borradorId ? { id: borradorId } : {}), paso, datos }) @@ -193,39 +206,43 @@ export default function AltaClientePage() {
-

- Tienes {borradores.length} borrador{borradores.length === 1 ? '' : 'es'} guardado - {borradores.length === 1 ? '' : 's'} -

+ {borradores.length > 0 && ( + <> +

+ Tienes {borradores.length} borrador{borradores.length === 1 ? '' : 'es'} guardado + {borradores.length === 1 ? '' : 's'} +

+
    + {borradores.map((b) => ( +
  • +
    +

    {b.nombre}

    +

    + Paso {b.paso + 1} de {PASOS.length} · {formatoFechaHora(b.updatedAt)} +

    +
    +
    + + +
    +
  • + ))} +
+ + )} {errorCarga &&

{errorCarga}

} -
    - {borradores.map((b) => ( -
  • -
    -

    {b.nombre}

    -

    - Paso {b.paso + 1} de {PASOS.length} · {formatoFechaHora(b.updatedAt)} -

    -
    -
    - - -
    -
  • - ))} -
+ +
+

+ Desde un lead de Monday +

+

+ Empieza un alta nueva con el nombre, email y celular del lead ya capturados. +

+
+ +
+
) diff --git a/apps/alta-clientes/src/services/api.ts b/apps/alta-clientes/src/services/api.ts index 7d22efe..e90825c 100644 --- a/apps/alta-clientes/src/services/api.ts +++ b/apps/alta-clientes/src/services/api.ts @@ -164,6 +164,25 @@ export async function obtenerCotizacion(id: string): Promise { + const res = await fetch(`${API_URL}/leads/buscar?q=${encodeURIComponent(q)}`, conAuth()) + manejar401(res) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return (await res.json()) as LeadResumen[] +} + // ---------- Liga remota de firma (auth) ---------- export interface LigaRemota { diff --git a/apps/alta-clientes/src/types/index.ts b/apps/alta-clientes/src/types/index.ts index f24acb4..d1b1370 100644 --- a/apps/alta-clientes/src/types/index.ts +++ b/apps/alta-clientes/src/types/index.ts @@ -146,6 +146,9 @@ export interface DatosFirma { } export interface AltaCliente { + // ID del item del tablero Leads de Monday cuando el alta se prellenó desde + // un lead (null = captura manual). Passthrough hasta payloadJson del API. + leadMondayId: string | null datosGenerales: DatosGenerales empleo: DatosEmpleo economicos: DatosEconomicos diff --git a/apps/alta-clientes/vitest.config.ts b/apps/alta-clientes/vitest.config.ts index 58eb15a..3cc4061 100644 --- a/apps/alta-clientes/vitest.config.ts +++ b/apps/alta-clientes/vitest.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config' // Config de tests separada para no mezclar los tipos de Vite y Vitest export default defineConfig({ test: { - environment: 'node', + // jsdom para los tests de componentes; los tests puros corren igual + environment: 'jsdom', }, }) diff --git a/apps/landing/package-lock.json b/apps/landing/package-lock.json index bb57e69..1fd6598 100644 --- a/apps/landing/package-lock.json +++ b/apps/landing/package-lock.json @@ -14,10 +14,13 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.3", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^2.1.8", + "jsdom": "^26.1.0", "tailwindcss": "^4.0.0", "typescript": "~5.6.3", "vite": "^6.0.7", @@ -38,6 +41,27 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -272,6 +296,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -327,6 +361,121 @@ "dev": true, "license": "MIT" }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1513,6 +1662,61 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1725,6 +1929,16 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-regex": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", @@ -1751,6 +1965,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1931,6 +2155,20 @@ "node": ">= 8" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1938,6 +2176,20 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1956,6 +2208,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -1966,6 +2225,16 @@ "node": ">=6" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1976,6 +2245,13 @@ "node": ">=8" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2011,6 +2287,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2222,6 +2511,19 @@ "node": ">=8" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2229,6 +2531,47 @@ "dev": true, "license": "MIT" }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2239,6 +2582,13 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2333,6 +2683,46 @@ "dev": true, "license": "MIT" }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2637,6 +3027,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2750,6 +3150,13 @@ "node": ">=18" } }, + "node_modules/nwsapi": { + "version": "2.2.25", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.25.tgz", + "integrity": "sha512-kktSgNDIbyEIs/LrE6dtEMnfzxSDpXViFl6c4gFjz/CgtnL4GDCR3wyZXzjvAXsNB8dXU4dAfvIOgLMH2/vwLg==", + "dev": true, + "license": "MIT" + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -2757,6 +3164,19 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2857,6 +3277,54 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", @@ -2878,6 +3346,13 @@ "react": "^19.2.8" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2934,6 +3409,33 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3134,6 +3636,13 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", @@ -3231,6 +3740,52 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", @@ -4447,6 +5002,67 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4578,6 +5194,45 @@ "node": ">=8" } }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/apps/landing/package.json b/apps/landing/package.json index 91abc80..8c41bcd 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -17,10 +17,13 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.3", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^2.1.8", + "jsdom": "^26.1.0", "tailwindcss": "^4.0.0", "typescript": "~5.6.3", "vite": "^6.0.7", diff --git a/apps/landing/src/App.tsx b/apps/landing/src/App.tsx index c30fe40..bd4b335 100644 --- a/apps/landing/src/App.tsx +++ b/apps/landing/src/App.tsx @@ -1,5 +1,6 @@ import { MODULOS } from './data/modulos' import { TarjetaModulo } from './components/TarjetaModulo' +import { ConfiguracionCuenta } from './components/ConfiguracionCuenta' export default function App() { return ( @@ -19,6 +20,8 @@ export default function App() { ))} + +