diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..2aee2dc --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,54 @@ +name: deploy-branch + +# CI/CD: cada push a cualquier rama se despliega en .localesp.es +# Estrategia A — un entorno por rama. El registro DNS A es manual (precondición). +# Detalles: docs/07-cicd.md + +on: + push: + branches: + - "**" + delete: + workflow_dispatch: + inputs: + branch: + description: "Rama a desplegar (vacío = la actual)" + required: false + default: "" + +jobs: + deploy: + if: github.event_name != 'delete' + runs-on: self-hosted + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build (npm ci + vite build) + shell: bash + run: | + # better-sqlite3 (v12) requiere C++20; el GCC 8 del sistema (AlmaLinux 8) no lo soporta. + # gcc-toolset-12 (GCC 12) ya está instalado en /opt/rh. + source /opt/rh/gcc-toolset-12/enable 2>/dev/null || echo "WARN: gcc-toolset-12 no encontrado" + npm ci + npm run build + + - name: Deploy branch + env: + BRANCH: ${{ github.event.inputs.branch || github.ref_name }} + run: bash scripts/deploy.sh "$BRANCH" + + # Al borrar una rama, se limpia su entorno + teardown: + if: github.event_name == 'delete' + runs-on: self-hosted + timeout-minutes: 5 + steps: + - name: Checkout (default branch, trae scripts/) + uses: actions/checkout@v4 + - name: Teardown branch env + env: + # github.event.ref en un 'delete' es refs/heads/ + BRANCH: ${{ github.event.ref }} + run: bash scripts/teardown.sh "${BRANCH#refs/heads/}" diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml new file mode 100644 index 0000000..a7df262 --- /dev/null +++ b/.gitea/workflows/tests.yml @@ -0,0 +1,49 @@ +# CI: ejecuta la suite rápida de pruebas (offline) y el build en cada push y PR. +# La suite de integración con Nominatim (red real) es opt-in: se lanza a mano +# desde la pestaña Actions del repositorio, y un fallo de red no rompe el CI. +name: tests + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + test: + name: Unitarias + componente + build + # El runner disponible es self-hosted (AlmaLinux, Node 20 del sistema); + # las deps soportan Node >=18/20 (jsdom@26), así que no hace falta setup-node. + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + - name: Instalar dependencias + # better-sqlite3 (v12) requiere C++20 y el GCC 8 base de AlmaLinux 8 + # no lo soporta: se activa gcc-toolset-12 igual que en deploy.yml + shell: bash + run: | + source /opt/rh/gcc-toolset-12/enable 2>/dev/null || echo "WARN: gcc-toolset-12 no encontrado" + npm ci + + - name: Pruebas unitarias y de componente (offline) + run: npm test + + - name: Build de producción + run: npm run build + + integracion: + name: Integración (Nominatim real, opt-in) + runs-on: self-hosted + if: github.event_name == 'workflow_dispatch' + continue-on-error: true # depende de un servicio externo: informativa + steps: + - uses: actions/checkout@v4 + + - name: Instalar dependencias + shell: bash + run: | + source /opt/rh/gcc-toolset-12/enable 2>/dev/null || echo "WARN: gcc-toolset-12 no encontrado" + npm ci + + - name: Pruebas de integración (API en memoria + Nominatim) + run: npm run test:integration diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2137e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +dist +.env +.env.local +localesp.db +.claude/ +.codex/ +.pi/ diff --git a/index.html b/index.html new file mode 100644 index 0000000..f327c7f --- /dev/null +++ b/index.html @@ -0,0 +1,54 @@ + + + + + + Locales Españoles + + + + + + + + + + + + + + + + +
+ + + diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..d3d88e9 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,10 @@ +[build] + command = "npm run build" + publish = "dist" + +# SPA: todas las rutas van al index.html (React gestiona /admin; el resto es +# la PWA principal, instalable en móvil) +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 diff --git a/openspec/changes/archive/2026-08-16-pasarela-proponer-local/.openspec.yaml b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/.openspec.yaml new file mode 100644 index 0000000..0c73c8f --- /dev/null +++ b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-15 diff --git a/openspec/changes/archive/2026-08-16-pasarela-proponer-local/design.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/design.md new file mode 100644 index 0000000..a42eb20 --- /dev/null +++ b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/design.md @@ -0,0 +1,116 @@ +# Diseño — Pasarela para proponer locales + +## Context + +- `src/App.jsx` es la única app en producción (PC y móvil): `main.jsx` solo monta `App` y `AdminPanel`; `src/AppMovil.jsx` es código muerto no referenciado. Por tanto "funcionar para PC y móvil" se resuelve haciendo el FAB y el modal responsive dentro de `App.jsx`. +- El formulario actual es un bloque en línea (`mostrarForm`) con nombre, provincia, categoría, subcategoría, ubicación (dos modos: dirección / enlace de Google Maps), descripción y puntuación, más checkbox de privacidad, filtro de palabras prohibidas y comprobación de duplicados contra `server/index.js`. +- El backend ya tolera la ausencia de `descripcion` (columna nullable) y `puntuacion` (`?? 0` al insertar), así que eliminarlos del cliente no exige cambios de API ni migraciones. +- La extracción de coordenadas hoy es una única función `parsearEnlaceGoogleMaps` en `src/utils/geo.js`, y la geocodificación por texto se hace con Nominatim (`geocodificarDireccion`, `buscarLugares`). +- Convenciones del proyecto: estilos inline con variables CSS (`--color-*`), componentes pequeños en `src/components/`, utilidades puras en `src/utils/`, sin dependencias de UI externas. El patrón de modal existente es `ModalPrivacidad.jsx` (overlay `fixed` + `stopPropagation` + `role="dialog"`). +- PWA instalada (service worker): el FAB debe convivir con la barra de navegación de la PWA en iOS/Android (`env(safe-area-inset-bottom)`). + +## Goals / Non-Goals + +**Goals:** +- Sustituir el botón de cabecera por un FAB fijo abajo a la derecha, con tooltip, usable en PC y móvil. +- Pasarela en modal con un campo por pantalla, empezando por Ubicación. +- Campo universal de ubicación que autodetecta Google Maps / Waze / OpenStreetMap / coordenadas (fallback: texto vía Nominatim). +- Inferir Nombre y Provincia desde la ubicación (geocodificación inversa Nominatim), editables. +- Arquitectura de proveedores extensible: varias clases con una interfaz común (`detecta`/`extrae`), resultado extensible, registro abierto/cerrado. +- Eliminar comentario y puntuación del flujo de propuesta. + +**Non-Goals:** +- Gestionar comentarios/valoraciones de otra forma (cambio posterior). +- Modificar backend, base de datos, panel de administración o `AppMovil.jsx`. +- Autocompletado en la pantalla de Nombre (se anota como mejora futura; ahora es un input simple pre-rellenado). +- Mapa de previsualización dentro del modal (se anota como mejora futura). + +## Decisions + +### D1 — Un único `App.jsx` responsive para PC y móvil +**Decisión:** FAB y pasarela se implementan en `App.jsx`; no se toca `AppMovil.jsx` (muerto). +**Alternativas:** duplicar la UI en `AppMovil.jsx` — rechazado: duplicaría lógica y mantiene vivo código que no está en producción. + +### D2 — FAB con tooltip propio, sin dependencias +**Decisión:** botón `position: fixed; bottom/right` con `z-index` por debajo del modal (modal usa 1000) y `padding-bottom: env(safe-area-inset-bottom)` en móvil. Tooltip implementado con estado React (`onMouseEnter`/`onMouseLeave`/`onFocus`/`onBlur`) y posicionado a la izquierda del botón, con `aria-label` en el botón para accesibilidad sin ratón. +**Alternativas:** atributo `title` nativo — rechazado por no ser estilizable ni fiable en móvil/táctil; librería de tooltips — rechazada para no añadir dependencias a un proyecto sin ellas. + +### D3 — Modal propio siguiendo el patrón `ModalPrivacidad` +**Decisión:** overlay `fixed` con `role="dialog"`/`aria-modal`, cierre con click en fondo, ✕ y `Escape` (listener de teclado en `useEffect`), foco inicial en el primer control de cada pantalla. En pantallas estrechas (`matchMedia` vía hook `useEsMovil`), el panel ocupa todo el ancho y casi todo el alto; en escritorio queda centrado con `maxWidth` ~480px. Sin portal: se renderiza al final del árbol de `App`, igual que `ModalPrivacidad`. +**Alternativas:** `createPortal` — innecesario aquí (no hay `overflow: hidden` ancestros problemáticos); librería de modals — descartada por política de cero dependencias de UI. + +### D4 — Pasarela como máquina de pasos sobre el `form` existente +**Decisión:** el estado del formulario (`form`) vive en `App.jsx` como hoy; la pasarela añade un índice `paso` y un array declarativo de pantallas: `[ubicacion, nombre, provincia, categoria, subcategoria, resumen]`. Cada pantalla es un componente que recibe `form`/`setForm` y expone su validez (`puedeContinuar`) para habilitar "Siguiente". "Atrás" simplemente decrementa el índice (los datos se conservan). La pantalla final muestra el resumen + checkbox de privacidad y dispara el flujo de envío existente (`contienepalabrasProhibidas` → `comprobarDuplicado` → `guardarPropuesta`), con los avisos de error/duplicado renderizados dentro del modal. +**Alternativas:** cada paso con estado propio y despachar al final — más propenso a inconsistencias; mover el envío a un hook nuevo — innecesario, la lógica actual ya está aislada en funciones de `App.jsx`. + +### D5 — `FORM_VACIO` sin `descripcion` ni `puntuacion` +**Decisión:** el objeto vacío del formulario elimina ambos campos y se dejan de enviar en el payload (`enviarPropuesta` los omite; el backend ya aplica defaults). `formValido` deja de exigir `puntuacion > 0`. `StarRating` deja de usarse en la propuesta (se conserva para mostrar valoraciones existentes en tarjetas). +**Alternativas:** seguir enviando `puntuacion: 0` y `descripcion: ""` explícitos — innecesario: el servidor ya normaliza. + +### D6 — Arquitectura de proveedores de ubicación (núcleo extensible) +**Decisión:** nuevo módulo `src/utils/ubicacion/`: + +``` +src/utils/ubicacion/ + ProveedorUbicacion.js # clase base = interfaz + helpers compartidos + proveedorGoogleMaps.js # detecta: *.google.* / maps.app.goo.gl … + proveedorWaze.js # detecta: waze.com/ul?ll=… (o q=texto → delega en Nominatim) + proveedorOSM.js # detecta: openstreetmap.org (mlat/mlon, #map=…) + proveedorCoordenadas.js # detecta: "41.38, 2.17" (± grados, coma o punto) + proveedorNominatim.js # fallback: texto de dirección → search; y reverse() para inferir + index.js # registro ordenado + resolverUbicacion(entrada) + inferirDesdeCoords() +``` + +- **Interfaz** (JS no tiene interfaces nativas): la clase base `ProveedorUbicacion` define el contrato documentado — `id`, `detecta(entrada) → boolean`, `extrae(entrada) → Promise` (la base lanza `Error("sin implementar")`, como una abstracta) — y helpers comunes: cabeceras/UA para Nominatim, `normalizar()` de textos y `emparejarProvincia()` contra `PROVINCIAS` (insensible a acentos/mayúsculas, reutilizando la lógica de `AutocompletarLocal`). +- **Resultado extensible:** `{ proveedor, lat, lng, nombre?, provincia?, direccion?, …extra }`. Los consumidores leen solo lo que conocen; añadir campos mañana (teléfono, horarios…) no rompe nada. +- **Resolver:** `resolverUbicacion(entrada)` recorre el array `PROVEEDORES` en orden de prioridad (coordenadas → Google → Waze → OSM → Nominatim texto) y devuelve el resultado del primero cuyo `detecta` acepte. Registrar un proveedor nuevo = añadir la clase y push al array (abierto/cerrado: ni el resolver ni las demás clases cambian). +- **Inferencia:** tras obtener coordenadas, `inferirDesdeCoords(lat, lng)` llama a `Nominatim reverse` (`/reverse?format=jsonv2&addressdetails=1&namedetails=1`) y devuelve `{ nombre, provincia, direccion }` con la provincia normalizada al listado canónico (si no casa, provincia `""` para forzar selección manual). `geocodificarDireccion`/`parsearEnlaceGoogleMaps` de `geo.js` se reubican como implementación interna de los proveedores (se mantiene `geo.js` exportando lo que usan `LocalCard` y el mapa: enlaces y distancias). +- **Errores de red:** todo `fetch` de proveedor va envuelto para devolver `null`/`{ ok: false }` controlado; la UI muestra aviso, nunca lanza al consumidor. + +**Alternativas:** funciones sueltas con un `switch` — no extensible (habría que editar el switch por cada backend); cadena de responsabilidad con instancias singleton — equivalente pero más boilerplate en un codebase sin clases hasta ahora; *strategy registry* en JSON/config — pierde la capacidad de lógica por proveedor (p.ej. Waze delegando en Nominatim para `q=`). + +### D7 — Pantalla de Ubicación: un solo input con feedback inmediato +**Decisión:** un input universal con debounce (~600ms). Al resolver, muestra un chip con el proveedor detectado (p. ej. "Google Maps detectado") y las coordenadas; si hay inferencia, se marcan `nombre`/`provincia` en el `form` (el usuario las ve ya pre-rellenadas en sus pantallas). Si `detecta` no reconoce nada, aviso "No se pudo interpretar la ubicación" y "Siguiente" deshabilitado hasta obtener una resolución válida. +**Alternativas:** mantener los dos modos (dirección/enlace) — rechazado por requisito ("un solo campo universal"); validar solo al pulsar Siguiente — peor UX, no avisa de errores mientras se pega el enlace. + +### D8 — Envío y confirmación +**Decisión:** al enviar, el modal permanece abierto con estados "Comprobando…/Enviando…" y, si hay duplicado, muestra el aviso con las opciones actuales ("Enviar de todos modos" / "Revisar datos"). Tras éxito: cierra modal, resetea `form`, y muestra el banner de confirmación existente (`enviado`). +**Alternativas:** mover el banner dentro del modal — innecesario y menos visible. + +## Risks / Trade-offs + +- [Rate-limit de Nominatim (política ~1 req/s)] → Debounce de 600ms, una sola llamada por resolución (search **o** reverse, no ambas salvo fallback), y mensaje de error amable con reintento. +- [FAB solapa contenido o la barra PWA en móvil] → `safe-area-inset-bottom`, tamaño moderado (52–56px) y `z-index` inferior al modal; se prueba en viewport estrecho. +- [Teclado móvil sube y tapa el modal] → modal con `height` flexible y `overflow-y: auto`; inputs al inicio de cada pantalla. +- [Enlaces acortados (maps.app.goo.gl, waze.to) sin coordenadas en la URL] → v1 no hace *unshorten* (requiere fetch al proveedor y CORS no garantizado): se avisa "pega el enlace completo"; se anota como mejora futura. +- [Inferencia errónea (nombre genérico tipo "Calle Mayor")] → los campos son editables y la pantalla de resumen lo hace evidente antes de enviar. +- [JS sin interfaces nativas] → el contrato se documenta en la clase base y se comprueba en el registro (métodos presentes); riesgo bajo al ser codebase propio. + +## Migration Plan + +1. Implementación puramente frontend (rama/PR normal). Sin cambios de esquema ni de API: desplegar y listo. +2. Rollback: revert del deploy (Netlify) — el backend nunca cambia de forma durante este cambio. + +## Open Questions + +- ¿Querremos *unshorten* de enlaces acortados en el futuro? (Anotado como mejora; no bloquea.) -> mejora +- ¿Autocompletado en la pantalla de Nombre reutilizando `AutocompletarLocal`? (Anotado como mejora futura; hoy input simple pre-rellenado.) + +## Extensión: pruebas automatizadas (añadida tras la implementación inicial) + +La verificación manual (sección 5 de tasks) se complementa con una suite programática para poder regresionar sin navegador. Se decide: + +### D9 — Stack de pruebas ligado a Vite, sin dependencias de más +**Decisión:** `vitest` como runner (mismo ecosistema que el proyecto, sin Jest), `jsdom` + `@testing-library/react` para pruebas de componente (con `// @vitest-environment jsdom` por archivo) y `@testing-library/dom` como peer obligatorio. El `fetch` se simula con `vi.stubGlobal` — no se añade msw ni librerías de mock HTTP, en línea con la política de cero dependencias extra del proyecto. +**Alternativas:** Jest + jsdom — duplicaría configuración fuera del ecosistema Vite; msw — potente pero innecesario para el alcance actual. + +### D10 — Integración con el backend local: supertest + BD en memoria +**Decisión:** refactor mínimo y sin cambio de comportamiento en `server/`: `server/index.js` exporta `app` y solo escucha cuando se ejecuta como script (`node server/index.js`), y `server/db.js` admite `process.env.LOCALESP_DB` (incluido `:memory:`) como ruta de la base de datos. Las pruebas de integración usan `supertest` contra la app Express con SQLite en memoria: totalmente offline y deterministas, sin puerto real. +**Alternativas:** levantar el servidor en un puerto efímero y hacer fetch real — más frágil y lento; mockear la BD — dejaría de ser integración. + +### D11 — Integración con Nominatim real: suite opt-in respetuosa con el rate-limit +**Decisión:** suite separada ejecutada con `npm run test:integration` (config propia) que hace llamadas reales a Nominatim: sondea la conectividad en `beforeAll` y se auto-salta (`describe.skipIf`) sin red; pocos casos secuenciales con ~1 req/s para respetar la política de uso. La suite rápida (`npm test`) queda 100% offline para CI. +**Alternativas:** grabar/replicar respuestas — mantendría los fixtures sincronizados con la API real; llamadas reales en la suite por defecto — haría el CI dependiente de un servicio externo. + +### D12 — Alias de nombres cooficiales de provincias (endurecimiento) +Al escribir las pruebas surgió que respuestas de Nominatim en euskera/catalán/gallego (`Bizkaia`, `Girona`, `Lleida`, `Ourense`, `Illes Balears`, `Asturies`) no casaban con el listado canónico. `ProveedorUbicacion.emparejarProvincia` gana un mapa pequeño de alias normalizados → provincia canónica, cubierto por pruebas unitarias. diff --git a/openspec/changes/archive/2026-08-16-pasarela-proponer-local/proposal.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/proposal.md new file mode 100644 index 0000000..8a34f4b --- /dev/null +++ b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/proposal.md @@ -0,0 +1,38 @@ +# Pasarela para proponer locales + +## Why + +El botón "Proponer local" actual vive en la cabecera y despliega un formulario largo en línea con muchos campos a la vez (nombre, provincia, categoría, ubicación con dos modos, descripción y puntuación). En móvil resulta abrumador y la ubicación exige que el usuario elija manualmente entre "dirección" o "enlace de Google Maps". Queremos una experiencia guiada, campo a campo, que arranque por la ubicación (un único campo universal que acepte enlaces de Google Maps, Waze, OpenStreetMap o coordenadas) y que infiera automáticamente nombre y provincia, reduciendo el esfuerzo del proponente y los errores de datos. + +## What Changes + +- El botón "Proponer local" pasa de la cabecera a un **botón flotante (FAB) fijo en la esquina inferior derecha**, visible y usable tanto en PC como en móvil. +- Al pasar el ratón por encima (hover / foco), muestra un **tooltip** que explica que sirve para añadir un nuevo local. +- Al pulsarlo se abre una **pasarela (wizard) en un modal**: cada campo se especifica en su propia pantalla, con navegación atrás/siguiente e indicador de progreso. +- **Primera pantalla: Ubicación**, un solo campo universal que autodetecta el origen del input: enlace de Google Maps, enlace de Waze, enlace de OpenStreetMap o coordenadas geográficas (y texto de dirección como fallback vía Nominatim). +- **Nombre y Provincia se infieren** automáticamente a partir de la ubicación introducida (geocodificación inversa con Nominatim), quedando pre-rellenados en sus pantallas para que el usuario los confirme o corrija. +- **Integraciones con backends de mapas extensible**: varias clases (adaptadores) implementan una misma interfaz (`detecta`/`extrae`), registradas en un resolver que autodetecta el proveedor. El resultado de `extrae` es un objeto extensible (coordenadas hoy, otros campos en el futuro). Añadir un proveedor nuevo no debe requerir tocar el resto del código. +- **Se elimina el campo comentario/descripción** del formulario de propuesta (se gestionará en otro cambio posterior). +- **Se elimina el campo puntuación** del formulario de propuesta. +- Se mantiene la lógica existente de validación: filtro de palabras prohibidas, comprobación de duplicados (avisos dentro del modal) y aceptación de la política de privacidad antes de enviar. +- No hay cambios de API: el backend ya tolera `descripcion` y `puntuacion` ausentes (valores por defecto). + +## Capabilities + +### New Capabilities +- `boton-flotante-proponer-local`: botón de acción flotante fijo en la esquina inferior derecha, responsive (PC y móvil), con tooltip al hover y apertura de la pasarela al pulsarlo. +- `pasarela-proponer-local`: modal de propuesta paso a paso con una pantalla por campo (Ubicación → Nombre → Provincia → Categoría → Tipo específico → Revisión y envío), sin campos de comentario ni puntuación. +- `proveedores-ubicacion`: sistema extensible de integraciones con backends de ubicación (Google Maps, Waze, OpenStreetMap, coordenadas y geocodificación Nominatim) bajo una interfaz común, capaz de extraer coordenadas e inferir nombre y provincia, ampliable a otras extracciones futuras. + +### Modified Capabilities + + +## Impact + +- **`src/App.jsx`**: se elimina el botón de cabecera y el formulario en línea (`mostrarForm`); se sustituye por el FAB + modal. La lógica de envío (filtro de palabras, duplicados, `enviarPropuesta`) se reutiliza. +- **Nuevos componentes** (`src/components/`): botón flotante, modal de la pasarela y pantallas de cada paso. +- **Nuevos módulos** (`src/utils/proveedores/`): interfaz común, adaptadores por proveedor y resolver de autodetección. +- **`src/utils/geo.js`**: se reutiliza/amplía (geocodificación inversa vía Nominatim; extracción de coordenadas por proveedor). +- **Sin cambios de backend**: `server/index.js` y la base de datos ya aceptan propuestas sin `descripcion` ni `puntuacion` (defaults `null`/`0`); el panel de administración no se modifica. +- **`src/AppMovil.jsx`** no está referenciado por `main.jsx` (código muerto): no se toca. +- Accesibilidad: tooltip también vía `aria-label`, foco atrapado en el modal, cierre con Escape. diff --git a/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/boton-flotante-proponer-local/spec.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/boton-flotante-proponer-local/spec.md new file mode 100644 index 0000000..7b313c2 --- /dev/null +++ b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/boton-flotante-proponer-local/spec.md @@ -0,0 +1,36 @@ +# Botón flotante para proponer locales + +## ADDED Requirements + +### Requirement: Botón flotante fijo en la esquina inferior derecha +El sistema SHALL mostrar un botón de acción flotante (FAB) fijado en la esquina inferior derecha de la ventana, siempre visible sobre el contenido, tanto en PC como en móvil. + +#### Scenario: Visible en escritorio +- **WHEN** la página se carga en una pantalla de escritorio +- **THEN** el botón flotante aparece fijado en la esquina inferior derecha, por encima del resto del contenido + +#### Scenario: Visible en móvil +- **WHEN** la página se carga o se hace scroll en un dispositivo móvil +- **THEN** el botón flotante permanece fijo y accesible en la esquina inferior derecha, sin quedar oculto por el contenido ni interferir con la barra inferior existente + +#### Scenario: Reemplazo del botón de cabecera +- **WHEN** la página se carga +- **THEN** ya no existe el botón "Proponer local" en la cabecera y el único punto de entrada para proponer un local es el botón flotante + +### Requirement: Tooltip explicativo al pasar el ratón +El botón flotante SHALL mostrar un tooltip al pasar el ratón por encima (hover) y al recibir foco por teclado, explicando que sirve para añadir un nuevo local. + +#### Scenario: Tooltip en hover +- **WHEN** el usuario pasa el ratón por encima del botón flotante +- **THEN** aparece un tooltip con un texto explicativo del tipo "Añade un nuevo local al directorio" + +#### Scenario: Tooltip accesible sin ratón +- **WHEN** el botón flotante recibe el foco por teclado o se consulta con un lector de pantalla +- **THEN** el botón expone su propósito mediante `aria-label`, de modo que la función sea accesible sin depender del hover + +### Requirement: El botón abre la pasarela en un modal +El botón flotante SHALL abrir, al pulsarlo, la pasarela de propuesta de local en un modal por encima de la página. + +#### Scenario: Apertura de la pasarela +- **WHEN** el usuario pulsa el botón flotante +- **THEN** se abre un modal con la pasarela de propuesta, comenzando por la primera pantalla (Ubicación) diff --git a/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/pasarela-proponer-local/spec.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/pasarela-proponer-local/spec.md new file mode 100644 index 0000000..a33166e --- /dev/null +++ b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/pasarela-proponer-local/spec.md @@ -0,0 +1,92 @@ +# Pasarela de propuesta de local + +## ADDED Requirements + +### Requirement: Pasarela paso a paso en un modal +El sistema SHALL presentar la propuesta de un local como una pasarela (wizard) dentro de un modal, con un solo campo por pantalla, navegación atrás/siguiente e indicador de progreso. + +#### Scenario: Una pantalla por campo +- **WHEN** la pasarela está abierta +- **THEN** cada pantalla muestra únicamente el campo correspondiente al paso actual, con botones para avanzar y retroceder + +#### Scenario: Navegación hacia atrás +- **WHEN** el usuario pulsa "Atrás" en una pantalla intermedia +- **THEN** vuelve a la pantalla anterior conservando los datos ya introducidos + +#### Scenario: Avance bloqueado sin dato válido +- **WHEN** el campo de la pantalla actual está vacío o es inválido +- **THEN** el botón "Siguiente" está deshabilitado y la pantalla no avanza + +#### Scenario: Cierre del modal +- **WHEN** el usuario cierra el modal (botón ✕, botón Cancelar o tecla Escape) +- **THEN** la pasarela se cierra y se descarta el borrador en curso + +### Requirement: Orden de las pantallas empezando por Ubicación +La pasarela SHALL presentar las pantallas en este orden: 1) Ubicación, 2) Nombre, 3) Provincia, 4) Categoría, 5) Tipo específico (opcional), 6) Revisión y envío. + +#### Scenario: Primera pantalla +- **WHEN** se abre la pasarela +- **THEN** la primera pantalla que se muestra es la de Ubicación + +#### Scenario: Tipo específico omitible +- **WHEN** el usuario está en la pantalla de Tipo específico +- **THEN** puede continuar sin seleccionar ninguna subcategoría + +### Requirement: Inferencia de Nombre y Provincia desde la Ubicación +El sistema SHALL pre-rellenar los campos Nombre y Provincia a partir de la ubicación introducida en la primera pantalla (geocodificación inversa), permitiendo al usuario corregirlos. + +#### Scenario: Nombre y provincia pre-rellenados +- **WHEN** el usuario introduce una ubicación válida en la primera pantalla y esta se resuelve con éxito +- **THEN** al llegar a las pantallas de Nombre y Provincia ambos campos aparecen pre-rellenados con los valores inferidos + +#### Scenario: Valores corregibles +- **WHEN** los valores inferidos no son correctos +- **THEN** el usuario puede editarlos libremente antes de enviar + +#### Scenario: Degradación sin inferencia +- **WHEN** la inferencia falla o devuelve campos vacíos +- **THEN** las pantallas de Nombre y Provincia aparecen vacías y el usuario los rellena manualmente, sin bloquear la pasarela + +### Requirement: Sin campo de comentario ni puntuación +La pasarela SHALL NOT incluir el campo de comentario/descripción ni el campo de puntuación; la propuesta se envía sin esos datos. + +#### Scenario: Envío sin comentario ni puntuación +- **WHEN** el usuario completa la pasarela y envía +- **THEN** la propuesta se registra con nombre, provincia, categoría, subcategoría, dirección/enlace y coordenadas, sin descripción y sin puntuación + +### Requirement: Revisión y aceptación de privacidad antes de enviar +La última pantalla de la pasarela SHALL mostrar un resumen de los datos introducidos y el checkbox de aceptación de la política de privacidad, siendo este último obligatorio para enviar. + +#### Scenario: Envío bloqueado sin aceptar privacidad +- **WHEN** el usuario llega a la pantalla final sin marcar la casilla de privacidad +- **THEN** el botón de envío está deshabilitado hasta que la marque + +#### Scenario: Resumen antes de enviar +- **WHEN** el usuario llega a la pantalla final +- **THEN** ve un resumen legible de todos los datos introducidos con posibilidad de volver atrás a corregirlos + +### Requirement: Validaciones existentes integradas en la pasarela +La pasarela SHALL conservar las validaciones actuales del formulario: filtro de palabras prohibidas y comprobación de duplicados, mostrando los avisos dentro del modal. + +#### Scenario: Palabra prohibida detectada +- **WHEN** el texto introducido contiene una palabra no permitida +- **THEN** se muestra un aviso dentro del modal y la propuesta no se envía + +#### Scenario: Posible duplicado detectado +- **WHEN** al enviar se detecta un local igual o muy cercano +- **THEN** se muestra el aviso de duplicado dentro del modal con las coincidencias y las opciones de enviar de todos modos o revisar los datos + +#### Scenario: Envío correcto +- **WHEN** la propuesta se envía con éxito +- **THEN** el modal se cierra y aparece la confirmación de propuesta enviada en la página principal + +### Requirement: Modal responsive accesible +El modal de la pasarela SHALL ser usable en PC (diálogo centrado) y móvil (ocupando prácticamente toda la pantalla), con foco contenido y cierre mediante Escape. + +#### Scenario: Uso en móvil +- **WHEN** la pasarela se abre en una pantalla estrecha +- **THEN** el modal ocupa el ancho completo y es operativo con teclado y scroll táctil + +#### Scenario: Foco contenido en el modal +- **WHEN** el modal está abierto +- **THEN** el foco de teclado permanece dentro del modal y la tecla Escape lo cierra diff --git a/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/proveedores-ubicacion/spec.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/proveedores-ubicacion/spec.md new file mode 100644 index 0000000..5db96cd --- /dev/null +++ b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/specs/proveedores-ubicacion/spec.md @@ -0,0 +1,73 @@ +# Proveedores de ubicación (integraciones extensible) + +## ADDED Requirements + +### Requirement: Interfaz común para proveedores de ubicación +Cada integración con un backend de ubicación SHALL implementarse como una clase que cumpla una misma interfaz: identificarse (`id`), indicar si reconoce una entrada (`detecta(entrada)`) y extraer la información disponible (`extrae(entrada)`). + +#### Scenario: Contrato uniforme +- **WHEN** se consulta cualquier proveedor registrado +- **THEN** expone los métodos `detecta(entrada)` y `extrae(entrada)` con la misma firma y semántica, de modo que el consumidor no necesita conocer la clase concreta + +### Requirement: Resultado de extracción extensible +El método `extrae(entrada)` SHALL devolver un objeto de resultado extensible que incluya como mínimo las coordenadas (`lat`, `lng`) y el identificador del proveedor que las extrajo, pudiendo incorporar campos adicionales en el futuro sin romper a los consumidores. + +#### Scenario: Extracción mínima +- **WHEN** un proveedor extrae una ubicación válida +- **THEN** el resultado contiene `lat`, `lng` y el identificador del proveedor + +#### Scenario: Campos adicionales sin ruptura +- **WHEN** en el futuro un proveedor añade nuevos campos al resultado (horarios, teléfono, fotos…) +- **THEN** los consumidores existentes siguen funcionando sin modificación, ignorando los campos que no conocen + +### Requirement: Autodetección del proveedor a partir de la entrada +El sistema SHALL autodetectar automáticamente el proveedor adecuado para una entrada dada, recorriendo los proveedores registrados y usando el primero que la reconozca. + +#### Scenario: Enlace de Google Maps +- **WHEN** el usuario pega un enlace de Google Maps (formatos `@lat,lng`, `!3d…!4d…` o `?q=lat,lng`) +- **THEN** el sistema lo resuelve con el proveedor de Google Maps y obtiene las coordenadas + +#### Scenario: Enlace de Waze +- **WHEN** el usuario pega un enlace de Waze (`waze.com/ul`, parámetro `ll=lat,lng` o búsqueda `q=`) +- **THEN** el sistema lo resuelve con el proveedor de Waze y obtiene las coordenadas + +#### Scenario: Enlace de OpenStreetMap +- **WHEN** el usuario pega un enlace de openstreetmap.org (parámetros `mlat`/`mlon` o fragmento `#map=zoom/lat/lng`) +- **THEN** el sistema lo resuelve con el proveedor de OpenStreetMap y obtiene las coordenadas + +#### Scenario: Coordenadas geográficas sueltas +- **WHEN** el usuario escribe coordenadas directamente (por ejemplo `41.3851, 2.1734`) +- **THEN** el sistema lo resuelve con el proveedor de coordenadas y obtiene lat/lng + +#### Scenario: Texto de dirección como fallback +- **WHEN** la entrada no coincide con ningún enlace soportado y parece una dirección en texto +- **THEN** el sistema la resuelve con el proveedor de geocodificación (Nominatim) realizando una búsqueda por texto + +#### Scenario: Entrada no reconocida +- **WHEN** la entrada no es reconocida por ningún proveedor ni resuelta como dirección +- **THEN** el sistema informa de que no se pudo interpretar la ubicación y no avanza de pantalla + +### Requirement: Inferencia de nombre y provincia +Los proveedores SHALL extraer, cuando estén disponibles, los campos inferibles `nombre` y `provincia` a partir de la entrada resuelta (geocodificación inversa cuando solo hay coordenadas), normalizando la provincia al listado canónico de provincias de España. + +#### Scenario: Inferencia desde coordenadas +- **WHEN** la entrada se resuelve a coordenadas sin más contexto +- **THEN** una geocodificación inversa obtiene un nombre descriptivo y la provincia canónica (coincidencia insensible a acentos y mayúsculas con el listado de PROVINCIAS) + +#### Scenario: Provincia fuera del listado +- **WHEN** la provincia inferida no coincide con ninguna provincia del listado canónico +- **THEN** el campo provincia queda vacío para que el usuario lo seleccione manualmente + +### Requirement: Extensibilidad sin modificar proveedores existentes +Añadir un proveedor nuevo SHALL consistir en crear una nueva clase que implemente la interfaz y registrarla en el resolver, sin modificar las clases existentes ni la lógica de la pasarela (principio abierto/cerrado). + +#### Scenario: Registro de un proveedor nuevo +- **WHEN** se añade una clase `MiProveedor` con `detecta`/`extrae` y se registra en la lista de proveedores +- **THEN** el campo universal empieza a aceptar las entradas que ese proveedor reconoce sin ningún otro cambio de código + +### Requirement: Errores de red degradados +Los proveedores SHALL tratar sus errores de red o de servicio devolviendo un fallo controlado, sin romper la pasarela ni propagar excepciones al consumidor. + +#### Scenario: Fallo del servicio de geocodificación +- **WHEN** Nominatim no responde o devuelve un error +- **THEN** la pantalla de Ubicación muestra un aviso de error y el usuario puede reintentar o continuar sin inferencia diff --git a/openspec/changes/archive/2026-08-16-pasarela-proponer-local/tasks.md b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/tasks.md new file mode 100644 index 0000000..766b12f --- /dev/null +++ b/openspec/changes/archive/2026-08-16-pasarela-proponer-local/tasks.md @@ -0,0 +1,70 @@ +## 1. Proveedores de ubicación (núcleo extensible) + +- [x] 1.1 Crear `src/utils/ubicacion/ProveedorUbicacion.js`: clase base con el contrato documentado (`id`, `detecta(entrada)`, `extrae(entrada)` que lanza "sin implementar") y helpers compartidos (`normalizar()`, `emparejarProvincia()` contra `PROVINCIAS`, fetch a Nominatim con cabeceras y manejo de errores que devuelve `null` en fallo) +- [x] 1.2 Implementar `proveedorCoordenadas.js`: detecta pares "lat, lng" (coma o punto, signo, espacios) y devuelve `{ proveedor, lat, lng }` +- [x] 1.3 Implementar `proveedorGoogleMaps.js`: detecta URLs de Google Maps y extrae coordenadas de los formatos `@lat,lng`, `!3d…!4d…` y `?q=lat,lng` (migrando `parsearEnlaceGoogleMaps` de `geo.js`) +- [x] 1.4 Implementar `proveedorWaze.js`: detecta `waze.com` y extrae de `ll=lat,lng`; si solo hay `q=texto`, delega la resolución en la búsqueda de Nominatim marcando el proveedor como waze +- [x] 1.5 Implementar `proveedorOSM.js`: detecta `openstreetmap.org` y extrae de `mlat`/`mlon` y del fragmento `#map=zoom/lat/lng` +- [x] 1.6 Implementar `proveedorNominatim.js`: búsqueda por texto (`/search`, migrando `geocodificarDireccion`) como fallback de dirección, y método `reverse(lat, lng)` (`/reverse` con `addressdetails=1&namedetails=1`) que devuelve `{ nombre, provincia, direccion }` con provincia normalizada al listado canónimo (vacía si no casa) +- [x] 1.7 Crear `src/utils/ubicacion/index.js`: array `PROVEEDORES` ordenado por prioridad (coordenadas → Google → Waze → OSM → Nominatim), función `resolverUbicacion(entrada)` que usa el primer `detecta` que acepte, y `inferirDesdeCoords(lat, lng)` que envuelve el reverse de Nominatim con manejo de errores +- [x] 1.8 Ajustar `src/utils/geo.js`: conservar exportaciones usadas por `LocalCard`/mapa (`enlaceGoogleMapsDesdeLocal`, `enlaceWazeDesdeLocal`, `distanciaKm`, `formatearDistancia`, `buscarLugares`) y reubicar lo migrado en los proveedores, sin romper imports existentes + +## 2. Pantallas de la pasarela + +- [x] 2.1 Crear `PasoUbicacion.jsx`: input universal con debounce (~600ms) que llama a `resolverUbicacion`; al resolver muestra chip con el proveedor detectado y coordenadas; llama a `inferirDesdeCoords` y pre-rellena `nombre`/`provincia`/`direccion` en el `form`; muestra avisos de entrada no reconocida o error de red; "Siguiente" solo habilitado con resolución válida +- [x] 2.2 Crear `PasoNombre.jsx` y `PasoProvincia.jsx`: inputs simples pre-rellenados con lo inferido (editables); provincia como select del listado `PROVINCIAS`; vacíos si la inferencia falló +- [x] 2.3 Crear `PasoCategoria.jsx` y `PasoSubcategoria.jsx`: selects con `CATEGORIAS`/`NOMBRES_CATEGORIAS`; subcategoría opcional (permite continuar sin elegir), solo accesible tras elegir categoría +- [x] 2.4 Crear `PasoResumen.jsx`: resumen legible de todos los datos, checkbox de aceptación de privacidad (enlace al `ModalPrivacidad` existente) y botón "Enviar propuesta" deshabilitado sin aceptación; renderiza avisos de palabra prohibida y de duplicado (con "Enviar de todos modos"/"Revisar datos") dentro del modal + +## 3. Modal de la pasarela y FAB + +- [x] 3.1 Crear `ModalPasarela.jsx`: overlay siguiendo el patrón de `ModalPrivacidad` (`role="dialog"`, `aria-modal`, cierre por fondo/✕/Escape), cabecera con título e indicador de progreso, cuerpo con la pantalla activa y pie con botones Atrás/Siguiente; en móvil panel a ancho completo y casi todo el alto (hook `useEsMovil` con `matchMedia`), en escritorio centrado `maxWidth≈480px`; foco inicial en el primer control de cada pantalla +- [x] 3.2 Crear `BotonFlotante.jsx`: FAB fijo abajo a la derecha (`z-index` < modal, `padding-bottom: env(safe-area-inset-bottom)`), con tooltip propio al hover/foco ("Añade un nuevo local al directorio") posicionado a la izquierda y `aria-label` en el botón +- [x] 3.3 Integrar en `App.jsx`: eliminar el botón "Proponer local" de la cabecera y el bloque de formulario en línea; renderizar `BotonFlotante` (abre el modal) y `ModalPasarela`; sustituir `mostrarForm` por estado `pasarelaAbierta`; actualizar el texto del pie "Proponlo arriba" para referirse al botón flotante + +## 4. Envío de la propuesta + +- [x] 4.1 Actualizar `FORM_VACIO` y el flujo de envío en `App.jsx`: quitar `descripcion` y `puntuacion` del formulario y del payload de `enviarPropuesta`; `formValido` sin requisito de puntuación; conservar filtro de palabras, `comprobarDuplicado`, fallback de coordenadas por provincia y confirmación `enviado` tras cerrar el modal; estados "Comprobando…/Enviando…" dentro del modal + +## 5. Verificación + +- [x] 5.1 Probar manualmente los formatos de entrada del campo universal: enlace Google Maps (@/, !3d!4d, ?q=), Waze (ll= y q=), OSM (mlat/mlon y #map=), coordenadas sueltas y dirección en texto; verificar aviso con entrada no reconocida + > Verificado a nivel de módulo contra los proveedores reales (con red): los 17 casos pasan, incluidos Waze `q=` delegando en Nominatim y entradas no reconocidas devolviendo `null` (dispara el aviso del paso). Falta el clic-through visual en navegador. + +- [x] 5.2 Verificar inferencia: nombre y provincia pre-rellenados desde coordenadas de varias comunidades (normalización de acentos), y flujo completo de propuesta sin comentario ni puntuación con aviso de duplicado y de palabra prohibida dentro del modal + > Verificado `npm test` (82/82: 51 unitarias + 31 componente) y `npm run test:integration` (10/10 con red real). Además se cubrió programáticamente gran parte del contenido: avisos de duplicado/palabra prohibida en `PasoResumen.spec.jsx`, flujo de propuesta sin comentario/puntuación contra el backend real en `apiLocal.spec.js`, y normalización multi-comunidad en `nominatim.spec.js`. Pendiente solo el clic-through visual en navegador con backend levantado. + +- [x] 5.3 Verificar responsive y accesibilidad: FAB visible sin tapar contenido en viewport estrecho y con barra PWA; tooltip en hover y foco; modal navegable con teclado, Escape cierra, foco contenido; enviar aceptación de privacidad obligatoria + > Cobertura programática añadida: tooltip en foco/hover (`BotonFlotante.spec.jsx`), Escape/✕/foco contenido/Tab wrap (`ModalPasarela.spec.jsx`), privacidad obligatoria (`PasoResumen.spec.jsx`). Pendiente solo la verificación visual en dispositivo/viewport estrecho y barra PWA (safe-area). + +- [x] 5.4 Verificar no-regresión: tarjetas, mapa, favoritos, reportes y panel admin siguen funcionando (imports de `geo.js` intactos); `npm run build` sin errores + > `npm run build` OK (65 módulos); imports de `geo.js` intactos en LocalCard, AdminPanel, App y AutocompletarLocal; sin referencias muertas (`mostrarForm`/`CampoUbicacion`/`formValido`). + +## 6. Infraestructura de pruebas + +- [x] 6.1 Añadir dependencias y configuración de pruebas: `vitest` + `jsdom` + `@testing-library/react` (+ `@testing-library/dom`) + `supertest` como devDeps; `vitest.config.mjs` (unitarias/componente, offline) y `vitest.config.integration.mjs`; scripts `npm test`, `npm run test:watch`, `npm run test:integration` +- [x] 6.2 Hacer el servidor comprobable sin cambiar su comportamiento: ruta de BD configurable (`process.env.LOCALESP_DB`, admite `:memory:`) y `server/index.js` exportando `app` escuchando solo al ejecutarse como script (`supertest` sin puerto) + +## 7. Pruebas unitarias (proveedores de ubicación) + +- [x] 7.1 `ProveedorUbicacion` (base): `normalizar()`, `emparejarProvincia()` (acentos, mayúsculas, artículos, alias cooficiales, sin coincidencia → `""`), `extrae()` de la base lanza "sin implementar", `detecta()` base no acepta nada, `fetchNominatim` devuelve `null` si `fetch` lanza o la respuesta no es ok +- [x] 7.2 `proveedorCoordenadas`: pares con coma y punto decimal, separador `,`/`;`/espacio, signo, espacios; latitud fuera de rango → `null`; texto que no es un par → `detecta` false +- [x] 7.3 `proveedorGoogleMaps`: formatos `@lat,lng`, `!3d…!4d…`, `?q=lat,lng`; prioridad `@` sobre `!3d!4d` (comportamiento migrado de `parsearEnlaceGoogleMaps`); enlace acortado `detecta` pero `extrae` → `null`; URL ajena → `detecta` false +- [x] 7.4 `proveedorWaze`: `ll=lat,lng` ok; `ll` malformado → `null`; `q=texto` delega en la búsqueda Nominatim (fetch simulado) marcando `proveedor: "waze"`; `q` sin resultados → `null` +- [x] 7.5 `proveedorOSM`: prioridad `mlat`/`mlon` sobre `#map=`; `#map=zoom/lat/lng`; sin ninguno → `null` +- [x] 7.6 `proveedorNominatim`: `detecta` rechaza URLs y textos cortos y acepta direcciones; `desdeResultado` normaliza provincia (Bizkaia→Vizcaya, A Coruña→La Coruña, desconocida→ `""`); `extrae` sin resultados → `null` +- [x] 7.7 `resolverUbicacion`/`inferirDesdeCoords`: orden de prioridad de `PROVEEDORES`, fallback de texto, proveedor que lanza → se degrada y prueba el siguiente, entrada vacía → `null`; `ETIQUETAS_PROVEEDOR` cubre todos los ids registrados + +## 8. Pruebas de componente (pasarela) + +- [x] 8.1 `PasoUbicacion`: debounce ~600ms (fake timers, no resuelve antes), chip con proveedor detectado y coordenadas, aviso de entrada no reconocida, pre-relleno del `form` (nombre/provincia/dirección/lat/lng/enlace según proveedor), aviso cuando la inferencia inversa falla +- [x] 8.2 `PasoNombre`/`PasoProvincia`/`PasoCategoria`/`PasoSubcategoria`: pre-rellenados con lo inferido y editables; provincia como select de `PROVINCIAS`; subcategoría deshabilitada sin categoría y opcional (se puede continuar) +- [x] 8.3 `PasoResumen`: resumen con los datos del form; botón "Enviar propuesta" deshabilitado sin aceptar privacidad y habilitado al marcar; abre `ModalPrivacidad` vía enlace; avisos de palabra prohibida y de duplicado con "Enviar de todos modos"/"Revisar datos"; estados "Comprobando…/Enviando…" +- [x] 8.4 `ModalPasarela`: arranca en Ubicación; "Siguiente" deshabilitado sin dato válido y habilitado con él; "Atrás" conserva datos; Escape y ✕ cierran (`onCerrar`); foco inicial en el primer control de cada pantalla y contención de Tab dentro del modal +- [x] 8.5 `BotonFlotante`: `aria-label` descriptivo, tooltip oculto por defecto y visible al enfocar (y oculto al desenfocar), `onClick` al pulsar + +## 9. Pruebas de integración (backends reales) + +- [x] 9.1 API local con `supertest` + BD `:memory:`: `POST /api/propuestas` sin `descripcion`/`puntuacion` → 201 con defaults (`puntuacion` 0, estado pendiente); `POST /api/check-duplicado` detecta por nombre normalizado (acentos/mayúsculas) + misma provincia; nombre igual en otra provincia y lejos → no duplicado; `GET /api/locales` arranca limpio +- [x] 9.2 Nominatim real (suite opt-in `npm run test:integration`, auto-skip sin red, ≤1 req/s): búsqueda por dirección → coordenadas + provincia canónica; reverse de Madrid y Bilbao → provincia canónica; reverse fuera de España → provincia vacía + diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..392946c --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,20 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours diff --git a/openspec/specs/boton-flotante-proponer-local/spec.md b/openspec/specs/boton-flotante-proponer-local/spec.md new file mode 100644 index 0000000..80d9046 --- /dev/null +++ b/openspec/specs/boton-flotante-proponer-local/spec.md @@ -0,0 +1,40 @@ +# Botón flotante para proponer locales + +## Purpose + +Punto de entrada único y siempre visible para proponer un nuevo local al directorio: un botón de acción flotante (FAB) fijo en la esquina inferior derecha, usable en PC y móvil, con tooltip explicativo y apertura de la pasarela de propuesta al pulsarlo. + +## Requirements + +### Requirement: Botón flotante fijo en la esquina inferior derecha +El sistema SHALL mostrar un botón de acción flotante (FAB) fijado en la esquina inferior derecha de la ventana, siempre visible sobre el contenido, tanto en PC como en móvil. + +#### Scenario: Visible en escritorio +- **WHEN** la página se carga en una pantalla de escritorio +- **THEN** el botón flotante aparece fijado en la esquina inferior derecha, por encima del resto del contenido + +#### Scenario: Visible en móvil +- **WHEN** la página se carga o se hace scroll en un dispositivo móvil +- **THEN** el botón flotante permanece fijo y accesible en la esquina inferior derecha, sin quedar oculto por el contenido ni interferir con la barra inferior existente + +#### Scenario: Reemplazo del botón de cabecera +- **WHEN** la página se carga +- **THEN** ya no existe el botón "Proponer local" en la cabecera y el único punto de entrada para proponer un local es el botón flotante + +### Requirement: Tooltip explicativo al pasar el ratón +El botón flotante SHALL mostrar un tooltip al pasar el ratón por encima (hover) y al recibir foco por teclado, explicando que sirve para añadir un nuevo local. + +#### Scenario: Tooltip en hover +- **WHEN** el usuario pasa el ratón por encima del botón flotante +- **THEN** aparece un tooltip con un texto explicativo del tipo "Añade un nuevo local al directorio" + +#### Scenario: Tooltip accesible sin ratón +- **WHEN** el botón flotante recibe el foco por teclado o se consulta con un lector de pantalla +- **THEN** el botón expone su propósito mediante `aria-label`, de modo que la función sea accesible sin depender del hover + +### Requirement: El botón abre la pasarela en un modal +El botón flotante SHALL abrir, al pulsarlo, la pasarela de propuesta de local en un modal por encima de la página. + +#### Scenario: Apertura de la pasarela +- **WHEN** el usuario pulsa el botón flotante +- **THEN** se abre un modal con la pasarela de propuesta, comenzando por la primera pantalla (Ubicación) diff --git a/openspec/specs/pasarela-proponer-local/spec.md b/openspec/specs/pasarela-proponer-local/spec.md new file mode 100644 index 0000000..b15a5cb --- /dev/null +++ b/openspec/specs/pasarela-proponer-local/spec.md @@ -0,0 +1,96 @@ +# Pasarela de propuesta de local + +## Purpose + +Experiencia guiada (wizard) en un modal para proponer un local con un solo campo por pantalla —empezando por la ubicación con autodetección del proveedor e inferencia de nombre/provincia—, sin campos de comentario ni puntuación, conservando las validaciones existentes (palabras prohibidas, duplicados, privacidad obligatoria). + +## Requirements + +### Requirement: Pasarela paso a paso en un modal +El sistema SHALL presentar la propuesta de un local como una pasarela (wizard) dentro de un modal, con un solo campo por pantalla, navegación atrás/siguiente e indicador de progreso. + +#### Scenario: Una pantalla por campo +- **WHEN** la pasarela está abierta +- **THEN** cada pantalla muestra únicamente el campo correspondiente al paso actual, con botones para avanzar y retroceder + +#### Scenario: Navegación hacia atrás +- **WHEN** el usuario pulsa "Atrás" en una pantalla intermedia +- **THEN** vuelve a la pantalla anterior conservando los datos ya introducidos + +#### Scenario: Avance bloqueado sin dato válido +- **WHEN** el campo de la pantalla actual está vacío o es inválido +- **THEN** el botón "Siguiente" está deshabilitado y la pantalla no avanza + +#### Scenario: Cierre del modal +- **WHEN** el usuario cierra el modal (botón ✕, botón Cancelar o tecla Escape) +- **THEN** la pasarela se cierra y se descarta el borrador en curso + +### Requirement: Orden de las pantallas empezando por Ubicación +La pasarela SHALL presentar las pantallas en este orden: 1) Ubicación, 2) Datos del local (nombre, provincia y dirección inferidos, juntos en una misma pantalla), 3) Categoría, 4) Tipo específico (opcional), 5) Revisión y envío. + +#### Scenario: Primera pantalla +- **WHEN** se abre la pasarela +- **THEN** la primera pantalla que se muestra es la de Ubicación + +#### Scenario: Tipo específico omitible +- **WHEN** el usuario está en la pantalla de Tipo específico +- **THEN** puede continuar sin seleccionar ninguna subcategoría + +### Requirement: Inferencia de Nombre, Provincia y Dirección desde la Ubicación +El sistema SHALL pre-rellenar los campos Nombre, Provincia y Dirección a partir de la ubicación introducida en la primera pantalla (geocodificación inversa), mostrándolos juntos en la pantalla de Datos del local y permitiendo al usuario corregirlos (incluida la dirección). + +#### Scenario: Datos pre-rellenados en su pantalla conjunta +- **WHEN** el usuario introduce una ubicación válida en la primera pantalla y esta se resuelve con éxito +- **THEN** al llegar a la pantalla de Datos del local los campos nombre, provincia y dirección aparecen pre-rellenados con los valores inferidos + +#### Scenario: Valores corregibles +- **WHEN** los valores inferidos no son correctos +- **THEN** el usuario puede editarlos libremente antes de enviar + +#### Scenario: Degradación sin inferencia +- **WHEN** la inferencia falla o devuelve campos vacíos +- **THEN** la pantalla de Datos del local aparece vacía y el usuario la rellena manualmente, sin bloquear la pasarela + +### Requirement: Sin campo de comentario ni puntuación +La pasarela SHALL NOT incluir el campo de comentario/descripción ni el campo de puntuación; la propuesta se envía sin esos datos. + +#### Scenario: Envío sin comentario ni puntuación +- **WHEN** el usuario completa la pasarela y envía +- **THEN** la propuesta se registra con nombre, provincia, categoría, subcategoría, dirección/enlace y coordenadas, sin descripción y sin puntuación + +### Requirement: Revisión y aceptación de privacidad antes de enviar +La última pantalla de la pasarela SHALL mostrar un resumen de los datos introducidos y el checkbox de aceptación de la política de privacidad, siendo este último obligatorio para enviar. + +#### Scenario: Envío bloqueado sin aceptar privacidad +- **WHEN** el usuario llega a la pantalla final sin marcar la casilla de privacidad +- **THEN** el botón de envío está deshabilitado hasta que la marque + +#### Scenario: Resumen antes de enviar +- **WHEN** el usuario llega a la pantalla final +- **THEN** ve un resumen legible de todos los datos introducidos con posibilidad de volver atrás a corregirlos + +### Requirement: Validaciones existentes integradas en la pasarela +La pasarela SHALL conservar las validaciones actuales del formulario: filtro de palabras prohibidas y comprobación de duplicados, mostrando los avisos dentro del modal. + +#### Scenario: Palabra prohibida detectada +- **WHEN** el texto introducido contiene una palabra no permitida +- **THEN** se muestra un aviso dentro del modal y la propuesta no se envía + +#### Scenario: Posible duplicado detectado +- **WHEN** al enviar se detecta un local igual o muy cercano +- **THEN** se muestra el aviso de duplicado dentro del modal con las coincidencias y las opciones de enviar de todos modos o revisar los datos + +#### Scenario: Envío correcto +- **WHEN** la propuesta se envía con éxito +- **THEN** el modal se cierra y aparece la confirmación de propuesta enviada en la página principal + +### Requirement: Modal responsive accesible +El modal de la pasarela SHALL ser usable en PC (diálogo centrado) y móvil (ocupando prácticamente toda la pantalla), con foco contenido y cierre mediante Escape. + +#### Scenario: Uso en móvil +- **WHEN** la pasarela se abre en una pantalla estrecha +- **THEN** el modal ocupa el ancho completo y es operativo con teclado y scroll táctil + +#### Scenario: Foco contenido en el modal +- **WHEN** el modal está abierto +- **THEN** el foco de teclado permanece dentro del modal y la tecla Escape lo cierra diff --git a/openspec/specs/proveedores-ubicacion/spec.md b/openspec/specs/proveedores-ubicacion/spec.md new file mode 100644 index 0000000..0c57c4a --- /dev/null +++ b/openspec/specs/proveedores-ubicacion/spec.md @@ -0,0 +1,77 @@ +# Proveedores de ubicación (integraciones extensible) + +## Purpose + +Sistema extensible de integraciones con backends de ubicación (Google Maps, Waze, OpenStreetMap, coordenadas sueltas y geocodificación Nominatim) bajo una interfaz común (`id`/`detecta`/`extrae`), capaz de autodetectar el proveedor de una entrada libre, extraer coordenadas e inferir nombre y provincia, y ampliable a otras extracciones futuras sin modificar los proveedores existentes. + +## Requirements + +### Requirement: Interfaz común para proveedores de ubicación +Cada integración con un backend de ubicación SHALL implementarse como una clase que cumpla una misma interfaz: identificarse (`id`), indicar si reconoce una entrada (`detecta(entrada)`) y extraer la información disponible (`extrae(entrada)`). + +#### Scenario: Contrato uniforme +- **WHEN** se consulta cualquier proveedor registrado +- **THEN** expone los métodos `detecta(entrada)` y `extrae(entrada)` con la misma firma y semántica, de modo que el consumidor no necesita conocer la clase concreta + +### Requirement: Resultado de extracción extensible +El método `extrae(entrada)` SHALL devolver un objeto de resultado extensible que incluya como mínimo las coordenadas (`lat`, `lng`) y el identificador del proveedor que las extrajo, pudiendo incorporar campos adicionales en el futuro sin romper a los consumidores. + +#### Scenario: Extracción mínima +- **WHEN** un proveedor extrae una ubicación válida +- **THEN** el resultado contiene `lat`, `lng` y el identificador del proveedor + +#### Scenario: Campos adicionales sin ruptura +- **WHEN** en el futuro un proveedor añade nuevos campos al resultado (horarios, teléfono, fotos…) +- **THEN** los consumidores existentes siguen funcionando sin modificación, ignorando los campos que no conocen + +### Requirement: Autodetección del proveedor a partir de la entrada +El sistema SHALL autodetectar automáticamente el proveedor adecuado para una entrada dada, recorriendo los proveedores registrados y usando el primero que la reconozca. + +#### Scenario: Enlace de Google Maps +- **WHEN** el usuario pega un enlace de Google Maps (formatos `@lat,lng`, `!3d…!4d…` o `?q=lat,lng`) +- **THEN** el sistema lo resuelve con el proveedor de Google Maps y obtiene las coordenadas + +#### Scenario: Enlace de Waze +- **WHEN** el usuario pega un enlace de Waze (`waze.com/ul`, parámetro `ll=lat,lng` o búsqueda `q=`) +- **THEN** el sistema lo resuelve con el proveedor de Waze y obtiene las coordenadas + +#### Scenario: Enlace de OpenStreetMap +- **WHEN** el usuario pega un enlace de openstreetmap.org (parámetros `mlat`/`mlon` o fragmento `#map=zoom/lat/lng`) +- **THEN** el sistema lo resuelve con el proveedor de OpenStreetMap y obtiene las coordenadas + +#### Scenario: Coordenadas geográficas sueltas +- **WHEN** el usuario escribe coordenadas directamente (por ejemplo `41.3851, 2.1734`) +- **THEN** el sistema lo resuelve con el proveedor de coordenadas y obtiene lat/lng + +#### Scenario: Texto de dirección como fallback +- **WHEN** la entrada no coincide con ningún enlace soportado y parece una dirección en texto +- **THEN** el sistema la resuelve con el proveedor de geocodificación (Nominatim) realizando una búsqueda por texto + +#### Scenario: Entrada no reconocida +- **WHEN** la entrada no es reconocida por ningún proveedor ni resuelta como dirección +- **THEN** el sistema informa de que no se pudo interpretar la ubicación y no avanza de pantalla + +### Requirement: Inferencia de nombre y provincia +Los proveedores SHALL extraer, cuando estén disponibles, los campos inferibles `nombre` y `provincia` a partir de la entrada resuelta (geocodificación inversa cuando solo hay coordenadas), normalizando la provincia al listado canónico de provincias de España. + +#### Scenario: Inferencia desde coordenadas +- **WHEN** la entrada se resuelve a coordenadas sin más contexto +- **THEN** una geocodificación inversa obtiene un nombre descriptivo y la provincia canónica (coincidencia insensible a acentos y mayúsculas con el listado de PROVINCIAS) + +#### Scenario: Provincia fuera del listado +- **WHEN** la provincia inferida no coincide con ninguna provincia del listado canónico +- **THEN** el campo provincia queda vacío para que el usuario lo seleccione manualmente + +### Requirement: Extensibilidad sin modificar proveedores existentes +Añadir un proveedor nuevo SHALL consistir en crear una nueva clase que implemente la interfaz y registrarla en el resolver, sin modificar las clases existentes ni la lógica de la pasarela (principio abierto/cerrado). + +#### Scenario: Registro de un proveedor nuevo +- **WHEN** se añade una clase `MiProveedor` con `detecta`/`extrae` y se registra en la lista de proveedores +- **THEN** el campo universal empieza a aceptar las entradas que ese proveedor reconoce sin ningún otro cambio de código + +### Requirement: Errores de red degradados +Los proveedores SHALL tratar sus errores de red o de servicio devolviendo un fallo controlado, sin romper la pasarela ni propagar excepciones al consumidor. + +#### Scenario: Fallo del servicio de geocodificación +- **WHEN** Nominatim no responde o devuelve un error +- **THEN** la pantalla de Ubicación muestra un aviso de error y el usuario puede reintentar o continuar sin inferencia diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fb55eae --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4717 @@ +{ + "name": "locales-espanoles", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "locales-espanoles", + "version": "1.0.0", + "dependencies": { + "better-sqlite3": "^12.0.0", + "express": "^5.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", + "@vitejs/plugin-react": "^4.2.1", + "concurrently": "^8.0.0", + "jsdom": "^26.1.0", + "supertest": "^7.2.2", + "vite": "^5.2.0", + "vitest": "^3.2.7" + } + }, + "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", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@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", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "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.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "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", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "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/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", + "integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "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/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/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "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/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.409", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.409.tgz", + "integrity": "sha512-ChI4N44d0B4A6C8prnNjMOaGgE59fUyEVYcRYm2XEXIjMbbvF5i9UL1cblDbpGqiU0uS8FE8UcKxqZqTXdmzbQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.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-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "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/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "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/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "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", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "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-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/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.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/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "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", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "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==", + "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.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "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/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "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-encoding/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/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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..20492ed --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "locales-espanoles", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "server": "node server/index.js", + "dev:all": "concurrently -k -n vite,api -c cyan,green \"npm run dev\" \"npm run server\"", + "test": "vitest run", + "test:watch": "vitest", + "test:integration": "vitest run --config vitest.config.integration.mjs" + }, + "dependencies": { + "better-sqlite3": "^12.0.0", + "express": "^5.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", + "@vitejs/plugin-react": "^4.2.1", + "concurrently": "^8.0.0", + "jsdom": "^26.1.0", + "supertest": "^7.2.2", + "vite": "^5.2.0", + "vitest": "^3.2.7" + } +} diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..6882fd6 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon-32.png b/public/favicon-32.png new file mode 100644 index 0000000..6b171bb Binary files /dev/null and b/public/favicon-32.png differ diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..34fc8e9 Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512-maskable.png b/public/icon-512-maskable.png new file mode 100644 index 0000000..af6fea9 Binary files /dev/null and b/public/icon-512-maskable.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..30da7ef Binary files /dev/null and b/public/icon-512.png differ diff --git a/public/logo-localesp.png b/public/logo-localesp.png new file mode 100644 index 0000000..fb08011 Binary files /dev/null and b/public/logo-localesp.png differ diff --git a/public/logo-localesp@2x.png b/public/logo-localesp@2x.png new file mode 100644 index 0000000..7d2f121 Binary files /dev/null and b/public/logo-localesp@2x.png differ diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..f954994 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,16 @@ +{ + "name": "LocalESP · Locales Españoles", + "short_name": "LocalESP", + "description": "Directorio colaborativo de negocios locales por toda España", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#8B0000", + "lang": "es", + "icons": [ + { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..7fe4d77 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,56 @@ +const CACHE_NAME = "localesp-shell-v2"; +const SHELL_ASSETS = [ + "/", + "/manifest.webmanifest", + "/icon-192.png", + "/icon-512.png", + "/icon-512-maskable.png", + "/apple-touch-icon.png", + "/favicon-32.png", + "/logo-localesp.png", + "/logo-localesp@2x.png", +]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_ASSETS)) + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) + ) + ); + self.clients.claim(); +}); + +// Network-first para la API (los datos deben estar frescos); cache-first +// para el resto del shell estático, para que la app abra offline. +self.addEventListener("fetch", (event) => { + const url = new URL(event.request.url); + + if (event.request.method !== "GET") return; + + if (url.pathname.startsWith("/api/")) { + event.respondWith( + fetch(event.request).catch(() => caches.match(event.request)) + ); + return; + } + + event.respondWith( + caches.match(event.request).then((cached) => { + const network = fetch(event.request) + .then((response) => { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); + return response; + }) + .catch(() => cached); + return cached || network; + }) + ); +}); diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..66336f0 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# deploy.sh — Despliega la rama actual a .localesp.es +# Toda la lógica vive en lib/env.sh (modelo OOP). Éste es sólo el entrypoint. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/env.sh +. "$SCRIPT_DIR/lib/env.sh" + +usage() { + echo "usage: $0 " >&2 + exit 2 +} + +main() { + local branch root + + branch="${1:-${GITHUB_REF_NAME:-}}" + if [ -z "$branch" ]; then + usage + fi + root="$(cd "$SCRIPT_DIR/.." && pwd)" + + Logger::set_tag deploy + Env::new env "$branch" "$root" + Logger::info "$(Env::banner env)" + + Env::deploy env + + Logger::info "LISTO: $(Env::url env) ($(Env::summary env))" +} + +main "$@" diff --git a/scripts/lib/env.sh b/scripts/lib/env.sh new file mode 100644 index 0000000..d0e26eb --- /dev/null +++ b/scripts/lib/env.sh @@ -0,0 +1,450 @@ +#!/usr/bin/env bash +# lib/env.sh — Modelo OOP de entornos de previsualización por rama. +# +# Estrategia (entorno por rama): +# /opt/localesp/ + systemd unit localesp- + vhost nginx + cert Let's Encrypt. +# Reutiliza el puerto de la unit existente (adopta entornos legacy) o asigna uno libre (>=8082). +# +# PRECONDICIÓN MANUAL: el registro A .localesp.es debe existir (se gestiona a mano). +# Si no resuelve aún, la app queda servida por HTTP y se emite el cert cuando el DNS apunte aquí. +# +# Representación OOP en bash: +# - Instancia = array asociativo global, referenciado por NOMBRE. +# - Constructor = Class::new (declara el array y establece invariantes). +# - Método = Class::method ; accede al estado vía `local -n self="$1"`. +# - Composición = Env posee los nombres de sus colaboradores (__unit, __vhost, __cert). +# - Servicios sin estado (Slug, Host, Port, Artifacts, Deps, Logger) = métodos estáticos. +# +# Requiere bash >= 4.3 (namerefs). Sin efectos al sourcear: sólo define funciones/constantes. +# +# CONTRATO (importante): el NOMBRE de instancia pasado a Env::new (p.ej. "env") es el +# nombre de un array asociativo global. NUNCA declares en el caller una `local` con ese +# mismo nombre: bash usa ámbito dinámico y la local sombrearía al array global, rompiendo +# el nameref `local -n self=env`. Pasa el nombre como literal: Env::new env ... + +DOMAIN="localesp.es" +PORT_BASE=8082 # 8080/8081 son entornos legacy +LOGGER_TAG="env" + +# --------------------------------------------------------------------------- +# Logger — servicio singleton (tag configurable según entrypoint). +# --------------------------------------------------------------------------- +Logger::set_tag() { + LOGGER_TAG="$1" +} + +Logger::info() { + printf '\033[1;34m[%s]\033[0m %s\n' "$LOGGER_TAG" "$*" +} + +Logger::error() { + printf '\033[1;31m[%s:ERROR]\033[0m %s\n' "$LOGGER_TAG" "$*" >&2 +} + +# --------------------------------------------------------------------------- +# Slug — value object estático: normaliza un nombre de rama a slug. +# --------------------------------------------------------------------------- +Slug::from() { + local branch="$1" + printf '%s' "$branch" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' +} + +# --------------------------------------------------------------------------- +# Host — servicio estático: IP pública y resolución DNS. +# --------------------------------------------------------------------------- +Host::public_ip() { + curl -s4 --max-time 5 ifconfig.me || true +} + +Host::resolved_ip() { + local host="$1" + getent hosts "$host" | awk '{print $1}' | head -1 || true +} + +# --------------------------------------------------------------------------- +# Port — servicio estático: asigna/reutiliza puerto para una unit+dest. +# --------------------------------------------------------------------------- +Port::acquire() { + local unit="$1" + local dest="$2" + local unit_file="/etc/systemd/system/$unit.service" + local used + local port + + # Adopta entorno existente: reutiliza el puerto de la unit ya instalada. + if [ -f "$unit_file" ]; then + port="$(grep -oE 'PORT=[0-9]+' "$unit_file" | head -1 | cut -d= -f2 || true)" + if [ -n "$port" ]; then + echo "$port" + return + fi + fi + if [ -f "$dest/.port" ]; then + cat "$dest/.port" + return + fi + + used="$( { grep -rhoE 'PORT=[0-9]+' /etc/systemd/system/localesp*.service 2>/dev/null | cut -d= -f2 + grep -rhoE 'localhost:[0-9]+' /etc/nginx/conf.d/*.conf 2>/dev/null | cut -d: -f2; } | sort -un)" + port="$PORT_BASE" + while printf '%s\n' "$used" | grep -qx "$port"; do + port=$((port+1)) + done + echo "$port" +} + +# --------------------------------------------------------------------------- +# Artifacts — servicio estático: artefactos buildados. +# --------------------------------------------------------------------------- +Artifacts::require_built() { + local root="$1" + + if [ ! -d "$root/dist" ]; then + Logger::error "falta dist/ — ¿se ejecutó el build?" + exit 1 + fi +} + +Artifacts::sync() { + local root="$1" + local dest="$2" + + mkdir -p "$dest/dist" "$dest/server" + rsync -a --delete "$root/dist/" "$dest/dist/" + rsync -a --delete "$root/server/" "$dest/server/" + cp -a "$root/package.json" "$root/package-lock.json" "$dest/" + # localesp.db se PRESERVA (datos de usuarios). No se toca aquí. +} + +# --------------------------------------------------------------------------- +# Deps — servicio estático: dependencias de producción. +# --------------------------------------------------------------------------- +Deps::install() { + local root="$1" + local dest="$2" + + # Reutiliza los node_modules ya compilados por el workflow (better-sqlite3 v12 exige + # C++20, GCC 12). Copiar + npm prune evita recompilar en cada despliegue. + # Fallback a npm ci --omit=dev si se ejecuta a mano sin build previo. + if [ -d "$root/node_modules" ]; then + mkdir -p "$dest/node_modules" + rsync -a --delete "$root/node_modules/" "$dest/node_modules/" + ( cd "$dest" && npm prune --omit=dev --no-audit --no-fund ) + else + ( cd "$dest" && npm ci --omit=dev --no-audit --no-fund ) + fi +} + +# --------------------------------------------------------------------------- +# SystemdUnit — entidad: fichero .service de un entorno. +# --------------------------------------------------------------------------- +SystemdUnit::new() { + local self="$1" + local name="$2" + local dest="$3" + local branch="$4" + local host="$5" + local port="$6" + + declare -gA "$self" + local -n u="$self" + u[name]="$name" + u[dest]="$dest" + u[branch]="$branch" + u[host]="$host" + u[port]="$port" + u[file]="/etc/systemd/system/${name}.service" +} + +SystemdUnit::render() { + local self="$1" + local -n u="$self" + + cat < "${u[file]}" + echo "${u[port]}" > "${u[dest]}/.port" + systemctl daemon-reload + systemctl enable "${u[name]}" >/dev/null 2>&1 || true + systemctl restart "${u[name]}" + Logger::info "${u[name]} arrancada" +} + +SystemdUnit::remove() { + local self="$1" + local -n u="$self" + + systemctl disable --now "${u[name]}" 2>/dev/null || true + rm -f "${u[file]}" + systemctl daemon-reload +} + +# --------------------------------------------------------------------------- +# NginxVhost — entidad: vhost nginx de un entorno. +# --------------------------------------------------------------------------- +NginxVhost::new() { + local self="$1" + local slug="$2" + local host="$3" + local port="$4" + + declare -gA "$self" + local -n v="$self" + v[slug]="$slug" + v[host]="$host" + v[port]="$port" +} + +NginxVhost::find_path() { + local self="$1" + local -n v="$self" + local host_re + host_re="$(printf '%s' "${v[host]}" | sed 's/\./\\./g')" + grep -rlE "server_name[[:space:]]+$host_re[[:space:]]*;" /etc/nginx/conf.d/*.conf 2>/dev/null | head -1 || true +} + +NginxVhost::render() { + local self="$1" + local -n v="$self" + + cat < "$path" +} + +NginxVhost::repoint() { + local self="$1" + local -n v="$self" + local path="$2" + + Logger::info "vhost existente: $path — forzando proxy_pass -> :${v[port]}" + sed -i -E "s|proxy_pass http://localhost:[0-9]+;|proxy_pass http://localhost:${v[port]};|g" "$path" +} + +NginxVhost::configure() { + local self="$1" + local existing + + existing="$(NginxVhost::find_path "$self")" + if [ -z "$existing" ]; then + NginxVhost::create "$self" + else + NginxVhost::repoint "$self" "$existing" + fi + nginx -t 2>&1 | tail -2 + systemctl reload nginx + Logger::info "nginx recargado" +} + +NginxVhost::remove() { + local self="$1" + local path + + path="$(NginxVhost::find_path "$self")" + if [ -n "$path" ]; then + Logger::info "borrando vhost $path" + rm -f "$path" + nginx -t && systemctl reload nginx + fi +} + +# --------------------------------------------------------------------------- +# TlsCert — entidad: certificado Let's Encrypt de un entorno. +# --------------------------------------------------------------------------- +TlsCert::new() { + local self="$1" + local host="$2" + + declare -gA "$self" + local -n c="$self" + c[host]="$host" +} + +TlsCert::exists() { + local self="$1" + local -n c="$self" + [ -d "/etc/letsencrypt/live/${c[host]}" ] +} + +TlsCert::ensure() { + local self="$1" + local -n c="$self" + local host="${c[host]}" + local pub_ip resolved + + if TlsCert::exists "$self"; then + Logger::info "cert TLS ya presente para $host" + return + fi + + Logger::info "comprobando DNS de $host" + pub_ip="$(Host::public_ip)" + resolved="$(Host::resolved_ip "$host")" + + if [ -n "$pub_ip" ] && [ "$resolved" = "$pub_ip" ]; then + Logger::info "emitiendo cert con certbot --nginx" + if certbot --nginx -d "$host" -n --redirect --keep-until-expiring; then + Logger::info "cert emitido ✓" + else + Logger::error "certbot falló; $host sigue en HTTP. Revisa y vuelve a lanzar el workflow." + fi + else + Logger::error "DNS de $host -> '${resolved:-}', esperado $pub_ip." + Logger::error "Crea el registro A $host -> $pub_ip y, tras propagar, re-lanza el workflow" + Logger::error "(o ejecuta: certbot --nginx -d $host). La app ya vive en http://$host" + fi +} + +TlsCert::delete() { + local self="$1" + local -n c="$self" + local host="${c[host]}" + + if TlsCert::exists "$self"; then + Logger::info "borrando cert $host" + certbot delete --cert-name "$host" -n 2>/dev/null || true + fi +} + +TlsCert::protocol() { + local self="$1" + + if TlsCert::exists "$self"; then + echo "https" + else + echo "http" + fi +} + +# --------------------------------------------------------------------------- +# Env — aggregate root: un entorno de rama. Compone SystemdUnit, NginxVhost, TlsCert. +# --------------------------------------------------------------------------- +Env::new() { + local self="$1" + local branch="$2" + local root="$3" + + declare -gA "$self" + local -n s="$self" + + s[branch]="$branch" + s[root]="$root" + s[slug]="$(Slug::from "$branch")" + if [ -z "${s[slug]}" ]; then + Logger::error "slug vacío para la rama '$branch'" + exit 2 + fi + s[host]="${s[slug]}.$DOMAIN" + s[dest]="/opt/localesp/${s[slug]}" + s[unit_name]="localesp-${s[slug]}" + s[port]="$(Port::acquire "${s[unit_name]}" "${s[dest]}")" + + # Composición: crea los colaboradores y guarda sus nombres en el agregado. + SystemdUnit::new "${self}__unit" "${s[unit_name]}" "${s[dest]}" "$branch" "${s[host]}" "${s[port]}" + NginxVhost::new "${self}__vhost" "${s[slug]}" "${s[host]}" "${s[port]}" + TlsCert::new "${self}__cert" "${s[host]}" +} + +Env::deploy() { + local self="$1" + local -n s="$self" + + Artifacts::require_built "${s[root]}" + Logger::info "sincronizando artefactos -> ${s[dest]}" + Artifacts::sync "${s[root]}" "${s[dest]}" + + Logger::info "instalando dependencias (prod)" + Deps::install "${s[root]}" "${s[dest]}" + + SystemdUnit::write "${self}__unit" + + Logger::info "configurando nginx para ${s[host]}" + NginxVhost::configure "${self}__vhost" + + TlsCert::ensure "${self}__cert" +} + +Env::teardown() { + local self="$1" + local -n s="$self" + + SystemdUnit::remove "${self}__unit" + TlsCert::delete "${self}__cert" + NginxVhost::remove "${self}__vhost" + rm -rf "${s[dest]}" +} + +Env::url() { + local self="$1" + local -n s="$self" + local proto + + proto="$(TlsCert::protocol "${self}__cert")" + echo "$proto://${s[host]}" +} + +Env::banner() { + local self="$1" + local -n s="$self" + + echo "rama=${s[branch]} slug=${s[slug]} host=${s[host]} dest=${s[dest]} puerto=${s[port]}" +} + +Env::identity() { + local self="$1" + local -n s="$self" + + echo "${s[host]} (${s[dest]}, ${s[unit_name]})" +} + +Env::summary() { + local self="$1" + local -n s="$self" + + echo "rama=${s[branch]} unit=${s[unit_name]} puerto=${s[port]}" +} diff --git a/scripts/teardown.sh b/scripts/teardown.sh new file mode 100755 index 0000000..b06e14b --- /dev/null +++ b/scripts/teardown.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# teardown.sh — Elimina el entorno de una rama (al borrar la rama o a mano). +# Toda la lógica vive en lib/env.sh (modelo OOP). Éste es sólo el entrypoint. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/env.sh +. "$SCRIPT_DIR/lib/env.sh" + +usage() { + echo "usage: $0 " >&2 + exit 2 +} + +main() { + local branch root + + branch="${1:-}" + if [ -z "$branch" ]; then + usage + fi + root="$(cd "$SCRIPT_DIR/.." && pwd)" + + Logger::set_tag teardown + Env::new env "$branch" "$root" + Logger::info "$(Env::identity env)" + + Env::teardown env + + Logger::info "entorno eliminado ✓" +} + +main "$@" diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..8eab8c8 --- /dev/null +++ b/server/db.js @@ -0,0 +1,89 @@ +import Database from "better-sqlite3"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +// Ruta de la BD sobreescribible para pruebas (admite ":memory:"); en +// producción se usa el archivo del repo. +const dbPath = process.env.LOCALESP_DB || join(__dirname, "../localesp.db"); +const db = new Database(dbPath); + +db.exec(` + CREATE TABLE IF NOT EXISTS locales ( + id TEXT PRIMARY KEY, + nombre TEXT NOT NULL, + descripcion TEXT, + provincia TEXT, + subcategoria TEXT, + direccion TEXT, + enlaceGoogleMaps TEXT, + puntuacion INTEGER DEFAULT 0, + categoria TEXT, + fecha TEXT DEFAULT CURRENT_TIMESTAMP, + lat REAL, + lng REAL + ); + + CREATE TABLE IF NOT EXISTS pendientes ( + id TEXT PRIMARY KEY, + nombre TEXT NOT NULL, + descripcion TEXT, + provincia TEXT, + subcategoria TEXT, + direccion TEXT, + enlaceGoogleMaps TEXT, + puntuacion INTEGER DEFAULT 0, + categoria TEXT, + estado TEXT DEFAULT 'pendiente', + fechaPropuesta TEXT DEFAULT CURRENT_TIMESTAMP, + lat REAL, + lng REAL + ); + + CREATE TABLE IF NOT EXISTS config ( + id TEXT PRIMARY KEY, + datos TEXT + ); + + CREATE TABLE IF NOT EXISTS reportes ( + id TEXT PRIMARY KEY, + localId TEXT NOT NULL, + motivo TEXT, + comentario TEXT, + fecha TEXT DEFAULT CURRENT_TIMESTAMP, + estado TEXT DEFAULT 'pendiente' + ); + + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + created TEXT DEFAULT CURRENT_TIMESTAMP + ); +`); + +// Migración suave: si la base de datos ya existía con el esquema antiguo +// (sin provincia/subcategoria/direccion/enlaceGoogleMaps/puntuacion), +// añadimos las columnas que falten sin borrar nada, porque +// CREATE TABLE IF NOT EXISTS no modifica tablas ya creadas. +function ensureColumn(table, column, definition) { + const cols = db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name); + if (!cols.includes(column)) { + db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`); + } +} + +for (const table of ["locales", "pendientes"]) { + ensureColumn(table, "provincia", "TEXT"); + ensureColumn(table, "subcategoria", "TEXT"); + ensureColumn(table, "direccion", "TEXT"); + ensureColumn(table, "enlaceGoogleMaps", "TEXT"); + ensureColumn(table, "puntuacion", "INTEGER DEFAULT 0"); +} +// fechaConfirmacion: última vez que alguien confirmó que el local sigue activo. +// Al aprobarse por primera vez se inicializa igual a "fecha"; luego se actualiza +// con /api/locales/:id/confirmar cada vez que un usuario pulsa "Sigue aquí". +ensureColumn("locales", "fechaConfirmacion", "TEXT"); +db.prepare("UPDATE locales SET fechaConfirmacion = fecha WHERE fechaConfirmacion IS NULL").run(); + +export default db; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..10c7f6a --- /dev/null +++ b/server/index.js @@ -0,0 +1,180 @@ +import express from "express"; +import { fileURLToPath, pathToFileURL } from "url"; +import { dirname, join } from "path"; +import db from "./db.js"; +import { randomUUID } from "crypto"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const app = express(); + +app.use(express.json()); +app.use(express.static(join(__dirname, "../dist"))); + +// Locales (público, solo lectura) +app.get("/api/locales", (_req, res) => { + const locales = db.prepare("SELECT * FROM locales ORDER BY fecha DESC").all(); + res.json(locales); +}); + +// Pendientes (admin) +app.get("/api/pendientes", (_req, res) => { + const pendientes = db.prepare("SELECT * FROM pendientes ORDER BY fechaPropuesta DESC").all(); + res.json(pendientes); +}); + +// Utilidades para detectar duplicados +function normaliza(s) { + return (s || "").toString().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); +} +function distanciaMetros(lat1, lng1, lat2, lng2) { + const R = 6371000; + const toRad = (d) => (d * Math.PI) / 180; + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +} + +// Comprueba si ya existe un local con el mismo nombre (en la misma provincia o muy +// cerca geográficamente) entre los ya publicados y los pendientes de revisión. +app.post("/api/check-duplicado", (req, res) => { + const { nombre, provincia, lat, lng } = req.body; + const nombreN = normaliza(nombre); + if (!nombreN) return res.json({ duplicado: false, coincidencias: [] }); + + const candidatos = [ + ...db.prepare("SELECT id, nombre, provincia, direccion, categoria, lat, lng FROM locales").all().map((l) => ({ ...l, estado: "publicado" })), + ...db.prepare("SELECT id, nombre, provincia, direccion, categoria, lat, lng FROM pendientes").all().map((l) => ({ ...l, estado: "pendiente" })), + ]; + + const coincidencias = candidatos.filter((l) => { + if (normaliza(l.nombre) !== nombreN) return false; + const mismaProvincia = provincia && normaliza(l.provincia) === normaliza(provincia); + const cerca = lat && lng && l.lat && l.lng && distanciaMetros(lat, lng, l.lat, l.lng) < 200; + return mismaProvincia || cerca; + }); + + res.json({ duplicado: coincidencias.length > 0, coincidencias }); +}); + +// Enviar propuesta +app.post("/api/propuestas", (req, res) => { + const { nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion, categoria, lat, lng } = req.body; + const id = randomUUID(); + const fechaPropuesta = new Date().toISOString(); + + db.prepare(` + INSERT INTO pendientes (id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion, categoria, estado, fechaPropuesta, lat, lng) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pendiente', ?, ?, ?) + `).run(id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion ?? 0, categoria, fechaPropuesta, lat, lng); + + res.status(201).json({ id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion: puntuacion ?? 0, categoria, estado: "pendiente", fechaPropuesta, lat, lng }); +}); + +// Aprobar propuesta +app.post("/api/aprobar/:id", (req, res) => { + const { id } = req.params; + const propuesta = db.prepare("SELECT * FROM pendientes WHERE id = ?").get(id); + + if (!propuesta) return res.status(404).json({ error: "Propuesta no encontrada" }); + + const localeId = randomUUID(); + const fecha = new Date().toISOString(); + + db.prepare(` + INSERT INTO locales (id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion, categoria, fecha, lat, lng, fechaConfirmacion) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run(localeId, propuesta.nombre, propuesta.descripcion, propuesta.provincia, propuesta.subcategoria, propuesta.direccion, propuesta.enlaceGoogleMaps, propuesta.puntuacion ?? 0, propuesta.categoria, fecha, propuesta.lat, propuesta.lng, fecha); + + db.prepare("DELETE FROM pendientes WHERE id = ?").run(id); + + res.json({ ok: true }); +}); + +// Rechazar propuesta +app.post("/api/rechazar/:id", (req, res) => { + const { id } = req.params; + db.prepare("DELETE FROM pendientes WHERE id = ?").run(id); + res.json({ ok: true }); +}); + +// Eliminar local +app.delete("/api/locales/:id", (req, res) => { + const { id } = req.params; + db.prepare("DELETE FROM locales WHERE id = ?").run(id); + db.prepare("DELETE FROM reportes WHERE localId = ?").run(id); + res.json({ ok: true }); +}); + +// Confirmar que un local sigue activo (renueva fechaConfirmacion) +app.post("/api/locales/:id/confirmar", (req, res) => { + const { id } = req.params; + const local = db.prepare("SELECT id FROM locales WHERE id = ?").get(id); + if (!local) return res.status(404).json({ error: "Local no encontrado" }); + const ahora = new Date().toISOString(); + db.prepare("UPDATE locales SET fechaConfirmacion = ? WHERE id = ?").run(ahora, id); + res.json({ ok: true, fechaConfirmacion: ahora }); +}); + +// Reportar un local (cerrado / datos incorrectos / inapropiado / otro) +app.post("/api/locales/:id/reportar", (req, res) => { + const { id } = req.params; + const { motivo, comentario } = req.body; + const local = db.prepare("SELECT id FROM locales WHERE id = ?").get(id); + if (!local) return res.status(404).json({ error: "Local no encontrado" }); + const reporteId = randomUUID(); + db.prepare(` + INSERT INTO reportes (id, localId, motivo, comentario, fecha, estado) + VALUES (?, ?, ?, ?, ?, 'pendiente') + `).run(reporteId, id, motivo || "otro", comentario || "", new Date().toISOString()); + res.status(201).json({ ok: true, id: reporteId }); +}); + +// Reportes (admin) +app.get("/api/reportes", (_req, res) => { + const reportes = db.prepare(` + SELECT r.*, l.nombre AS localNombre, l.provincia AS localProvincia, l.direccion AS localDireccion + FROM reportes r LEFT JOIN locales l ON l.id = r.localId + WHERE r.estado = 'pendiente' + ORDER BY r.fecha DESC + `).all(); + res.json(reportes); +}); + +// Descartar un reporte (el admin revisó y no hace falta actuar) +app.post("/api/reportes/:id/descartar", (req, res) => { + const { id } = req.params; + db.prepare("UPDATE reportes SET estado = 'descartado' WHERE id = ?").run(id); + res.json({ ok: true }); +}); + +// Config (palabras filtradas) +app.get("/api/config/filtro_palabras", (_req, res) => { + const config = db.prepare("SELECT datos FROM config WHERE id = ?").get("filtro_palabras"); + if (config) { + res.json({ palabras: JSON.parse(config.datos) }); + } else { + res.json({ palabras: [] }); + } +}); + +app.post("/api/config/filtro_palabras", (req, res) => { + const { palabras } = req.body; + db.prepare("INSERT OR REPLACE INTO config (id, datos) VALUES (?, ?)").run("filtro_palabras", JSON.stringify(palabras)); + res.json({ ok: true }); +}); + +// SPA fallback +app.get(/^\/(?!api\/).*/, (_req, res) => { + res.sendFile(join(__dirname, "../dist/index.html")); +}); + +// Escucha solo cuando se ejecuta como script (`node server/index.js`); al ser +// importado (p. ej. por las pruebas con supertest) se exporta la app sin puerto. +const PORT = process.env.PORT || 3000; +const esPuntoDeEntrada = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; +if (esPuntoDeEntrada) { + app.listen(PORT, () => console.log(`Server listening on http://localhost:${PORT}`)); +} + +export default app; diff --git a/src/AdminPanel.jsx b/src/AdminPanel.jsx new file mode 100644 index 0000000..eb86ca6 --- /dev/null +++ b/src/AdminPanel.jsx @@ -0,0 +1,351 @@ +// AdminPanel.jsx — /admin +// No aparece en ningún menú público. Acceso solo por URL directa. +import { useState, useEffect } from "react"; +import { + escucharAuth, loginAdmin, registrarAdmin, cerrarSesion, + suscribirPendientes, aprobarPropuesta, rechazarPropuesta, + suscribirLocales, deleteLocal, + obtenerReportes, descartarReporte, + obtenerPalabrasFiltradas, guardarPalabrasFiltradas, +} from "./firebase.js"; +import { obtenerCategoria } from "./constants/categorias.js"; +import { enlaceGoogleMapsDesdeLocal, enlaceWazeDesdeLocal } from "./utils/geo.js"; +import StarRating from "./components/StarRating.jsx"; +import Toast from "./components/Toast.jsx"; +import useToast from "./hooks/useToast.js"; + +const CLAVE_TOAST_LOGIN = "localesp_mostrar_toast_login"; + +const S = { + page: { minHeight:"100vh", background:"#F2EDE8", fontFamily:"-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" }, + header: { background:"#1A1A1A", color:"white", padding:"14px 28px", display:"flex", alignItems:"center", justifyContent:"space-between", position:"sticky", top:0, zIndex:10 }, + wrap: { maxWidth:920, margin:"0 auto", padding:"28px 20px 60px" }, + card: { background:"white", borderRadius:12, border:"1px solid #E8E0D5", padding:"20px 24px", marginBottom:14 }, + input: { width:"100%", boxSizing:"border-box", padding:"10px 13px", border:"1px solid #DDD", borderRadius:8, fontSize:14, fontFamily:"inherit", outline:"none" }, + btn: (bg="#8B0000",fg="white") => ({ padding:"9px 20px", background:bg, color:fg, border:"none", borderRadius:8, cursor:"pointer", fontWeight:600, fontSize:14, fontFamily:"inherit" }), + tab: (a) => ({ padding:"11px 22px", border:"none", background:"none", cursor:"pointer", fontFamily:"inherit", fontSize:14, fontWeight:a?700:400, color:a?"#8B0000":"#888", borderBottom:a?"2px solid #8B0000":"2px solid transparent", marginBottom:-2 }), + badge: (c,bg) => ({ display:"inline-block", padding:"3px 10px", borderRadius:20, fontSize:11, fontWeight:600, background:bg, color:c }), + tag: { display:"inline-flex", alignItems:"center", gap:5, background:"#F0EBE5", borderRadius:20, padding:"4px 12px", fontSize:13 }, + label: { fontSize:11, color:"#999", fontWeight:700, textTransform:"uppercase", letterSpacing:"0.5px", display:"block", marginBottom:5 }, +}; + +// ── Pantalla de login/registro ──────────────────────────────────────────────── +function PantallaAuth() { + const [modo, setModo] = useState("login"); + const [email, setEmail] = useState(""); + const [pass, setPass] = useState(""); + const [pass2, setPass2] = useState(""); + const [err, setErr] = useState(""); + const [busy, setBusy] = useState(false); + + const errMsg = code => ({ "auth/wrong-password":"Contraseña incorrecta.", "auth/user-not-found":"No existe ningún admin con ese email.", "auth/invalid-credential":"Email o contraseña incorrectos.", "auth/email-already-in-use":"Ese email ya está registrado.", "auth/weak-password":"Mínimo 6 caracteres.", "auth/invalid-email":"Email no válido.", "auth/too-many-requests":"Demasiados intentos. Espera un momento." }[code] || "Error inesperado."); + + const submit = async () => { + setErr(""); + if (!email || !pass) { setErr("Rellena todos los campos."); return; } + if (modo === "registro" && pass !== pass2) { setErr("Las contraseñas no coinciden."); return; } + setBusy(true); + try { + modo === "login" ? await loginAdmin(email, pass) : await registrarAdmin(email, pass); + try { sessionStorage.setItem(CLAVE_TOAST_LOGIN, modo === "login" ? "1" : "0"); } catch { /* sessionStorage no disponible */ } + } + catch(e) { setErr(errMsg(e.code)); } + setBusy(false); + }; + + return ( +
+
+
+
🔐
+

Panel de Administración

+

Locales Españoles · acceso restringido

+
+
+
+ {[{id:"login",l:"Iniciar sesión"},{id:"registro",l:"Crear admin"}].map(t => ( + + ))} +
+
+
setEmail(e.target.value)} onKeyDown={e=>e.key==="Enter"&&submit()} />
+
setPass(e.target.value)} onKeyDown={e=>e.key==="Enter"&&submit()} />
+ {modo==="registro" &&
setPass2(e.target.value)} onKeyDown={e=>e.key==="Enter"&&submit()} />
} + {err &&

{err}

} + +
+
+

Esta página no está indexada en el sitio público

+
+
+ ); +} + +// ── Tarjeta propuesta pendiente ─────────────────────────────────────────────── +function TarjetaPropuesta({ p, onAprobar, onRechazar, busy }) { + const cat = obtenerCategoria(p.categoria); + const fecha = new Date(p.fechaPropuesta).toLocaleDateString("es-ES", { day:"numeric", month:"short", hour:"2-digit", minute:"2-digit" }); + const gmUrl = p.enlaceGoogleMaps || enlaceGoogleMapsDesdeLocal(p); + const wazeUrl = enlaceWazeDesdeLocal(p); + return ( +
+
+
+
+ {p.nombre} + {p.categoria && {cat.emoji} {p.categoria}} + {p.subcategoria && {p.subcategoria}} +
+
+ 📍 {p.provincia} + {p.direccion && (gmUrl ? 🗺 {p.direccion} : 🗺 {p.direccion})} + + Enviado: {fecha} +
+ {p.descripcion &&

"{p.descripcion}"

} +
+ {gmUrl && Ver en Google Maps ↗} + {wazeUrl && Ver en Waze ↗} +
+
+
+ + +
+
+
+ ); +} + +// ── Panel principal ─────────────────────────────────────────────────────────── +function PanelAdmin({ email }) { + const [tab, setTab] = useState("pendientes"); + const [pendientes, setPendientes] = useState([]); + const [publicados, setPublicados] = useState([]); + const [reportes, setReportes] = useState([]); + const [palabras, setPalabras] = useState([]); + const [nuevaP, setNuevaP] = useState(""); + const [cargando, setCargando] = useState(true); + const [busy, setBusy] = useState(false); + const [guardando, setGuardando] = useState(false); + const [buscador, setBuscador] = useState(""); + const { toast, mostrarToast } = useToast(); + + const cargarReportes = () => obtenerReportes().then(setReportes).catch(() => {}); + + useEffect(() => { + try { + const marca = sessionStorage.getItem(CLAVE_TOAST_LOGIN); + if (marca !== null) { + mostrarToast(marca === "1" ? `Sesión iniciada como ${email}` : "Cuenta de administrador creada", "exito"); + sessionStorage.removeItem(CLAVE_TOAST_LOGIN); + } + } catch { /* sessionStorage no disponible */ } + }, []); + + useEffect(() => { + setCargando(true); + let cancelado = false; + let limpiarPendientes, limpiarPublicados; + suscribirPendientes(l => { if (!cancelado) { setPendientes(l); setCargando(false); } }, () => !cancelado && setCargando(false)) + .then(fn => { if (cancelado) fn?.(); else limpiarPendientes = fn; }); + suscribirLocales(l => !cancelado && setPublicados(l), () => {}) + .then(fn => { if (cancelado) fn?.(); else limpiarPublicados = fn; }); + obtenerPalabrasFiltradas().then(p => !cancelado && setPalabras(p)); + cargarReportes(); + return () => { cancelado = true; limpiarPendientes?.(); limpiarPublicados?.(); }; + }, []); + + const aprobar = async (p) => { setBusy(true); try { await aprobarPropuesta(p); mostrarToast(`✅ "${p.nombre}" aprobado y publicado`, "exito"); } catch(e) { mostrarToast("Error: "+e.message, "error"); } setBusy(false); }; + const rechazar = async (id) => { if (!confirm("¿Rechazar y eliminar esta propuesta?")) return; try { await rechazarPropuesta(id); mostrarToast("Propuesta rechazada", "info"); } catch(e) { mostrarToast("Error: "+e.message, "error"); } }; + const eliminar = async (id) => { if (!confirm("¿Eliminar este local publicado?")) return; try { await deleteLocal(id); cargarReportes(); mostrarToast("Local eliminado", "info"); } catch(e) { mostrarToast("Error: "+e.message, "error"); } }; + const descartar = async (id) => { try { await descartarReporte(id); setReportes(r => r.filter(x => x.id !== id)); mostrarToast("Aviso descartado", "info"); } catch(e) { mostrarToast("Error: "+e.message, "error"); } }; + + const addPalabra = () => { + const p = nuevaP.trim().toLowerCase(); + if (!p || palabras.includes(p)) return; + setPalabras([...palabras, p]); setNuevaP(""); + }; + const guardarFiltro = async () => { + setGuardando(true); + try { await guardarPalabrasFiltradas(palabras); mostrarToast("💾 Filtro de palabras guardado", "exito"); } + catch(e) { mostrarToast("Error: "+e.message, "error"); } + setGuardando(false); + }; + + const pubFiltrados = publicados.filter(l => + !buscador || l.nombre.toLowerCase().includes(buscador.toLowerCase()) || l.provincia?.toLowerCase().includes(buscador.toLowerCase()) + ); + + const TABS = [ + { id:"pendientes", l:`Pendientes (${pendientes.length})` }, + { id:"publicados", l:`Publicados (${publicados.length})` }, + { id:"reportes", l:`Reportes${reportes.length ? ` (${reportes.length})` : ""}` }, + { id:"filtro", l:"Filtro de palabras" }, + ]; + + return ( +
+
+
+ 🛡️ +
+

Panel de Administración

+

Locales Españoles · {email}

+
+
+ +
+ +
+ {/* Tabs */} +
+ {TABS.map(t => )} +
+ + {/* ── Pendientes ── */} + {tab === "pendientes" && ( + cargando ?

Cargando propuestas...

+ : pendientes.length === 0 ? ( +
+
🎉
+

Sin propuestas pendientes

+

Cuando un usuario proponga un local, aparecerá aquí para que lo apruebes o rechaces.

+
+ ) : ( + <> +

+ {pendientes.length} propuesta{pendientes.length!==1?"s":""} esperando revisión. +

+ {pendientes.map(p => )} + + ) + )} + + {/* ── Publicados ── */} + {tab === "publicados" && ( + <> +
+ setBuscador(e.target.value)} /> +
+ {pubFiltrados.length === 0 + ?

{publicados.length===0 ? "No hay locales publicados." : "Sin resultados."}

+ : pubFiltrados.map(l => ( +
+
+
+
+ {l.nombre} + {l.categoria && {obtenerCategoria(l.categoria).emoji} {l.categoria}} + 📍 {l.provincia} · {new Date(l.fecha).toLocaleDateString("es-ES",{day:"numeric",month:"short",year:"numeric"})} +
+ {l.direccion && (() => { + const url = l.enlaceGoogleMaps || enlaceGoogleMapsDesdeLocal(l); + return url + ? {l.direccion} + :

{l.direccion}

; + })()} +
+ +
+
+ )) + } + + )} + + {/* ── Reportes ── */} + {tab === "reportes" && ( + reportes.length === 0 ? ( +
+
🚩
+

Sin reportes pendientes

+

Cuando alguien reporte un local (cerrado, datos incorrectos...), aparecerá aquí.

+
+ ) : ( + <> +

{reportes.length} reporte{reportes.length!==1?"s":""} sin revisar.

+ {reportes.map(r => ( +
+
+
+
+ {r.localNombre || "(local ya eliminado)"} + {{ cerrado:"Ha cerrado", datos_incorrectos:"Datos incorrectos", inapropiado:"Contenido inapropiado", otro:"Otro motivo" }[r.motivo] || r.motivo} +
+

📍 {r.localProvincia} {r.localDireccion ? `· ${r.localDireccion}` : ""}

+ {r.comentario &&

"{r.comentario}"

} +

Reportado: {new Date(r.fecha).toLocaleDateString("es-ES", { day:"numeric", month:"short", hour:"2-digit", minute:"2-digit" })}

+
+
+ {r.localId && } + +
+
+
+ ))} + + ) + )} + + {/* ── Filtro de palabras ── */} + {tab === "filtro" && ( +
+
+

Palabras prohibidas

+

+ Las propuestas que contengan estas palabras en nombre, dirección o descripción serán bloqueadas automáticamente, sin llegar siquiera al panel de moderación. No distingue mayúsculas ni tildes. +

+
+ setNuevaP(e.target.value)} onKeyDown={e => e.key==="Enter" && addPalabra()} /> + +
+ {palabras.length === 0 + ?

No hay palabras filtradas todavía.

+ :
+ {palabras.map(p => ( + + {p} + + + ))} +
+ } + +
+
+

ℹ️ Cómo funciona el flujo completo

+
    +
  1. El usuario rellena el formulario público y pulsa "Enviar propuesta".
  2. +
  3. Antes de guardar, se comprueba el filtro de palabras. Si hay coincidencia → se rechaza al instante con un aviso.
  4. +
  5. Si pasa el filtro → se guarda en /pendientes en Firestore.
  6. +
  7. Aquí en el panel aparece la tarjeta. Tú pulsas Aprobar → pasa a /locales y se publica. O Rechazar → se elimina sin publicar.
  8. +
  9. Solo los locales en /locales son visibles en la web y la app móvil.
  10. +
+
+
+ )} +
+ +
+ ); +} + +// ── Raíz del panel ──────────────────────────────────────────────────────────── +export default function AdminPanel() { + const [user, setUser] = useState(undefined); + useEffect(() => escucharAuth(u => setUser(u)), []); + if (user === undefined) return

Cargando...

; + if (!user) return ; + return ; +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..96caf4d --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,454 @@ +import { useEffect, useRef, useState } from "react"; +import { comprobarDuplicado, contienepalabrasProhibidas, enviarPropuesta, obtenerPalabrasFiltradas, suscribirLocales } from "./firebase.js"; +import { CATEGORIAS, COLORES_MAPA, NOMBRES_CATEGORIAS, obtenerCategoria } from "./constants/categorias.js"; +import { COORDS_PROVINCIAS, PROVINCIAS, CENTRO_ESPANA } from "./constants/provincias.js"; +import { distanciaKm as calcularDistanciaKm, enlaceGoogleMapsDesdeLocal, enlaceWazeDesdeLocal } from "./utils/geo.js"; +import { inputStyle, ANCHO_CUERPO } from "./styles/shared.js"; +import LocalCard from "./components/LocalCard.jsx"; +import BannerMovil from "./components/BannerMovil.jsx"; +import ModalPrivacidad from "./components/ModalPrivacidad.jsx"; +import BotonFlotante from "./components/BotonFlotante.jsx"; +import BotonProponerArriba from "./components/BotonProponerArriba.jsx"; +import ModalPasarela from "./components/ModalPasarela.jsx"; +import useFavoritos from "./hooks/useFavoritos.js"; +import useUbicacion from "./hooks/useUbicacion.js"; + +const FORM_VACIO = { + nombre: "", provincia: "", categoria: "", subcategoria: "", + direccion: "", enlaceGoogleMaps: "", lat: null, lng: null, + entradaUbicacion: "", proveedorUbicacion: "", // estado interno del paso de ubicación, no se envía +}; + +// ── App principal ───────────────────────────────────────────────────────────── +export default function App() { + const [locales, setLocales] = useState([]); + const [vista, setVista] = useState("lista"); + const [pasarelaAbierta, setPasarelaAbierta] = useState(false); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [enviado, setEnviado] = useState(false); // confirmación de propuesta enviada + const [errorFiltro, setErrorFiltro] = useState(""); // palabra prohibida detectada + const [busqueda, setBusqueda] = useState(""); + const [filtroCategoria, setFiltroCategoria] = useState(""); + const [filtroSubcat, setFiltroSubcat] = useState(""); + const [filtroProvincia, setFiltroProvincia] = useState(""); + const [orden, setOrden] = useState("fecha"); + const [palabrasProhibidas, setPalabrasProhibidas] = useState([]); + const [comprobandoDuplicado, setComprobandoDuplicado] = useState(false); + const [posibleDuplicado, setPosibleDuplicado] = useState(null); // { coincidencias, lat, lng } + const [soloFavoritos, setSoloFavoritos] = useState(false); + const [aceptaPrivacidad, setAceptaPrivacidad] = useState(false); + const [mostrarPrivacidad, setMostrarPrivacidad] = useState(false); + const [localDestacado, setLocalDestacado] = useState(null); // id del local abierto vía enlace compartido + const { favoritos, esFavorito } = useFavoritos(); + const { ubicacion, buscando: buscandoUbicacion, error: errorUbicacion, pedirUbicacion, limpiarUbicacion } = useUbicacion(); + const mapRef = useRef(null); + const mapInstanceRef = useRef(null); + const markersRef = useRef([]); + + const [form, setForm] = useState(FORM_VACIO); + + // Carga locales aprobados en tiempo real. + // suscribirLocales es async (hace fetch antes de devolver la función de + // limpieza), así que su valor de retorno inmediato es una Promise, no la + // función en sí: hay que esperarla antes de poder cancelar el intervalo. + useEffect(() => { + setLoading(true); + let cancelado = false; + let limpiar; + suscribirLocales( + (lista) => { if (!cancelado) { setLocales(lista); setLoading(false); } }, + () => { if (!cancelado) { setLocales([]); setLoading(false); } }, + ).then((fn) => { if (cancelado) fn?.(); else limpiar = fn; }); + return () => { cancelado = true; limpiar?.(); }; + }, []); + + // Carga filtro de palabras desde el backend + useEffect(() => { + obtenerPalabrasFiltradas().then((p) => setPalabrasProhibidas(p)); + }, []); + + // Enlace compartido (?local=ID): al cargar, resalta y desplaza hasta esa ficha + useEffect(() => { + const id = new URLSearchParams(window.location.search).get("local"); + if (!id || loading || locales.length === 0) return; + setLocalDestacado(id); + const el = document.getElementById(`local-${id}`); + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "center" }); + const timer = setTimeout(() => setLocalDestacado(null), 3000); + return () => clearTimeout(timer); + } + }, [loading, locales]); + + const resetForm = () => setForm(FORM_VACIO); + + const abrirPasarela = () => { + setPasarelaAbierta(true); + setErrorFiltro(""); + setPosibleDuplicado(null); + }; + + // Cerrar la pasarela descarta el borrador en curso + const cerrarPasarela = () => { + setPasarelaAbierta(false); + resetForm(); + setAceptaPrivacidad(false); + setErrorFiltro(""); + setPosibleDuplicado(null); + }; + + const guardarPropuesta = async (lat, lng) => { + setSaving(true); + try { + await enviarPropuesta({ + nombre: form.nombre, provincia: form.provincia, + categoria: form.categoria, subcategoria: form.subcategoria, + direccion: form.direccion, enlaceGoogleMaps: form.enlaceGoogleMaps, + lat, lng, + }); + setEnviado(true); + resetForm(); + setAceptaPrivacidad(false); + setPasarelaAbierta(false); + setPosibleDuplicado(null); + setTimeout(() => setEnviado(false), 5000); + } catch (e) { + console.error(e); + } + setSaving(false); + }; + + const enviarLocal = async () => { + setErrorFiltro(""); + setPosibleDuplicado(null); + // Comprobar filtro de palabras + const textoCompleto = [form.nombre, form.direccion].join(" "); + if (contienepalabrasProhibidas(textoCompleto, palabrasProhibidas)) { + setErrorFiltro("Tu propuesta contiene palabras no permitidas. Por favor, revisa el nombre o la dirección."); + return; + } + + let { lat, lng } = form; + if (!lat || !lng) { + const fallback = COORDS_PROVINCIAS[form.provincia] || CENTRO_ESPANA; + lat = fallback[0] + (Math.random() - 0.5) * 0.04; + lng = fallback[1] + (Math.random() - 0.5) * 0.04; + } + + // Comprobar si ya existe un local igual (mismo nombre + provincia, o muy cerca) + setComprobandoDuplicado(true); + try { + const chequeo = await comprobarDuplicado({ nombre: form.nombre, provincia: form.provincia, lat, lng }); + setComprobandoDuplicado(false); + if (chequeo?.duplicado) { + setPosibleDuplicado({ coincidencias: chequeo.coincidencias, lat, lng }); + return; + } + } catch (e) { + console.error(e); + setComprobandoDuplicado(false); + // Si falla la comprobación, se permite continuar para no bloquear el envío + } + + await guardarPropuesta(lat, lng); + }; + + const confirmarPeseADuplicado = () => { + if (!posibleDuplicado) return; + guardarPropuesta(posibleDuplicado.lat, posibleDuplicado.lng); + }; + + const subcatsFiltro = filtroCategoria ? CATEGORIAS[filtroCategoria]?.subcategorias || [] : []; + + const localesFiltrados = locales + .filter((l) => { + const q = busqueda.toLowerCase(); + return (!q || l.nombre.toLowerCase().includes(q) || l.provincia.toLowerCase().includes(q) || (l.categoria || "").toLowerCase().includes(q) || (l.subcategoria || "").toLowerCase().includes(q) || (l.direccion || "").toLowerCase().includes(q)) + && (!filtroCategoria || l.categoria === filtroCategoria) + && (!filtroSubcat || l.subcategoria === filtroSubcat) + && (!filtroProvincia || l.provincia === filtroProvincia) + && (!soloFavoritos || esFavorito(l.id)); + }) + .sort((a, b) => { + if (orden === "distancia" && ubicacion) { + const da = a.lat && a.lng ? calcularDistanciaKm(ubicacion.lat, ubicacion.lng, a.lat, a.lng) : Infinity; + const db = b.lat && b.lng ? calcularDistanciaKm(ubicacion.lat, ubicacion.lng, b.lat, b.lng) : Infinity; + return da - db; + } + if (orden === "fecha") return new Date(b.fecha) - new Date(a.fecha); + if (orden === "puntuacion") return b.puntuacion - a.puntuacion; + return a.nombre.localeCompare(b.nombre); + }); + + // Mapa Leaflet. El contenedor se mantiene siempre montado (ver más abajo, + // solo se oculta con CSS) para que la instancia de Leaflet no se pierda al + // cambiar de pestaña. Si ya existe, solo recalculamos su tamaño: Leaflet no + // lo hace solo cuando el contenedor pasa de display:none a visible, y por + // eso antes hacía falta refrescar la página para que el mapa se viera bien. + useEffect(() => { + if (vista !== "mapa") return; + const timer = setTimeout(() => { + if (!mapRef.current) return; + if (mapInstanceRef.current) { + mapInstanceRef.current.invalidateSize(); + return; + } + if (!window.L) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"; + document.head.appendChild(link); + const script = document.createElement("script"); + script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"; + script.onload = () => initMap(); + document.head.appendChild(script); + } else { + initMap(); + } + }, 100); + return () => clearTimeout(timer); + }, [vista]); + + useEffect(() => { + if (vista === "mapa" && mapInstanceRef.current && window.L) updateMarkers(); + }, [locales, vista, filtroCategoria, filtroSubcat, filtroProvincia, busqueda, soloFavoritos, favoritos]); + + function initMap() { + if (!mapRef.current || mapInstanceRef.current) return; + const L = window.L; + const map = L.map(mapRef.current, { zoomControl: true }).setView([40.416, -3.703], 6); + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: '© OpenStreetMap', maxZoom: 19 }).addTo(map); + mapInstanceRef.current = map; + updateMarkers(map); + } + + function updateMarkers(mapObj) { + const L = window.L; + const map = mapObj || mapInstanceRef.current; + if (!L || !map) return; + markersRef.current.forEach((m) => m.remove()); + markersRef.current = []; + localesFiltrados.forEach((local) => { + if (!local.lat || !local.lng) return; + const cat = obtenerCategoria(local.categoria); + const color = COLORES_MAPA[local.categoria] || cat.color; + const puntuacion = Math.min(5, Math.max(0, Math.round(Number(local.puntuacion) || 0))); + const stars = "★".repeat(puntuacion) + "☆".repeat(5 - puntuacion); + const labelDir = local.direccion ? ` · ${local.direccion.split(",")[0]}` : ""; + const marker = L.marker([local.lat, local.lng], { + icon: L.divIcon({ className: "", html: `
${cat.emoji} ${local.nombre}${labelDir}
`, iconAnchor: [0, 32] }), + }).addTo(map); + const gmLink = local.enlaceGoogleMaps || enlaceGoogleMapsDesdeLocal(local); + const wazeLink = enlaceWazeDesdeLocal(local); + const direccionHtml = local.direccion + ? `
📍 ${gmLink ? `${local.direccion}` : local.direccion}` + : ""; + const enlacesHtml = `${gmLink ? `Google Maps ↗` : ""}${wazeLink ? `Waze ↗` : ""}`; + marker.bindPopup(`
${local.nombre}
${local.provincia}
${cat.emoji} ${local.categoria}${local.subcategoria ? `${local.subcategoria}` : ""}${direccionHtml}
${enlacesHtml}
${stars}${local.descripcion ? `"${local.descripcion}"` : ""}
`); + markersRef.current.push(marker); + }); + } + + const conteosCat = NOMBRES_CATEGORIAS.reduce((acc, c) => { acc[c] = locales.filter((l) => l.categoria === c).length; return acc; }, {}); + const provincias = [...new Set(locales.map((l) => l.provincia))].sort(); + const mediaGlobal = locales.length ? (locales.reduce((s, l) => s + l.puntuacion, 0) / locales.length).toFixed(1) : "—"; + + return ( +
+ {/* Header */} +
+
+

+ LocalESP · Locales Españoles +

+

Directorio colaborativo de negocios por toda España

+
+
+ + {/* Confirmación enviada */} + {enviado && ( +
+ +
+

¡Propuesta enviada!

+

El administrador revisará tu propuesta y la publicará si es correcta.

+
+
+ )} + + {/* Stats */} +
+ {[{ label: "Locales", value: locales.length }, { label: "Provincias", value: provincias.length }, { label: "Media ★", value: mediaGlobal }].map((s) => ( +
+

{s.label}

+

{s.value}

+
+ ))} +
+ + {/* Chips categorías */} +
+ {NOMBRES_CATEGORIAS.map((cat) => { + const c = CATEGORIAS[cat]; + const activo = filtroCategoria === cat; + return ( + + ); + })} + {filtroCategoria && } +
+ + + {/* Tabs */} +
+ {[{ id: "lista", label: "Lista", icon: "ti-list" }, { id: "mapa", label: "Mapa", icon: "ti-map-2" }].map((v) => ( + + ))} +
+ + {vista === "lista" && ( + <> +
+ setBusqueda(e.target.value)} aria-label="Buscar por nombre, dirección o categoría" /> + + + +
+ +
+ {!ubicacion ? ( + + ) : ( + + )} + {errorUbicacion && {errorUbicacion}} + +
+ + {loading ? ( +
+ + Cargando locales... +
+ ) : localesFiltrados.length === 0 ? ( +
+ +

+ {locales.length === 0 ? "Todavía no hay locales publicados" : soloFavoritos ? "Aún no tienes favoritos guardados" : "No hay locales que coincidan"} +

+

{soloFavoritos ? "Pulsa el corazón ♥ en un local para guardarlo aquí" : "Sé el primero en proponer uno — el administrador lo revisará y publicará"}

+
+ ) : ( +
+ {localesFiltrados.map((local) => ( +
+ +
+ ))} + {localesFiltrados.length < locales.length && ( +

+ Mostrando {localesFiltrados.length} de {locales.length} locales +

+ )} +
+ )} + + )} + + {/* El contenedor del mapa se mantiene siempre montado (solo se oculta con + CSS) para que Leaflet no pierda su instancia al cambiar de pestaña. */} +
+
+ {NOMBRES_CATEGORIAS.map((cat) => ( + + + {CATEGORIAS[cat].emoji} {cat} + + ))} +
+ {locales.length === 0 &&
Todavía no hay locales publicados
} + {locales.length > 0 && localesFiltrados.length === 0 &&
Ningún local coincide con el filtro seleccionado
} +
+
+ + + {localesFiltrados.length} {localesFiltrados.length === 1 ? "local marcado" : "locales marcados"} + {localesFiltrados.length !== locales.length && ` de ${locales.length}`} + +
+
+ +

+ Los locales pasan por revisión antes de publicarse · ¿Tienes un negocio? Proponlo con el botón flotante ➕ + {" · "} + +

+ + {/* Botones de propuesta (ambos abren la misma pasarela) y modal */} + + + {pasarelaAbierta && ( + setPosibleDuplicado(null)} + aceptaPrivacidad={aceptaPrivacidad} + setAceptaPrivacidad={setAceptaPrivacidad} + onAbrirPrivacidad={() => setMostrarPrivacidad(true)} + errorFiltro={errorFiltro} + posibleDuplicado={posibleDuplicado} + comprobandoDuplicado={comprobandoDuplicado} + saving={saving} + /> + )} + + {mostrarPrivacidad && setMostrarPrivacidad(false)} />} + + +
+ ); +} diff --git a/src/AppMovil.jsx b/src/AppMovil.jsx new file mode 100644 index 0000000..50a1462 --- /dev/null +++ b/src/AppMovil.jsx @@ -0,0 +1,383 @@ +import { useState, useEffect, useCallback } from "react"; +import { suscribirLocales, enviarPropuesta, obtenerPalabrasFiltradas, contienepalabrasProhibidas } from "./firebase.js"; + +const CATEGORIAS = { + "Restauración": { emoji:"🍽️", color:"#8B0000", bg:"#FFF0F0", text:"#8B0000", subcategorias:["Tapas y raciones","Paella y arroces","Pintxos","Asador / Carne a la brasa","Mariscos y pescados","Bocadillos y montaditos","Menú del día","Cocina vasca","Cocina catalana","Cocina andaluza","Cocina gallega","Cocina madrileña","Cocina mediterránea","Pizzería","Hamburguesería","Comida rápida","Cocina internacional","Cafetería / Desayunos","Heladería","Pastelería"] }, + "Pequeño comercio": { emoji:"🛍️", color:"#1A5276", bg:"#EBF5FB", text:"#1A5276", subcategorias:["Alimentación / Ultramarinos","Frutería","Carnicería","Pescadería","Panadería","Farmacia","Papelería / Librería","Floristería","Joyería / Relojería","Zapatería","Ropa y moda","Juguetería","Ferretería","Bazar / Todo a 100","Estanco","Quiosco","Óptica","Ortopedia","Tienda de mascotas","Electrodomésticos"] }, + "Peluquería y estética": { emoji:"✂️", color:"#76448A", bg:"#F5EEF8", text:"#76448A", subcategorias:["Peluquería señora","Peluquería caballero","Peluquería unisex","Barbería","Centro de estética","Uñas / Manicura","Depilación","Masajes y spa","Tatuajes y piercings","Centro de bronceado","Micropigmentación"] }, + "Servicios del hogar": { emoji:"🔧", color:"#1E8449", bg:"#EAFAF1", text:"#1E8449", subcategorias:["Cerrajería","Fontanería / Plomería","Electricidad","Reformas y construcción","Pintura","Carpintería","Cristalería","Climatización / Aire acondicionado","Mudanzas","Limpieza","Jardinería","Instalación solar","Alarmas y seguridad","Reparación electrodomésticos"] }, + "Otros": { emoji:"📌", color:"#784212", bg:"#FDF2E9", text:"#784212", subcategorias:["Taller mecánico","Lavado de coches","Academia / Clases","Gestoría / Asesoría","Inmobiliaria","Agencia de viajes","Fotografía","Informática / Reparación móviles","Copistería / Imprenta","Veterinaria","Gimnasio / Fitness","Centro médico / Clínica","Fisioterapia","Psicología","Lavandería","Tintorería","Otro"] }, +}; +const CATS = Object.keys(CATEGORIAS); + +const PROVINCIAS = ["Álava","Albacete","Alicante","Almería","Asturias","Ávila","Badajoz","Barcelona","Burgos","Cáceres","Cádiz","Cantabria","Castellón","Ciudad Real","Córdoba","Cuenca","Gerona","Granada","Guadalajara","Guipúzcoa","Huelva","Huesca","Islas Baleares","Jaén","La Coruña","La Rioja","Las Palmas","León","Lérida","Lugo","Madrid","Málaga","Murcia","Navarra","Orense","Palencia","Pontevedra","Salamanca","Santa Cruz de Tenerife","Segovia","Sevilla","Soria","Tarragona","Teruel","Toledo","Valencia","Valladolid","Vizcaya","Zamora","Zaragoza"]; + +const COORDS = {"Álava":[42.85,-2.67],"Albacete":[38.99,-1.86],"Alicante":[38.35,-0.48],"Almería":[36.83,-2.46],"Asturias":[43.36,-5.86],"Ávila":[40.66,-4.68],"Badajoz":[38.88,-6.97],"Barcelona":[41.39,2.17],"Burgos":[42.34,-3.70],"Cáceres":[39.48,-6.37],"Cádiz":[36.53,-6.29],"Cantabria":[43.46,-3.81],"Castellón":[39.99,-0.05],"Ciudad Real":[38.98,-3.93],"Córdoba":[37.89,-4.78],"Cuenca":[40.07,-2.14],"Gerona":[41.98,2.82],"Granada":[37.18,-3.60],"Guadalajara":[40.63,-3.16],"Guipúzcoa":[43.32,-1.98],"Huelva":[37.26,-6.94],"Huesca":[42.14,-0.41],"Islas Baleares":[39.57,2.65],"Jaén":[37.78,-3.78],"La Coruña":[43.36,-8.41],"La Rioja":[42.46,-2.44],"Las Palmas":[28.12,-15.44],"León":[42.60,-5.57],"Lérida":[41.62,0.62],"Lugo":[43.01,-7.56],"Madrid":[40.42,-3.70],"Málaga":[36.72,-4.42],"Murcia":[37.99,-1.13],"Navarra":[42.82,-1.64],"Orense":[42.34,-7.86],"Palencia":[42.01,-4.53],"Pontevedra":[42.43,-8.65],"Salamanca":[40.97,-5.66],"Santa Cruz de Tenerife":[28.46,-16.25],"Segovia":[40.94,-4.11],"Sevilla":[37.39,-5.98],"Soria":[41.76,-2.46],"Tarragona":[41.12,1.24],"Teruel":[40.34,-1.11],"Toledo":[39.86,-4.03],"Valencia":[39.47,-0.38],"Valladolid":[41.65,-4.72],"Vizcaya":[43.26,-2.94],"Zamora":[41.50,-5.74],"Zaragoza":[41.65,-0.89]}; + +async function geocodificar(dir) { + try { + const q = encodeURIComponent(dir + ", España"); + const r = await fetch(`https://nominatim.openstreetmap.org/search?q=${q}&format=json&limit=1&countrycodes=es`, { headers:{"Accept-Language":"es","User-Agent":"LocalesMovil/1.0"} }); + const d = await r.json(); + if (d?.length) return { lat: parseFloat(d[0].lat), lng: parseFloat(d[0].lon) }; + } catch {} + return null; +} + +function parsearGoogleMaps(url) { + try { + let m = url.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/); + if (m) return { lat: parseFloat(m[1]), lng: parseFloat(m[2]) }; + m = url.match(/!3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/); + if (m) return { lat: parseFloat(m[1]), lng: parseFloat(m[2]) }; + } catch {} + return null; +} + +// ─── Estilos base móvil ─────────────────────────────────────────────────────── +const S = { + screen: { minHeight:"100dvh", display:"flex", flexDirection:"column", background:"#F8F5F0", fontFamily:"'Georgia', serif" }, + header: { background:"#8B0000", color:"white", padding:"14px 16px 10px", position:"sticky", top:0, zIndex:10 }, + headerTitle: { margin:0, fontSize:20, fontWeight:700, letterSpacing:"-0.3px" }, + headerSub: { margin:"2px 0 0", fontSize:12, opacity:0.75 }, + body: { flex:1, overflowY:"auto", padding:"12px 14px 80px" }, + navBar: { position:"fixed", bottom:0, left:0, right:0, background:"white", borderTop:"1px solid #E5DDD5", display:"flex", zIndex:10, padding:"4px 0" }, + navBtn: (active) => ({ flex:1, display:"flex", flexDirection:"column", alignItems:"center", gap:3, padding:"6px 0", background:"none", border:"none", cursor:"pointer", color: active ? "#8B0000" : "#999", fontSize:10, fontWeight: active ? 700 : 400 }), + navIcon: (active) => ({ fontSize:22, lineHeight:1, color: active ? "#8B0000" : "#999" }), + card: { background:"white", borderRadius:12, padding:"14px 16px", marginBottom:10, border:"1px solid #EEE8E0" }, + badge: (cat) => { const c = CATEGORIAS[cat]; return { background: c?.bg||"#eee", color: c?.text||"#555", fontSize:11, padding:"3px 9px", borderRadius:20, fontWeight:500, display:"inline-block" }; }, + input: { width:"100%", boxSizing:"border-box", padding:"11px 13px", borderRadius:10, border:"1px solid #DDD7CF", background:"white", fontSize:15, outline:"none", fontFamily:"inherit" }, + label: { fontSize:12, color:"#888", display:"block", marginBottom:5, fontWeight:600, textTransform:"uppercase", letterSpacing:"0.4px" }, + btn: (primary) => ({ width:"100%", padding:"13px", borderRadius:10, border:"none", cursor:"pointer", fontSize:15, fontWeight:700, background: primary ? "#8B0000" : "#F0EBE5", color: primary ? "white" : "#666", fontFamily:"inherit" }), + stars: (n) => "★".repeat(n)+"☆".repeat(5-n), +}; + +// ─── Pantalla: Lista ────────────────────────────────────────────────────────── +function PantallaLista({ locales, onDelete, onBuscar, busqueda, filtroCat, setFiltroCat }) { + const filtrados = locales.filter(l => { + const q = busqueda.toLowerCase(); + return (!q || l.nombre.toLowerCase().includes(q) || (l.direccion||"").toLowerCase().includes(q) || l.provincia.toLowerCase().includes(q)) + && (!filtroCat || l.categoria === filtroCat); + }); + + return ( + <> +
+ onBuscar(e.target.value)} /> +
+ + {/* Chips categoría */} +
+ + {CATS.map(c => ( + + ))} +
+ + {filtrados.length === 0 ? ( +
+
🏘️
+

{locales.length===0 ? "¡Sé el primero en añadir un local!" : "Sin resultados"}

+
+ ) : filtrados.map(local => ( +
+
+
+

{local.nombre}

+

📍 {local.provincia}

+
+ +
+
+ {CATEGORIAS[local.categoria]?.emoji} {local.categoria} + {local.subcategoria && {local.subcategoria}} +
+ {local.direccion &&

{local.direccion}

} +
+ {S.stars(local.puntuacion)} + {(local.direccion || local.enlaceGoogleMaps) && ( + + Maps ↗ + + )} +
+ {local.descripcion &&

"{local.descripcion}"

} +
+ ))} + + ); +} + +// ─── Pantalla: Añadir ───────────────────────────────────────────────────────── +function PantallaAnadir({ onGuardar }) { + const [form, setForm] = useState({ nombre:"", provincia:"", categoria:"", subcategoria:"", direccion:"", enlaceGoogleMaps:"", puntuacion:0, descripcion:"" }); + const [modoUbic, setModoUbic] = useState("direccion"); + const [geocodif, setGeocodif] = useState(null); + const [guardando, setGuardando] = useState(false); + const [ok, setOk] = useState(false); + + const valido = form.nombre && form.provincia && form.categoria && form.puntuacion > 0 && (form.direccion || form.enlaceGoogleMaps); + + const guardar = async () => { + if (!valido) return; + setGuardando(true); + let lat, lng; + if (modoUbic === "direccion" && form.direccion) { + const geo = await geocodificar(form.direccion); + if (geo) { lat = geo.lat; lng = geo.lng; } + } else if (form.enlaceGoogleMaps) { + const c = parsearGoogleMaps(form.enlaceGoogleMaps); + if (c) { lat = c.lat; lng = c.lng; } + } + if (!lat) { + const fb = COORDS[form.provincia] || [40.4,-3.7]; + lat = fb[0] + (Math.random()-0.5)*0.04; + lng = fb[1] + (Math.random()-0.5)*0.04; + } + await onGuardar({ ...form, lat, lng, id: Date.now().toString(), fecha: new Date().toISOString() }); + setOk(true); + setGuardando(false); + setTimeout(() => { + setOk(false); + setForm({ nombre:"", provincia:"", categoria:"", subcategoria:"", direccion:"", enlaceGoogleMaps:"", puntuacion:0, descripcion:"" }); + setGeocodif(null); + }, 1500); + }; + + if (ok) return ( +
+
+

¡Local añadido!

+

Sincronizando con la web...

+
+ ); + + return ( +
+
+ + setForm(f=>({...f,nombre:e.target.value}))} /> +
+
+ + +
+
+ + +
+ {form.categoria && ( +
+ + +
+ )} + + {/* Ubicación */} +
+ +
+ {[{id:"direccion",label:"📍 Dirección"},{id:"enlace",label:"🔗 Google Maps"}].map(opt => ( + + ))} +
+ {modoUbic === "direccion" ? ( + { setForm(f=>({...f,direccion:e.target.value})); setGeocodif(null); }} /> + ) : ( + { + const val = e.target.value; + setForm(f=>({...f,enlaceGoogleMaps:val})); + const c = parsearGoogleMaps(val); + setGeocodif(c ? "✅ Coordenadas detectadas" : val ? "⚠️ No se detectaron coordenadas" : null); + }} /> + )} + {geocodif &&

{geocodif}

} +
+ + {/* Puntuación */} +
+ +
+ {[1,2,3,4,5].map(n => ( + + ))} +
+
+ +
+ +