From bed161c92aa001cb71b57cbca58ea5955cb05359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Thu, 14 May 2026 01:58:40 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20MVP=20Fase=200=20=E2=80=94=20Process=20?= =?UTF-8?q?Cost=20Platform=20v0.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plataforma completa de análisis de costos operativos basada en BPMN 2.0. Funcionalidades incluidas: - Import de archivos BPMN 2.0 por drag & drop + 3 procesos de ejemplo - Visualización read-only del diagrama (bpmn-js) - Configuración de actividades (costo, tiempo, recursos asignados) - CRUD de recursos (rol, persona, sistema, equipo, insumo) - Configuración global (moneda, overhead %, nombre, cliente) - Motor de simulación determinístico agregado con propagación de gateways XOR/AND - Reporte visual con mapa de calor en dos modos (relativo/absoluto) - Gráficos: KPIs, top actividades, composición, costo por recurso - Export PDF (jsPDF + html2canvas) y CSV (papaparse) - Persistencia en IndexedDB (Dexie) - 268 tests unitarios/integración + 6 E2E con Playwright - Deploy: https://process-cost-platform.pages.dev Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 31 + ARCHITECTURE.md | 835 ++ CLAUDE.md | 173 + README.md | 46 + TODO.md | 33 + index.html | 26 + package-lock.json | 8584 +++++++++++++++++ package.json | 81 + playwright.config.ts | 17 + playwright.prod.config.ts | 12 + postcss.config.js | 6 + public/favicon.svg | 10 + public/icons.svg | 24 + public/sample-processes/accent-test.bpmn | 86 + .../sample-processes/complex-with-loop.bpmn | 136 + .../medium-with-gateways.bpmn | 168 + public/sample-processes/simple-linear.bpmn | 98 + src/App.tsx | 14 + src/components/AppFooter.tsx | 9 + src/components/ui/badge.tsx | 30 + src/components/ui/button.tsx | 48 + src/components/ui/card.tsx | 55 + src/components/ui/input.tsx | 21 + src/components/ui/label.tsx | 18 + src/components/ui/scroll-area.tsx | 43 + src/components/ui/select.tsx | 147 + src/components/ui/separator.tsx | 23 + src/components/ui/slider.tsx | 22 + src/components/ui/table.tsx | 56 + src/components/ui/tabs.tsx | 52 + src/components/ui/toast.tsx | 98 + src/components/ui/toaster.tsx | 20 + src/components/ui/tooltip.tsx | 27 + src/components/ui/use-toast.ts | 70 + src/domain/bpmn-parser.ts | 153 + src/domain/schemas.ts | 54 + src/domain/simulation.ts | 255 + src/domain/types.ts | 175 + src/features/import/ImportPage.tsx | 277 + src/features/import/SampleProcessLoader.tsx | 119 + src/features/report/ActivitiesTable.tsx | 133 + src/features/report/CostByResourceChart.tsx | 28 + src/features/report/CostCompositionChart.tsx | 18 + src/features/report/HeatmapCanvas.tsx | 279 + src/features/report/HeatmapLegend.tsx | 35 + src/features/report/HeatmapModeToggle.tsx | 49 + src/features/report/KpiBar.tsx | 72 + src/features/report/MethodologyFooter.tsx | 36 + src/features/report/ReportHeader.tsx | 67 + src/features/report/ReportPage.tsx | 278 + src/features/report/TopActivitiesChart.tsx | 24 + src/features/report/WarningsBanner.tsx | 36 + src/features/report/useReportData.ts | 54 + src/features/simulation/useSimulate.ts | 56 + src/features/workspace/ActivityPanel.tsx | 241 + src/features/workspace/BpmnCanvas.tsx | 82 + src/features/workspace/GatewayPanel.tsx | 277 + .../workspace/GlobalSettingsPanel.tsx | 140 + src/features/workspace/ResourcesPanel.tsx | 201 + src/features/workspace/WorkspacePage.tsx | 293 + src/lib/chart-options.ts | 242 + src/lib/colors.ts | 149 + src/lib/export/csv-export.ts | 115 + src/lib/export/pdf-export.ts | 131 + src/lib/export/pdf-sections.ts | 291 + src/lib/export/slug.ts | 28 + src/lib/format.ts | 36 + src/lib/utils.ts | 6 + src/main.tsx | 10 + src/persistence/db.ts | 24 + src/persistence/repositories.ts | 114 + src/router.tsx | 51 + src/store/process-store.ts | 99 + src/store/simulation-store.ts | 28 + src/styles/globals.css | 92 + tailwind.config.js | 62 + tests/domain/simulation.test.ts | 210 + tests/e2e/export-pdf.spec.ts | 418 + tests/e2e/prod-smoke.spec.ts | 91 + .../__snapshots__/derivations.test.ts.snap | 85 + tests/features/report/components.test.tsx | 409 + tests/features/report/derivations.test.ts | 293 + tests/integration/bpmn-parser.test.ts | 251 + tests/integration/gateway-validation.test.ts | 157 + .../integration/indexeddb-persistence.test.ts | 331 + tests/integration/simulation-engine.test.ts | 278 + tests/lib/colors.test.ts | 213 + tests/lib/export/csv-export.test.ts | 220 + tests/lib/export/measure-sizes.test.ts | 88 + tests/lib/export/pdf-export.test.ts | 166 + tests/lib/export/slug.test.ts | 83 + tests/lib/format.test.ts | 211 + tests/setup.ts | 8 + tsconfig.app.json | 32 + tsconfig.json | 7 + tsconfig.node.json | 24 + vite.config.ts | 34 + 97 files changed, 19638 insertions(+) create mode 100644 .gitignore create mode 100644 ARCHITECTURE.md create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 TODO.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 playwright.prod.config.ts create mode 100644 postcss.config.js create mode 100644 public/favicon.svg create mode 100644 public/icons.svg create mode 100644 public/sample-processes/accent-test.bpmn create mode 100644 public/sample-processes/complex-with-loop.bpmn create mode 100644 public/sample-processes/medium-with-gateways.bpmn create mode 100644 public/sample-processes/simple-linear.bpmn create mode 100644 src/App.tsx create mode 100644 src/components/AppFooter.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/slider.tsx create mode 100644 src/components/ui/table.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/toast.tsx create mode 100644 src/components/ui/toaster.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/components/ui/use-toast.ts create mode 100644 src/domain/bpmn-parser.ts create mode 100644 src/domain/schemas.ts create mode 100644 src/domain/simulation.ts create mode 100644 src/domain/types.ts create mode 100644 src/features/import/ImportPage.tsx create mode 100644 src/features/import/SampleProcessLoader.tsx create mode 100644 src/features/report/ActivitiesTable.tsx create mode 100644 src/features/report/CostByResourceChart.tsx create mode 100644 src/features/report/CostCompositionChart.tsx create mode 100644 src/features/report/HeatmapCanvas.tsx create mode 100644 src/features/report/HeatmapLegend.tsx create mode 100644 src/features/report/HeatmapModeToggle.tsx create mode 100644 src/features/report/KpiBar.tsx create mode 100644 src/features/report/MethodologyFooter.tsx create mode 100644 src/features/report/ReportHeader.tsx create mode 100644 src/features/report/ReportPage.tsx create mode 100644 src/features/report/TopActivitiesChart.tsx create mode 100644 src/features/report/WarningsBanner.tsx create mode 100644 src/features/report/useReportData.ts create mode 100644 src/features/simulation/useSimulate.ts create mode 100644 src/features/workspace/ActivityPanel.tsx create mode 100644 src/features/workspace/BpmnCanvas.tsx create mode 100644 src/features/workspace/GatewayPanel.tsx create mode 100644 src/features/workspace/GlobalSettingsPanel.tsx create mode 100644 src/features/workspace/ResourcesPanel.tsx create mode 100644 src/features/workspace/WorkspacePage.tsx create mode 100644 src/lib/chart-options.ts create mode 100644 src/lib/colors.ts create mode 100644 src/lib/export/csv-export.ts create mode 100644 src/lib/export/pdf-export.ts create mode 100644 src/lib/export/pdf-sections.ts create mode 100644 src/lib/export/slug.ts create mode 100644 src/lib/format.ts create mode 100644 src/lib/utils.ts create mode 100644 src/main.tsx create mode 100644 src/persistence/db.ts create mode 100644 src/persistence/repositories.ts create mode 100644 src/router.tsx create mode 100644 src/store/process-store.ts create mode 100644 src/store/simulation-store.ts create mode 100644 src/styles/globals.css create mode 100644 tailwind.config.js create mode 100644 tests/domain/simulation.test.ts create mode 100644 tests/e2e/export-pdf.spec.ts create mode 100644 tests/e2e/prod-smoke.spec.ts create mode 100644 tests/features/report/__snapshots__/derivations.test.ts.snap create mode 100644 tests/features/report/components.test.tsx create mode 100644 tests/features/report/derivations.test.ts create mode 100644 tests/integration/bpmn-parser.test.ts create mode 100644 tests/integration/gateway-validation.test.ts create mode 100644 tests/integration/indexeddb-persistence.test.ts create mode 100644 tests/integration/simulation-engine.test.ts create mode 100644 tests/lib/colors.test.ts create mode 100644 tests/lib/export/csv-export.test.ts create mode 100644 tests/lib/export/measure-sizes.test.ts create mode 100644 tests/lib/export/pdf-export.test.ts create mode 100644 tests/lib/export/slug.test.ts create mode 100644 tests/lib/format.test.ts create mode 100644 tests/setup.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f83cc70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Playwright outputs +tests/e2e/__output__/ +test-results/ + +# Wrangler +.wrangler/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..a77a502 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,835 @@ +# Documento de Arquitectura +## Plataforma de Diseño, Mapeo y Cuantificación de Procesos de Negocio + +> **Mantra**: *Los procesos de negocio son activos estratégicos de las empresas y un diferencial competitivo.* + +| Atributo | Valor | +|---|---| +| Versión del documento | 0.1.0 | +| Fecha | Mayo 2026 | +| Estado | Draft — Fase 0 (MVP "Noche Cero") en construcción | +| Autor | Equipo fundador + Claude (arquitectura) | +| Alcance | MVP de esta noche + Norte V1.0 (12–18 meses) | + +--- + +## Tabla de Contenidos + +1. [Contexto y Propuesta de Valor](#1-contexto-y-propuesta-de-valor) +2. [Glosario y Convenciones](#2-glosario-y-convenciones) +3. [Norte Estratégico — Visión V1.0](#3-norte-estratégico--visión-v10) +4. [Fase 0 — MVP "Noche Cero"](#4-fase-0--mvp-noche-cero) +5. [Roadmap de Evolución](#5-roadmap-de-evolución) +6. [Arquitectura Técnica](#6-arquitectura-técnica) +7. [Modelo de Dominio](#7-modelo-de-dominio) +8. [Decisiones Arquitectónicas (ADRs)](#8-decisiones-arquitectónicas-adrs) +9. [Atributos de Calidad](#9-atributos-de-calidad) +10. [Riesgos y Mitigaciones](#10-riesgos-y-mitigaciones) +11. [Anexos](#11-anexos) + +--- + +## 1. Contexto y Propuesta de Valor + +### 1.1 Problema que resolvemos + +Las consultoras, gerentes de operaciones y profesionales de eficiencia operativa carecen de una herramienta accesible que les permita **cuantificar de forma objetiva, visual y comunicable el costo operativo real de un proceso de negocio**. Las alternativas actuales son: + +- **Caras y corporativas** (Bizagi, Signavio, ARIS, Interact Process Manager): pensadas para grandes corporaciones, distantes de la realidad LATAM. +- **Genéricas** (Visio, draw.io, Lucidchart): dibujan procesos pero no los cuantifican. +- **Manuales** (Excel + Word + correo): no escalan, no son colaborativas, no son visuales. + +### 1.2 Promesa de valor única (UVP) + +> **Permitir a cualquier consultor o gerente de procesos navegar el resultado de la simulación de un proceso BPMN y entender visualmente, actividad por actividad, cómo se componen sus costos operativos y dónde están las oportunidades de mejora.** + +### 1.3 Diferenciadores estratégicos + +| Diferenciador | Cómo se materializa | +|---|---| +| **Cercanía LATAM** | Construido desde Paraguay, soporte en español LATAM, portugués BR e inglés, precio accesible | +| **Democratización del costeo operativo** | No solo medianas/grandes empresas: cualquier consultor independiente o pyme puede usarlo | +| **Visualización deslumbrante** | El mapa de calor sobre el BPMN y el reporte interactivo son el corazón del producto, no un anexo | +| **Educativa por diseño** | Glosario integrado, validador pedagógico, tutoriales in-app, plantillas comunitarias | +| **Forward-compatible con IA/RPA/Agentes** | El modelo de datos contempla nodos no-BPMN-estándar para representar bots RPA y agentes IA | + +### 1.4 Audiencia objetivo + +**MVP (Fase 0)**: el propio fundador como consultor de automatización de procesos. + +**V1.0**: consultores independientes y boutique de procesos, gerentes de operaciones de pymes LATAM, equipos de Project/Process Office. + +**V2+**: estudiantes y entusiastas de eficiencia operativa (tier educativo gratuito). + +### 1.5 Lo que NO somos + +- ❌ NO somos un BPMS (motor de ejecución de procesos). +- ❌ NO generamos código fuente para automatización (eso lo hace Claude Code u otro generador). +- ❌ NO competimos contra Camunda, Bonita, ni motores de orquestación. +- ❌ NO somos un editor BPMN general — usamos uno embebido (bpmn-js) pero nuestro valor está en el costeo y la simulación. + +--- + +## 2. Glosario y Convenciones + +| Término | Definición | +|---|---| +| **BPMN 2.0** | Business Process Model and Notation, estándar OMG para modelado de procesos | +| **Actividad** | Nodo BPMN tipo `Task` o `SubProcess` con trabajo asociado | +| **Costo directo** | Costo imputable a la ejecución de la actividad (recursos humanos, sistemas, insumos) | +| **Costo indirecto** | Costo asignado por overhead (estructura, espacio, indirectos prorrateados) | +| **Simulación** | Cálculo agregado del costo esperado de un proceso dadas las configuraciones de sus actividades | +| **Mapa de calor** | Visualización donde cada nodo BPMN se colorea según métrica de costo | +| **Tenant** | Organización/cliente lógicamente aislado en el SaaS (post-MVP) | +| **CRDT** | Conflict-free Replicated Data Type — estructura de datos para colaboración sin conflictos | +| **ABC** | Activity-Based Costing — costeo basado en actividades | + +**Convenciones del documento**: + +- 🔴 **MVP** = aplica a Fase 0 (esta noche) +- 🟡 **V1.0** = aplica al norte estratégico +- 🔵 **V2+** = futuro lejano + +--- + +## 3. Norte Estratégico — Visión V1.0 + +### 3.1 Capacidades funcionales objetivo (V1.0) + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ PLATAFORMA V1.0 │ +├──────────────────────────────────────────────────────────────────┤ +│ │ +│ 📐 DISEÑO BPMN │ +│ ├─ Editor BPMN 2.0 completo (basado en bpmn-js extendido) │ +│ ├─ Paleta extendida: elementos RPA + Agentes IA │ +│ ├─ Validador pedagógico en tiempo real │ +│ ├─ Glosario BPMN integrado │ +│ └─ Importación/exportación BPMN 2.0 XML estándar │ +│ │ +│ 🤝 COLABORACIÓN │ +│ ├─ Edición en tiempo real (hasta 5 usuarios concurrentes) │ +│ ├─ CRDT-based, sin conflictos │ +│ ├─ Presencia (cursores, selección, avatares) │ +│ └─ Versionado lineal (snapshots manuales y automáticos) │ +│ │ +│ 🏢 ESTRUCTURA ORGANIZACIONAL │ +│ ├─ Jerarquía configurable hasta 10 niveles │ +│ ├─ Nombres de niveles configurables por tenant │ +│ ├─ Procesos cross-area con gobernanza RACI │ +│ └─ Process owners, SLAs entre áreas │ +│ │ +│ 💰 COSTEO Y SIMULACIÓN │ +│ ├─ Costeo dual: ABC (Activity-Based) + por hora-recurso │ +│ ├─ Tiempos como distribuciones de probabilidad │ +│ ├─ Recursos: roles, personas, sistemas, equipamiento, insumos │ +│ ├─ Riesgos cualitativos (matriz alto/medio/bajo) │ +│ └─ Cálculo agregado de costos esperados │ +│ │ +│ 📊 VISUALIZACIÓN Y REPORTES │ +│ ├─ Mapa de calor configurable (absoluto / relativo / desviación)│ +│ ├─ Drill-down interactivo por actividad │ +│ ├─ Reportes exportables (PDF, PNG, CSV, Excel) │ +│ └─ Narrativa visual del costo end-to-end │ +│ │ +│ 🧠 INTELIGENCIA ARTIFICIAL │ +│ └─ Detección automática de cuellos de botella e ineficiencias │ +│ │ +│ 🎓 EDUCACIÓN Y COMUNIDAD │ +│ ├─ Tutoriales interactivos in-app │ +│ ├─ Plantillas de procesos publicadas por la comunidad │ +│ └─ Validador BPMN con explicaciones pedagógicas │ +│ │ +│ 🌐 PLATAFORMA │ +│ ├─ SaaS multi-tenant │ +│ ├─ PWA (instalable, offline-capable) │ +│ ├─ i18n: es-LATAM, en, pt-BR │ +│ ├─ Auth RBAC │ +│ └─ Freemium + suscripción por usuario │ +│ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +### 3.2 Atributos de calidad objetivo (V1.0) + +| Atributo | Objetivo | +|---|---| +| Disponibilidad | 99% (no SLA crítico al inicio) | +| Tiempo de carga inicial | < 3s en 3G | +| Concurrencia editorial | 5 usuarios por diagrama, latencia < 200ms | +| Tamaño máximo diagrama | 200 nodos sin degradación | +| Mantenibilidad | Stack monolítico modular, contratable en LATAM | +| Soberanía de datos | Self-hostable en cualquier VPS estándar | + +--- + +## 4. Fase 0 — MVP "Noche Cero" + +### 4.1 Hipótesis a validar + +> Un consultor de procesos puede transformar un archivo `.bpmn` en un reporte visualmente deslumbrante de costos operativos en menos de 15 minutos, y usarlo al día siguiente en una reunión real con un cliente. + +### 4.2 Alcance del MVP (estricto) + +| # | Capacidad | Detalle | +|---|---|---| +| F1 | **Importar BPMN** | Drag & drop de archivo `.bpmn` 2.0 XML estándar | +| F2 | **Visualizar diagrama** | Canvas read-only con bpmn-js, navegable (zoom, pan) | +| F3 | **Editar atributos de actividad** | Panel lateral al seleccionar nodo: costo, tiempo, recursos asignados | +| F4 | **Definir recursos** | CRUD simple de recursos con costo/hora (roles, sistemas, insumos) | +| F5 | **Simular** | Cálculo agregado determinístico de costo total y por actividad | +| F6 | **Mapa de calor** | Coloreo de nodos sobre el canvas según costo relativo | +| F7 | **Reporte visual interactivo** | Dashboard con: total, ranking de actividades costosas, gráficos, drill-down | +| F8 | **Exportar reporte** | PDF + CSV | +| F9 | **Persistencia local** | IndexedDB: el trabajo del consultor se conserva entre sesiones del navegador | + +### 4.3 Fuera de alcance del MVP + +❌ Login/registro · ❌ Multi-tenant · ❌ Colaboración · ❌ Versionado · ❌ Edición del diagrama BPMN (solo lectura del XML importado) · ❌ Distribuciones de probabilidad (costos y tiempos fijos) · ❌ Riesgos · ❌ Gobernanza/RACI · ❌ Jerarquía organizacional · ❌ IA · ❌ Elementos RPA/Agente custom · ❌ Tutoriales · ❌ Marketplace · ❌ Backend · ❌ i18n (solo español inicialmente) · ❌ PWA installable (solo SPA web) + +### 4.4 Criterios de éxito del MVP + +✅ El consultor puede importar un BPMN real, configurarlo y obtener reporte en < 15 minutos +✅ El reporte tiene calidad visual presentable ante un cliente sin retoques +✅ La aplicación corre 100% en el navegador, sin backend +✅ El export PDF se ve profesional (logo, paleta, tipografía, layout) +✅ El mapa de calor es inmediatamente comprensible para alguien sin formación BPMN + +### 4.5 Diseño del flujo de usuario del MVP + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ FLUJO MVP "NOCHE CERO" │ +└────────────────────────────────────────────────────────────────────┘ + + [1. Landing] + │ + │ "Importar mi proceso" + ▼ + [2. Drop zone .bpmn] ─────────► IndexedDB + │ + ▼ + [3. Workspace de configuración] + ├─ Canvas BPMN (read-only, navegable, bpmn-js) + ├─ Panel lateral derecho: + │ ├─ Tab "Atributos de actividad" (al seleccionar nodo) + │ │ ├─ Costo directo + │ │ ├─ Tiempo de ejecución (minutos) + │ │ ├─ Recursos asignados (multi-select) + │ │ └─ Probabilidad de ocurrencia (para gateways) + │ ├─ Tab "Recursos" (CRUD global) + │ │ ├─ Nombre, tipo (rol/sistema/insumo/equipo), costo/hora + │ └─ Tab "Configuración global" + │ ├─ Moneda + │ ├─ Overhead % (costos indirectos) + │ └─ Nombre del proceso, cliente + │ + │ Botón "Simular" + ▼ + [4. Pantalla de Reporte] + ├─ Header: nombre proceso, cliente, fecha, costo total + ├─ Mapa de calor sobre el diagrama (gradiente verde→rojo) + ├─ KPIs: costo total, costo/instancia, tiempo total, # actividades + ├─ Tabla: top 10 actividades más costosas (interactivo) + ├─ Gráfico de barras: costo por actividad + ├─ Gráfico donut: composición costo directo/indirecto + ├─ Gráfico de barras: costo por tipo de recurso + └─ Botones: "Exportar PDF" | "Exportar CSV" | "Volver a editar" +``` + +### 4.6 Decisiones de diseño visual (MVP) + +| Aspecto | Decisión | +|---|---| +| Tipografía | Inter (UI) + JetBrains Mono (datos numéricos) | +| Paleta principal | Neutros oscuros sobre claros (light mode primero) | +| Paleta de heatmap | Verde (#10b981) → Amarillo (#f59e0b) → Rojo (#ef4444) | +| Componentes | shadcn/ui sobre Tailwind | +| Iconografía | Lucide | +| Densidad | Media-alta (consultor profesional, no consumidor) | +| Modo oscuro | Postergado para post-MVP | + +### 4.7 Recomendación sobre el mapa de calor (decisión solicitada) + +**Decisión arquitectónica**: el heatmap será **configurable con 3 modos**, default **costo relativo**. + +| Modo | Cuándo se usa | Cómo colorea | +|---|---|---| +| **Costo relativo (%)** ← default | Análisis interno, identificar el 20% que cuesta el 80% | Cada nodo coloreado por su % sobre el total del proceso | +| **Costo absoluto** | Comparar entre procesos del mismo cliente | Escala global definida por el rango min-max del proceso | +| **Desviación vs benchmark** | Consultoría comparativa | Requiere benchmark del usuario; pospuesto a V1.0 si no hay tiempo esta noche | + +**Justificación**: el modo relativo es el más útil para "comunicar dónde duele", que es el caso de uso del consultor. El absoluto se entrega gratis con el mismo cálculo. El de benchmark es valioso pero requiere base de datos comparativa que no tendremos esta noche. + +--- + +## 5. Roadmap de Evolución + +``` +┌────────────┬──────────────────────────────────────────────────────┐ +│ FASE │ CAPACIDADES │ +├────────────┼──────────────────────────────────────────────────────┤ +│ Fase 0 │ MVP client-side: importar, configurar, simular, │ +│ (1 noche) │ reportear, exportar PDF/CSV │ +├────────────┼──────────────────────────────────────────────────────┤ +│ Fase 1 │ + Backend mínimo + Auth + Persistencia en la nube │ +│ (1-2 sem) │ + i18n (es/en/pt-BR) + PWA installable │ +├────────────┼──────────────────────────────────────────────────────┤ +│ Fase 2 │ + Editor BPMN (no solo lectura) + Validador │ +│ (1 mes) │ + Glosario integrado + Recursos avanzados │ +├────────────┼──────────────────────────────────────────────────────┤ +│ Fase 3 │ + Estructura organizacional + Multi-tenant │ +│ (1 mes) │ + RBAC + Versionado lineal │ +├────────────┼──────────────────────────────────────────────────────┤ +│ Fase 4 │ + Colaboración en tiempo real (Yjs/CRDT) │ +│ (1-2 mes) │ + Presencia + Comentarios │ +├────────────┼──────────────────────────────────────────────────────┤ +│ Fase 5 │ + Distribuciones probabilísticas + Riesgos │ +│ (1 mes) │ + Costeo ABC completo + Gobernanza RACI │ +├────────────┼──────────────────────────────────────────────────────┤ +│ Fase 6 │ + IA: detección de cuellos de botella │ +│ (1-2 mes) │ + Elementos RPA/Agente IA en paleta │ +├────────────┼──────────────────────────────────────────────────────┤ +│ Fase 7 │ + Marketplace comunitario de plantillas │ +│ (continuo) │ + Tutoriales interactivos + Modo educativo │ +└────────────┴──────────────────────────────────────────────────────┘ +``` + +--- + +## 6. Arquitectura Técnica + +### 6.1 Vista de alto nivel — MVP (Fase 0) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ NAVEGADOR DEL USUARIO │ +│ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ React SPA (Vite + TypeScript) │ │ +│ │ │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ +│ │ │ Router │ │ State │ │ UI Layer │ │ │ +│ │ │ (TanStack│ │ (Zustand)│ │ (shadcn + │ │ │ +│ │ │ Router)│ │ │ │ Tailwind) │ │ │ +│ │ └──────────┘ └──────────┘ └──────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌──────────────────────┐ │ │ +│ │ │ bpmn-js │ │ Simulation Engine │ │ │ +│ │ │ (canvas viewer) │ │ (TypeScript puro) │ │ │ +│ │ └─────────────────┘ └──────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌──────────────────────┐ │ │ +│ │ │ D3 + ECharts │ │ Export Layer │ │ │ +│ │ │ (heatmap + viz) │ │ (jsPDF + html2canvas)│ │ │ +│ │ └─────────────────┘ └──────────────────────┘ │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ IndexedDB (via Dexie.js) │ │ +│ │ - processes (BPMN XML + metadata) │ │ +│ │ - resources │ │ +│ │ - activities (costos, tiempos, recursos) │ │ +│ │ - simulations (resultados snapshots) │ │ +│ └────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + + Deployable a: Cloudflare Pages / Vercel + (estático, gratis, global) +``` + +### 6.2 Vista de alto nivel — V1.0 + +``` +┌──────────────────┐ ┌──────────────────────────────┐ +│ Cliente PWA │ HTTPS │ API Gateway (Caddy) │ +│ (React + Vite) ├────────►│ │ +└────────┬─────────┘ └──────────────┬───────────────┘ + │ │ + │ WSS (Yjs) │ REST + WebSocket + │ ▼ + │ ┌───────────────────────────────┐ + │ │ Backend (NestJS Monolito │ + │ │ Modular en Node.js) │ + │ │ │ + │ │ ┌─────────┬─────────┐ │ + │ │ │ Auth │ Process │ │ + │ │ │ Module │ Module │ │ + │ │ ├─────────┼─────────┤ │ + │ │ │ Org │ Simul. │ │ + │ │ │ Module │ Module │ │ + │ │ ├─────────┼─────────┤ │ + │ │ │ Collab │ AI │ │ + │ │ │ Module │ Module │ │ + │ │ └─────────┴─────────┘ │ + │ └────────┬──────────────────────┘ + │ │ + └──Yjs WebSocket────────────┤ + │ + ┌──────────────────────┼──────────────────────┐ + ▼ ▼ ▼ + ┌────────────┐ ┌────────────┐ ┌────────────┐ + │ PostgreSQL │ │ Redis │ │ MinIO │ + │ (datos) │ │ (cache, │ │ (archivos, │ + │ │ │ pubsub, │ │ exports) │ + │ │ │ sessions) │ │ │ + └────────────┘ └────────────┘ └────────────┘ + + Todo desplegable en una VPS (Hostinger / Hetzner) + vía Docker Compose +``` + +### 6.3 Stack tecnológico + +#### 6.3.1 Stack MVP (Fase 0) + +| Capa | Tecnología | Versión | Justificación | +|---|---|---|---| +| Lenguaje | TypeScript | 5.x | Type safety, mantenibilidad | +| Framework UI | React | 18.x | Ecosistema, Claude Code domina | +| Build tool | Vite | 5.x | Velocidad, simplicidad | +| Routing | TanStack Router | 1.x | Type-safe, moderno | +| Estado | Zustand | 4.x | Simple, sin boilerplate, escala | +| Canvas BPMN | bpmn-js | 17.x | Estándar de facto, MIT, Camunda | +| UI components | shadcn/ui | latest | Copy-paste, no es dependencia rígida | +| Estilos | Tailwind CSS | 3.x | Productividad, mantenibilidad | +| Iconos | Lucide React | latest | Consistencia visual | +| Gráficos | Apache ECharts | 5.x | Calidad visual superior para reportes | +| Heatmap canvas | D3.js (selecciones) | 7.x | Coloreo de nodos bpmn-js | +| Persistencia | IndexedDB vía Dexie | 4.x | API ergonómica sobre IndexedDB | +| Export PDF | jsPDF + html2canvas | latest | Render WYSIWYG del reporte | +| Export CSV | papaparse | 5.x | Robusto, manejo de edge cases | +| Validación schemas | Zod | 3.x | Schemas type-safe | +| Testing | Vitest + Testing Library | latest | Suficiente para MVP | +| Deploy | Cloudflare Pages | — | Gratis, global CDN, SSL incluido | + +#### 6.3.2 Stack evolutivo (V1.0) + +| Capa | Tecnología | Justificación | +|---|---|---| +| Backend framework | **NestJS** | Monolito modular natural, DI, contratable | +| Runtime | Node.js LTS | Mismo lenguaje que frontend | +| ORM | Prisma | Type-safe, migraciones, excelente DX | +| Base de datos | PostgreSQL 16 | Robustez relacional + JSONB para BPMN | +| Cache/PubSub | Redis 7 | Sessions, Yjs awareness | +| Object storage | MinIO (self-hosted S3) | Exports, attachments, soberanía | +| Realtime | Yjs + y-websocket | CRDT estándar para colaboración | +| Auth | Better-Auth | Self-hosted, moderno, RBAC nativo | +| API Gateway | Caddy | TLS automático, reverse proxy simple | +| Containerización | Docker + Docker Compose | Suficiente para VPS única | +| Observabilidad | OpenTelemetry → Grafana stack | Cuando llegue el momento | +| i18n | i18next + react-i18next | Estándar de facto | +| E2E testing | Playwright | Estándar moderno | + +### 6.4 Arquitectura de carpetas (MVP) + +``` +process-cost-platform/ +├── README.md +├── ARCHITECTURE.md ← este documento +├── package.json +├── tsconfig.json +├── vite.config.ts +├── tailwind.config.ts +├── index.html +├── public/ +│ └── sample-processes/ ← BPMNs de ejemplo para demos +├── src/ +│ ├── main.tsx ← entry point +│ ├── App.tsx ← shell de la app +│ ├── router.tsx ← TanStack Router +│ │ +│ ├── domain/ ← modelo puro, sin UI +│ │ ├── types.ts ← Process, Activity, Resource, Simulation +│ │ ├── simulation.ts ← motor de cálculo (TypeScript puro) +│ │ ├── bpmn-parser.ts ← extracción de nodos de XML BPMN +│ │ └── schemas.ts ← validación Zod +│ │ +│ ├── store/ ← Zustand stores +│ │ ├── process-store.ts +│ │ ├── resources-store.ts +│ │ └── simulation-store.ts +│ │ +│ ├── persistence/ ← IndexedDB / Dexie +│ │ ├── db.ts ← schema Dexie +│ │ └── repositories.ts ← funciones CRUD +│ │ +│ ├── features/ ← organización por feature +│ │ ├── import/ +│ │ │ ├── ImportDropzone.tsx +│ │ │ └── useImportBpmn.ts +│ │ ├── workspace/ +│ │ │ ├── WorkspacePage.tsx +│ │ │ ├── BpmnCanvas.tsx ← integra bpmn-js +│ │ │ ├── ActivityPanel.tsx ← edición de atributos +│ │ │ ├── ResourcesPanel.tsx +│ │ │ └── GlobalSettingsPanel.tsx +│ │ ├── simulation/ +│ │ │ └── useSimulate.ts ← hook que orquesta el cálculo +│ │ └── report/ +│ │ ├── ReportPage.tsx +│ │ ├── HeatmapCanvas.tsx ← bpmn-js + D3 overlay +│ │ ├── KpiHeader.tsx +│ │ ├── TopActivitiesTable.tsx +│ │ ├── CostByActivityChart.tsx +│ │ ├── CostCompositionChart.tsx +│ │ ├── CostByResourceChart.tsx +│ │ └── ExportControls.tsx +│ │ +│ ├── components/ ← componentes compartidos (shadcn + custom) +│ │ ├── ui/ ← shadcn copy-pastes +│ │ └── ... +│ │ +│ ├── lib/ ← utilidades transversales +│ │ ├── format.ts ← formateo de moneda, números +│ │ ├── colors.ts ← lógica de heatmap +│ │ ├── pdf-export.ts ← jsPDF + html2canvas +│ │ └── csv-export.ts ← papaparse +│ │ +│ └── styles/ +│ └── globals.css +└── tests/ + ├── domain/ + │ └── simulation.test.ts ← tests del motor + └── e2e/ ← post-MVP +``` + +### 6.5 Modelo conceptual del motor de simulación (MVP) + +El motor de simulación del MVP es **determinístico y agregado**. NO es discrete event simulation. Su contrato: + +```typescript +interface SimulationInput { + processXml: string; + activities: Map; + resources: Resource[]; + globalSettings: { + currency: string; + overheadPercentage: number; // % sobre costos directos + }; +} + +interface ActivityConfig { + activityId: string; + directCostFixed: number; // costo fijo por ejecución + executionTimeMinutes: number; // tiempo estimado + assignedResources: Array<{ + resourceId: string; + utilizationPercent: number; // % del tiempo de la actividad + }>; + probabilityOfExecution: number; // 0..1, default 1 + // Para gateways: probabilidad por flujo saliente +} + +interface SimulationResult { + totalCost: number; + totalDirectCost: number; + totalIndirectCost: number; + totalTimeMinutes: number; + perActivity: Array<{ + activityId: string; + activityName: string; + expectedDirectCost: number; + expectedIndirectCost: number; + expectedTotalCost: number; + percentOfTotal: number; + expectedExecutions: number; // por la prob. acumulada del camino + resourceCostBreakdown: Array<{ resourceId: string; cost: number }>; + }>; + perResource: Array<{ + resourceId: string; + totalCost: number; + totalMinutesUsed: number; + }>; + warnings: string[]; // ej. "el proceso tiene un loop sin probabilidad definida" +} +``` + +**Algoritmo (alto nivel)**: + +1. Parsear el BPMN XML → grafo dirigido de nodos y flujos. +2. Calcular probabilidad acumulada de ejecución para cada actividad mediante recorrido desde Start Events, propagando probabilidades por gateways. +3. Detectar y advertir loops (en MVP los tratamos como ejecución única; manejo riguroso en V1.0). +4. Para cada actividad: `costoEsperado = (costoDirectoFijo + Σ(recursos.costo/hora × utilización × tiempo/60)) × probabilidadAcumulada`. +5. Costo indirecto = `costoDirectoTotal × overheadPercentage`. +6. Agregar totales y rankings. + +### 6.6 Integración bpmn-js + heatmap + +`bpmn-js` expone: +- Un `viewer` (read-only) ideal para MVP. +- API para añadir overlays/colores a elementos por ID. +- Eventos de click sobre elementos del diagrama. + +El heatmap se implementa así: +1. Cargar BPMN con `Viewer.importXML(xml)`. +2. Tras la simulación, iterar resultados y para cada actividad: + - Calcular color según modo (relativo/absoluto). + - Aplicar fill al elemento SVG via `canvas.addMarker(id, className)` o manipulación directa del SVG hijo del nodo. +3. Registrar handler `element.click` para drill-down. +4. Mostrar tooltip con detalle al hover. + +### 6.7 Deployment del MVP + +**Esta noche**: +1. `npm run build` → genera `dist/` estático. +2. Subir a Cloudflare Pages (drag & drop o GitHub integration). +3. Dominio: `tu-subdominio.pages.dev` o dominio propio. + +**Costo**: $0. + +--- + +## 7. Modelo de Dominio + +### 7.1 Entidades núcleo (MVP) + +**Distinción fundamental de capas:** +- `ProcessGraph` (en memoria, reconstruido del XML cada vez): TODOS los nodos del diagrama incluidos gateways, eventos, flujos. Vive solo en el parser/motor, no se persiste. +- `Activity` (persistido en IndexedDB): solo `task` y `subprocess` — nodos con trabajo costeable. +- `GatewayConfig` (persistido en IndexedDB): probabilidades por flujo saliente de gateways divergentes (XOR/OR). Los gateways convergentes (merge) no tienen config — el motor suma probabilidades entrantes. +- El heatmap **solo colorea nodos con Activity**. Los gateways conservan estilo BPMN neutro. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Process │ +├─────────────────────────────────────────────────────────────┤ +│ + id: UUID │ +│ + name: string │ +│ + clientName: string │ +│ + bpmnXml: string (raw XML) │ +│ + currency: string (ISO 4217) │ +│ + overheadPercentage: number (0..1) │ +│ + createdAt, updatedAt │ +└─────────────────────────────────────────────────────────────┘ + │ 1 │ 1 + │ │ + │ N │ N +┌─────────────────────────┐ ┌──────────────────────────────┐ +│ Activity │ │ GatewayConfig │ +├─────────────────────────┤ ├──────────────────────────────┤ +│ + id: UUID │ │ + id: UUID │ +│ + processId: UUID (FK) │ │ + processId: UUID (FK) │ +│ + bpmnElementId: string │ │ + bpmnElementId: string │ +│ (FK conceptual al │ │ (id del gateway en XML) │ +│ nodo en el XML) │ │ + gatewayType: │ +│ + name: string │ │ 'exclusive'|'inclusive' │ +│ + type: │ │ |'parallel' │ +│ 'task'|'subprocess' │ │ + branches: Array<{ │ +│ + directCostFixed: │ │ flowId: string, │ +│ number │ │ probability: number │ +│ + executionTimeMinutes: │ │ }> │ +│ number │ │ (paralelo: todas 1.0, │ +└─────────────────────────┘ │ exclusivo: suma = 1.0) │ + │ N └──────────────────────────────┘ + │ + │ M +┌─────────────────────────────────────────────────────────────┐ +│ ActivityResourceAssignment │ +├─────────────────────────────────────────────────────────────┤ +│ + activityId: UUID (FK) │ +│ + resourceId: UUID (FK) │ +│ + utilizationPercent: number (0..1) │ +└─────────────────────────────────────────────────────────────┘ + │ N + │ + │ 1 +┌─────────────────────────────────────────────────────────────┐ +│ Resource │ +├─────────────────────────────────────────────────────────────┤ +│ + id: UUID │ +│ + name: string │ +│ + type: 'role' | 'person' | 'system' | 'equipment' │ +│ | 'supply' │ +│ + costPerHour: number │ +│ + currency: string │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ Simulation │ +├─────────────────────────────────────────────────────────────┤ +│ + id: UUID │ +│ + processId: UUID (FK) │ +│ + executedAt: timestamp │ +│ + resultJson: SimulationResult (snapshot) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 7.2 Modelo extendido (V1.0) + +Añade: `Tenant`, `User`, `Role`, `OrganizationUnit` (jerárquica recursiva), `ProcessOwnership` (RACI), `Risk`, `RiskProcessLink`, `ProcessVersion`, `Comment`, `Notification`, `Template`. + +--- + +## 8. Decisiones Arquitectónicas (ADRs) + +### ADR-001 — MVP client-side sin backend +- **Contexto**: tiempo de entrega = una noche, presupuesto cero. +- **Decisión**: el MVP corre 100% en navegador con IndexedDB. +- **Consecuencias**: ✅ Cero costo, cero infraestructura. ❌ Sin colaboración, sin sync entre dispositivos. +- **Cuándo revisar**: al inicio de Fase 1. + +### ADR-002 — bpmn-js como motor de canvas +- **Contexto**: existen 3 opciones — bpmn-js, librería custom, render SVG manual. +- **Decisión**: usar bpmn-js (Camunda, MIT). +- **Consecuencias**: ✅ Estándar de la industria, mantenido, soporta BPMN 2.0 XML estándar end-to-end. ❌ Atado a su modelo; extensiones de paleta requerirán plugins propios. +- **Alternativas descartadas**: librería propia (reinventar la rueda, costo prohibitivo). + +### ADR-003 — Persistencia con BPMN XML estándar como fuente de verdad +- **Contexto**: hay que decidir si guardamos BPMN XML, JSON propio, o ambos. +- **Decisión**: BPMN XML 2.0 estándar como fuente de verdad. Los costos/tiempos/recursos se guardan en estructuras paralelas vinculadas por `bpmnElementId`. +- **Consecuencias**: ✅ Interoperabilidad con cualquier herramienta BPMN. ❌ Costos no viajan dentro del XML (no es estándar BPMN); requiere export combinado en el futuro. + +### ADR-004 — React + Vite + TypeScript +- **Decisión**: React 18 + Vite + TS estricto. +- **Justificación**: ecosistema más grande, Claude Code óptimo en React, contratable en LATAM. +- **Alternativas descartadas**: Vue (menos ecosistema BPMN), Svelte (menor masa crítica), Next.js (SSR innecesario para MVP). + +### ADR-005 — Zustand para estado global +- **Decisión**: Zustand sobre Redux/Jotai/Context. +- **Justificación**: simplicidad, sin boilerplate, suficiente para alcance MVP y V1.0. + +### ADR-006 — NestJS para backend (V1.0) +- **Decisión**: NestJS como framework backend. +- **Justificación**: modularidad por design (encaja con monolito modular), DI nativo, tipado fuerte, ecosistema empresarial. +- **Alternativas descartadas**: Fastify puro (menos estructura), Express (legacy), Python/FastAPI (cambio de lenguaje innecesario). + +### ADR-007 — PostgreSQL + Prisma (V1.0) +- **Decisión**: Postgres como BD principal, Prisma como ORM. +- **Justificación**: relaciones jerárquicas + JSONB para BPMN + robustez probada + tipado e2e con Prisma. + +### ADR-008 — Yjs + y-websocket para colaboración (V1.0) +- **Decisión**: CRDTs vía Yjs. +- **Justificación**: estándar en editores colaborativos modernos, sin necesidad de transformación operacional, escala bien. + +### ADR-009 — Monolito modular antes que microservicios +- **Decisión**: NestJS monolito con módulos por dominio. +- **Justificación**: equipo pequeño, complejidad innecesaria de microservicios, posibilidad de extraer servicios después si hace falta. + +### ADR-010 — Heatmap configurable con default "costo relativo" +- **Decisión**: 3 modos, default relativo. +- **Justificación**: el modo relativo comunica mejor "dónde duele" — el caso de uso primario del consultor. + +### ADR-011 — Deploy en Cloudflare Pages para MVP, VPS para V1.0 +- **Decisión**: Cloudflare Pages mientras sea estático; migración a VPS al introducir backend. +- **Justificación**: $0 al inicio, latencia global, sin operación. Migrar cuando haya backend. + +--- + +## 9. Atributos de Calidad + +### 9.1 Performance + +| Métrica | MVP | V1.0 | +|---|---|---| +| Tiempo carga inicial | < 2s | < 3s en 3G | +| Tamaño bundle JS inicial | < 500KB gzip | < 700KB gzip | +| Cálculo de simulación (50 nodos) | < 100ms | < 500ms | +| Render mapa de calor | < 200ms | < 200ms | +| Export PDF | < 5s | < 5s | + +### 9.2 Mantenibilidad + +- **TypeScript strict** activado. +- **ESLint + Prettier** configurados. +- **Convención de carpetas por feature**, no por tipo de archivo. +- **Dominio puro separado** de UI (en `/src/domain`) — testeable sin React. +- **Sin lógica de negocio en componentes** — siempre en hooks o domain. + +### 9.3 Seguridad (V1.0) + +- TLS obligatorio (Cloudflare en MVP, Caddy en VPS). +- Auth con tokens cortos + refresh. +- RBAC: roles `owner`, `admin`, `editor`, `viewer`. +- Sanitización del BPMN XML antes de parsear (evitar XXE). +- CSP estricto en producción. +- Backups cifrados (V1.0). + +### 9.4 Accesibilidad + +- WCAG 2.1 AA como objetivo V1.0. +- Contraste mínimo 4.5:1 en texto. +- Navegación por teclado en el flujo principal. +- Labels en todos los inputs. + +--- + +## 10. Riesgos y Mitigaciones + +| # | Riesgo | Probabilidad | Impacto | Mitigación | +|---|---|---|---|---| +| R1 | El parser BPMN no maneja todos los XML que importe el consultor | Alta | Alto en MVP | Documentar elementos soportados; mostrar warnings claros; testear con BPMNs reales esta noche | +| R2 | El cálculo agregado es engañoso ante loops complejos | Media | Medio | Detectar loops y advertir; en V1.0 implementar Monte Carlo | +| R3 | El reporte exportado a PDF se ve mal por diferencias de render | Media | Alto en MVP | Diseñar el reporte pensando en print desde el día uno; testear export inmediatamente | +| R4 | El cliente del consultor cuestiona la metodología de costeo | Alta | Medio | Documentar fórmula visible en el reporte; permitir override de overhead | +| R5 | IndexedDB se llena o se pierde (modo incógnito, limpieza) | Media | Medio | Botón "Exportar workspace completo" como respaldo manual; en Fase 1 migrar a backend | +| R6 | Performance degrada con diagramas > 100 nodos | Baja en MVP | Bajo | MVP solo soporta hasta 50 nodos por requisito | +| R7 | Dependencia de bpmn-js a futuro (lock-in) | Baja | Bajo | bpmn-js es MIT y mantenido por Camunda; standard BPMN XML protege salida | +| R8 | El usuario espera edición y se frustra con read-only | Alta | Alto | Comunicar claramente que es importación + costeo; Fase 2 trae edición | +| R9 | Caída de motivación / scope creep | Alta | Crítico | Este documento existe para anclar el alcance. Defenderlo. | + +--- + +## 11. Anexos + +### 11.1 Comandos de arranque para Claude Code (MVP) + +```bash +# Inicialización +npm create vite@latest process-cost-platform -- --template react-ts +cd process-cost-platform +npm install + +# Core libs +npm install bpmn-js zustand dexie @tanstack/react-router zod +npm install d3 echarts echarts-for-react +npm install jspdf html2canvas papaparse +npm install lucide-react + +# Tailwind + shadcn +npm install -D tailwindcss postcss autoprefixer +npx tailwindcss init -p +npx shadcn@latest init + +# Dev tooling +npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom +npm install -D eslint prettier +``` + +### 11.2 BPMN XML de prueba sugerido + +Para testing inicial, usar el BPMN público de ejemplo de bpmn.io: https://github.com/bpmn-io/bpmn-js-examples + +### 11.3 Recursos de referencia + +- **bpmn-js docs**: https://bpmn.io/toolkit/bpmn-js/ +- **BPMN 2.0 spec**: https://www.omg.org/spec/BPMN/2.0/ +- **shadcn/ui**: https://ui.shadcn.com/ +- **TanStack Router**: https://tanstack.com/router +- **Zustand**: https://zustand-demo.pmnd.rs/ +- **Dexie.js**: https://dexie.org/ + +### 11.4 Definition of Done — MVP "Noche Cero" + +- [ ] El usuario puede importar un archivo `.bpmn` por drag & drop +- [ ] El diagrama se renderiza correctamente en el canvas +- [ ] El usuario puede seleccionar cualquier actividad y editar sus atributos +- [ ] El usuario puede crear, editar y eliminar recursos +- [ ] El usuario puede configurar moneda y overhead global +- [ ] Al hacer click en "Simular", aparece un reporte +- [ ] El reporte muestra mapa de calor sobre el diagrama +- [ ] El reporte muestra KPIs, ranking, gráficos +- [ ] El usuario puede exportar el reporte a PDF y CSV +- [ ] El estado persiste al recargar la página (IndexedDB) +- [ ] El PDF exportado es presentable a un cliente real +- [ ] Deploy público accesible (Cloudflare Pages) + +--- + +> **Nota final del arquitecto**: +> +> Este documento es un contrato contigo mismo. El mayor riesgo del proyecto no es técnico, es la dispersión. Cada vez que sientas la tentación de agregar algo al MVP "porque sería genial", vuelve a la sección 4.3 y recuerda lo que dejaste fuera **por diseño**. +> +> El MVP no demuestra todas las capacidades de la plataforma. Demuestra **una sola cosa**: que un consultor puede transformar un BPMN en un reporte de costos visualmente deslumbrante en minutos. Si esta noche logras eso, ganaste. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..85c4f2a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,173 @@ +# CLAUDE.md — Contexto del Proyecto + +> Este archivo es leído automáticamente por Claude Code en cada sesión. +> Define el "norte" del proyecto y las reglas no negociables. + +--- + +## Identidad del Proyecto + +**Nombre de trabajo**: Process Cost Platform (nombre comercial por definir) + +**Una frase**: Plataforma web para que consultores de procesos transformen un archivo BPMN 2.0 en un reporte visual deslumbrante de costos operativos en minutos. + +**Mantra**: *Los procesos de negocio son activos estratégicos de las empresas y un diferencial competitivo.* + +--- + +## Estado Actual + +- **Fase**: 0 — MVP "Noche Cero" +- **Documento maestro**: `ARCHITECTURE.md` (raíz del repo) +- **Fecha objetivo MVP**: una noche +- **Deploy objetivo**: Cloudflare Pages (estático, gratis) + +--- + +## Reglas No Negociables del MVP + +### ✅ Lo que SÍ hacemos esta noche + +1. Importar archivo `.bpmn` por drag & drop +2. Renderizar el diagrama en read-only con bpmn-js +3. Editar atributos de actividades (costo, tiempo, recursos asignados) +4. CRUD de recursos (rol/persona/sistema/equipo/insumo + costo/hora) +5. Configuración global (moneda, overhead %, nombre proceso, cliente) +6. Motor de simulación determinístico agregado +7. Reporte visual con mapa de calor sobre el diagrama +8. Gráficos: KPIs, top actividades, costo por actividad, composición, costo por recurso +9. Export a PDF (presentable a cliente) y CSV +10. Persistencia en IndexedDB (Dexie) + +### ❌ Lo que NO hacemos esta noche + +Login · Multi-tenant · Backend · Colaboración · Edición del BPMN · Versionado · Distribuciones probabilísticas · Riesgos · Gobernanza/RACI · Jerarquía organizacional · IA · Elementos RPA/Agente custom · Tutoriales · Marketplace · i18n · PWA installable · Modo oscuro + +**Si dudás si una feature entra: NO ENTRA.** Defendé el alcance. + +--- + +## Stack Obligatorio (MVP) + +| Capa | Tecnología | +|---|---| +| Lenguaje | TypeScript estricto | +| Framework | React 18 + Vite | +| Routing | TanStack Router | +| Estado | Zustand | +| Canvas BPMN | bpmn-js (Viewer, no Modeler) | +| UI | shadcn/ui sobre Tailwind CSS | +| Iconos | Lucide React | +| Gráficos | Apache ECharts (vía echarts-for-react) | +| Persistencia | IndexedDB vía Dexie | +| Validación | Zod | +| Export PDF | jsPDF + html2canvas | +| Export CSV | papaparse | +| Testing | Vitest | +| Deploy | Cloudflare Pages | + +**No introduzcas librerías fuera de esta lista sin justificación explícita.** + +--- + +## Arquitectura de Carpetas (obligatoria) + +``` +src/ +├── main.tsx +├── App.tsx +├── router.tsx +├── domain/ ← lógica de negocio pura, sin React, testeable +│ ├── types.ts +│ ├── simulation.ts ← motor de simulación +│ ├── bpmn-parser.ts +│ └── schemas.ts ← Zod schemas +├── store/ ← Zustand stores +├── persistence/ ← Dexie / IndexedDB +├── features/ ← organizado por feature, no por tipo +│ ├── import/ +│ ├── workspace/ +│ ├── simulation/ +│ └── report/ +├── components/ui/ ← shadcn copy-pastes +├── lib/ ← utilidades transversales (format, colors, pdf, csv) +└── styles/ +``` + +**Reglas de dependencia**: +- `domain/` NO importa de React, ni de `store/`, ni de `features/`. Es puro. +- `features/` puede importar de `domain/`, `store/`, `persistence/`, `lib/`, `components/`. +- `store/` puede importar de `domain/` y `persistence/`. Nada más. + +--- + +## Principios de Código + +1. **TypeScript strict** activado. Nada de `any` salvo en bordes con librerías sin tipos. +2. **Dominio puro**: el motor de simulación es una función pura testeable sin DOM. +3. **Componentes tontos, hooks inteligentes**: lógica en custom hooks, JSX limpio. +4. **Sin lógica de negocio en componentes**. +5. **Convenciones de naming**: + - Componentes: `PascalCase.tsx` + - Hooks: `useCamelCase.ts` + - Utilidades: `kebab-case.ts` + - Tipos: `PascalCase`, interfaces con prefijo `I` PROHIBIDO +6. **Comentarios en español**, código (variables, funciones) en inglés. +7. **Errores con mensajes accionables** para el usuario, nunca traces crudos en UI. + +--- + +## Diferencial Visual (CRÍTICO) + +El 60% del valor del MVP está en **el reporte final**. Si el reporte se ve mediocre, el MVP fracasa aunque el resto funcione. Reglas: + +- Tipografía: **Inter** para UI, **JetBrains Mono** para números/datos +- Paleta heatmap: verde `#10b981` → amarillo `#f59e0b` → rojo `#ef4444` +- Cifras de dinero siempre formateadas con separadores y símbolo de moneda +- El export PDF debe verse igual o mejor que la pantalla +- Cada gráfico tiene título, leyenda clara y formato de moneda apropiado +- Densidad de información media-alta (audiencia: consultor profesional) + +--- + +## Definition of Done — Cada Feature + +Antes de marcar una tarea como terminada: + +- [ ] Funciona en el happy path +- [ ] Maneja el caso de datos vacíos sin crashear +- [ ] Persiste en IndexedDB cuando corresponde +- [ ] Tiene estilo consistente con el resto de la app +- [ ] Si toca dominio: tiene al menos un test en Vitest + +--- + +## Comandos del Proyecto + +```bash +npm run dev # desarrollo +npm run build # producción +npm run test # tests con Vitest +npm run lint # eslint +npm run preview # previsualizar build +``` + +--- + +## Cómo Trabajar Conmigo (el Usuario) + +- Cuando termines una tarea grande, **resumí qué cambió y qué falta**. +- Si una decisión no está clara, **proponé 2-3 opciones** con pros/contras antes de codear. +- Si encontrás que algo del documento de arquitectura está mal o falta, **avisame antes de improvisar**. +- No agregues features no pedidas. Si algo se te ocurre, anotalo en `TODO.md`. +- Cuando completes algo, **mostrame los archivos creados/modificados**, no los pegues completos en el chat salvo que te lo pida. + +--- + +## Contexto del Fundador + +- Consultor de automatización de procesos basado en Paraguay +- Construye esto como herramienta consultiva propia + para vender a colegas LATAM +- Usa Claude Code Pro como único "equipo de desarrollo" +- Lo va a presentar a clientes reales en días, no meses +- Valora: claridad, robustez, pulido visual, mantenibilidad \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7affdc --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Process Cost Platform — MVP Fase 0 + +Plataforma web que transforma un archivo BPMN 2.0 en un reporte visual +de costos operativos, listo para presentar a clientes en minutos. +100% en el navegador — sin backend, sin registro, datos guardados localmente. + +## Cómo usarlo + +1. **Importá** tu archivo `.bpmn` arrastrándolo a la pantalla principal, + o cargá uno de los tres procesos de ejemplo incluidos. +2. **Configurá** los costos fijos y tiempos de cada actividad en el workspace. + Ajustá las probabilidades de los gateways XOR si las hay. +3. **Simulá** para generar el análisis agregado de costos. +4. **Exportá** el reporte en PDF (presentación para el cliente) o CSV (datos). + +## Stack + +| Capa | Tecnología | +|---|---| +| Framework | React 19 + Vite 8 | +| Routing | TanStack Router | +| Estado | Zustand 5 | +| Canvas BPMN | bpmn-js 18 (Viewer) | +| UI | shadcn/ui + Tailwind CSS | +| Gráficos | Apache ECharts | +| Persistencia | IndexedDB vía Dexie 4 | +| Export PDF | jsPDF + jspdf-autotable | +| Export CSV | papaparse | +| Testing | Vitest + Playwright | +| Deploy | Cloudflare Pages | + +## Scripts + +```bash +npm run dev # Servidor de desarrollo (localhost:5173) +npm run build # Build de producción en dist/ +npm run preview # Preview del build en localhost:4173 +npm run test # Tests unitarios con Vitest +npx playwright test # Tests E2E con Playwright +``` + +## Estado + +**MVP Fase 0** — funcionalidad completa para uso consultivo básico. +Las features adicionales (multi-tenant, IA, versionado, etc.) +están documentadas en `TODO.md` para la Fase 1. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..acf70e3 --- /dev/null +++ b/TODO.md @@ -0,0 +1,33 @@ +# TODO — Process Cost Platform · Fase 1 + +> Features descartadas del MVP (scope defendido) para no arriesgar la fecha. +> Ordenadas por impacto estimado. + +## Alta prioridad + +- [ ] **Múltiples simulaciones**: historial de simulaciones por proceso con comparación de escenarios +- [ ] **Recursos con costo por hora**: link actividad ↔ recurso (rol, persona, sistema) con cálculo automático +- [ ] **Edición del nombre del proceso y cliente** desde el workspace (sin ir a configuración global) +- [ ] **Gateway OR inclusivo nativo**: actualmente se simula como XOR; implementar propagación OR real +- [ ] **Distribuciones de probabilidad**: gamma/triangular por actividad en lugar de valores fijos + +## Media prioridad + +- [ ] **Export a Excel** (.xlsx) con múltiples hojas (resumen + por actividad + metodología) +- [ ] **Modo oscuro** (Dark mode) +- [ ] **i18n**: inglés + español configurable +- [ ] **PWA installable**: manifest + service worker para uso offline +- [ ] **Zoom/pan del heatmap** desde el reporte (ahora es solo visualización estática) +- [ ] **Detalle de recursos por actividad** en el PDF (tabla de costos por recurso asignado) +- [ ] **Importación por URL**: cargar BPMN desde enlace directo (GitHub, Confluence, etc.) + +## Baja prioridad / Fase 2 + +- [ ] **Login y multi-tenant**: persistencia en la nube, colaboración +- [ ] **Versionado de procesos**: historial de cambios con diff visual +- [ ] **Gobernanza/RACI**: asignación de responsables por actividad +- [ ] **Jerarquía organizacional**: integración con orgchart para asignar costos por área +- [ ] **IA / asistente**: sugerencias de optimización, benchmarking automático +- [ ] **Marketplace de procesos**: biblioteca de BPMNs precostos por industria +- [ ] **Elementos RPA y agentes**: soporte para costos de bots y workflows automatizados +- [ ] **Tutoriales interactivos**: onboarding guiado para usuarios nuevos diff --git a/index.html b/index.html new file mode 100644 index 0000000..42beec3 --- /dev/null +++ b/index.html @@ -0,0 +1,26 @@ + + + + + + + + Process Cost Platform — Cuantificá tus procesos + + + + + + + + + + + + + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d83e481 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8584 @@ +{ + "name": "process-cost-platform", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "process-cost-platform", + "version": "0.0.0", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toast": "^1.2.15", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-router": "^1.169.2", + "@tanstack/router-devtools": "^1.166.13", + "@types/html2canvas": "^0.5.35", + "bpmn-js": "^18.16.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "d3": "^7.9.0", + "dexie": "^4.4.2", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.6", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.7", + "lucide-react": "^1.14.0", + "papaparse": "^5.5.3", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "tailwind-merge": "^3.6.0", + "uuid": "^14.0.0", + "zod": "^4.4.3", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.60.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/d3": "^7.4.3", + "@types/node": "^24.12.3", + "@types/papaparse": "^5.5.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/uuid": "^10.0.0", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "fake-indexeddb": "^6.2.5", + "globals": "^17.6.0", + "jsdom": "^29.1.1", + "pdf-lib": "^1.17.1", + "pdf-parse": "^2.4.5", + "postcss": "^8.5.14", + "prettier": "^3.8.3", + "sharp": "^0.34.5", + "tailwindcss": "^3.4.19", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12", + "vitest": "^4.1.6" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "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.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bpmn-io/diagram-js-ui": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bpmn-io/diagram-js-ui/-/diagram-js-ui-0.2.3.tgz", + "integrity": "sha512-OGyjZKvGK8tHSZ0l7RfeKhilGoOGtFDcoqSGYkX0uhFlo99OVZ9Jn1K7TJGzcE9BdKwvA5Y5kGqHEhdTxHvFfw==", + "license": "MIT", + "dependencies": { + "htm": "^3.1.1", + "preact": "^10.11.2" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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/canvas": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "dev": true, + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.129.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", + "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/standard-fonts/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, + "node_modules/@pdf-lib/upng/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", + "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", + "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", + "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", + "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", + "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", + "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", + "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", + "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", + "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", + "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", + "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", + "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", + "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", + "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/history": { + "version": "1.161.6", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.161.6.tgz", + "integrity": "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==", + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.169.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.169.2.tgz", + "integrity": "sha512-OJM7Kguc7ERnweaNRWsyWgIKcl3z23rD1B4jaxjzd9RGdnzpt2HfrWa9rggbT0Hfzhfo4D2ZmsfoTme035tniQ==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.161.6", + "@tanstack/react-store": "^0.9.3", + "@tanstack/router-core": "1.169.2", + "isbot": "^5.1.22" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-router-devtools": { + "version": "1.166.13", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.166.13.tgz", + "integrity": "sha512-6yKRFFJrEEOiGp5RAAuGCYsl81M4XAhJmLcu9PKj+HZle4A3dsP60lwHoqQYWHMK9nKKFkdXR+D8qxzxqtQbEA==", + "license": "MIT", + "dependencies": { + "@tanstack/router-devtools-core": "1.167.3" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.168.15", + "@tanstack/router-core": "^1.168.11", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + }, + "peerDependenciesMeta": { + "@tanstack/router-core": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz", + "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.9.3", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-core": { + "version": "1.169.2", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.169.2.tgz", + "integrity": "sha512-5sm0DJF1A7Mz+9gy4Gz/lLovNailK3yot4vYvz9MkBUPw26uLnhQiR8hSCYxucjE0wD6Mdlc5l+Z0/XTlZ7xHw==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.161.6", + "cookie-es": "^3.0.0", + "seroval": "^1.5.4", + "seroval-plugins": "^1.5.4" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-devtools": { + "version": "1.166.13", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools/-/router-devtools-1.166.13.tgz", + "integrity": "sha512-Qs8gkyI7m+eAxG3VcIOHuTSsUfA5ZxZcOa99ZyIIIJFxW6hy1k+m2s1J0ZYN1SNlip8P2ofd/MHiqmR1IWipMg==", + "license": "MIT", + "dependencies": { + "@tanstack/react-router-devtools": "1.166.13", + "clsx": "^2.1.1", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.168.15", + "csstype": "^3.0.10", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + }, + "peerDependenciesMeta": { + "csstype": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-devtools-core": { + "version": "1.167.3", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.167.3.tgz", + "integrity": "sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/router-core": "^1.168.11", + "csstype": "^3.0.10" + }, + "peerDependenciesMeta": { + "csstype": { + "optional": true + } + } + }, + "node_modules/@tanstack/store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", + "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "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/dom/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/@testing-library/dom/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/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "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/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/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/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "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/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "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/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/html2canvas": { + "version": "0.5.35", + "resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-0.5.35.tgz", + "integrity": "sha512-1A2dtWZbOIZ+rUK8jmAx1We/EiNV+5vScpphU3AF14Vby6COIazi/9StosrvlVCqlQegRhsEgZf7QYOuWbwuuA==", + "license": "MIT", + "dependencies": { + "@types/jquery": "*" + } + }, + "node_modules/@types/jquery": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-4.0.0.tgz", + "integrity": "sha512-Z+to+A2VkaHq1DfI2oSwsoCdhCHMpTSgjWzNcbNlRGYzksDBpPUgEcAL+RQjOBJRaLoEAOHXxqDGBVP+BblBwg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/papaparse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.3", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bpmn-js": { + "version": "18.16.1", + "resolved": "https://registry.npmjs.org/bpmn-js/-/bpmn-js-18.16.1.tgz", + "integrity": "sha512-qdJp/1JbKY3JtqB2L7LF4VEJtVlbl9AGkvyUjzJlBJ8jURNNTx+22OdrZMVNPxaGOjW+nuHK38ObFCmMCp/Kcg==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "bpmn-moddle": "^10.0.0", + "diagram-js": "^15.14.0", + "diagram-js-direct-editing": "^3.3.0", + "ids": "^3.0.2", + "inherits-browser": "^0.1.0", + "min-dash": "^5.0.0", + "min-dom": "^5.3.0", + "tiny-svg": "^4.1.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bpmn-moddle": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/bpmn-moddle/-/bpmn-moddle-10.0.0.tgz", + "integrity": "sha512-vXePD5jkatcILmM3zwJG/m6IIHIghTGB7WvgcdEraEw8E8VdJHrTgrvBUhbzqaXJpnsGQz15QS936xeBY6l9aA==", + "license": "MIT", + "dependencies": { + "min-dash": "^5.0.0", + "moddle": "^8.0.0", + "moddle-xml": "^12.0.0" + }, + "engines": { + "node": ">= 20.12" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "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/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "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-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dexie": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.2.tgz", + "integrity": "sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw==", + "license": "Apache-2.0" + }, + "node_modules/diagram-js": { + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/diagram-js/-/diagram-js-15.14.0.tgz", + "integrity": "sha512-xXIKBgK9qC5YIfdUs0Qx+rSlz3DNGMtYxx+pRK+U8eVBD184eZnLCu6KVzaZXXyyC5YRirQayorCVI55Ulhzvg==", + "license": "MIT", + "dependencies": { + "@bpmn-io/diagram-js-ui": "^0.2.3", + "clsx": "^2.1.1", + "didi": "^11.0.0", + "inherits-browser": "^0.1.0", + "min-dash": "^5.0.0", + "min-dom": "^5.3.0", + "object-refs": "^0.4.0", + "path-intersection": "^4.1.0", + "tiny-svg": "^4.1.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/diagram-js-direct-editing": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/diagram-js-direct-editing/-/diagram-js-direct-editing-3.3.0.tgz", + "integrity": "sha512-EjXYb35J3qBU8lLz5U81hn7wNykVmF7U5DXZ7BvPok2IX7rmPz+ZyaI5AEMiqaC6lpSnHqPxFcPgKEiJcAiv5w==", + "license": "MIT", + "dependencies": { + "min-dash": "^5.0.0", + "min-dom": "^5.2.0" + }, + "engines": { + "node": "*" + }, + "peerDependencies": { + "diagram-js": "*" + } + }, + "node_modules/didi": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/didi/-/didi-11.0.0.tgz", + "integrity": "sha512-PzCfRzQttvFpVcYMbSF7h8EsWjeJpVjWH4qDhB5LkMi1ILvHq4Ob0vhM2wLFziPkbUBi+PAo7ODbe2sacR7nJQ==", + "license": "MIT", + "engines": { + "node": ">= 20.12" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/domify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domify/-/domify-3.0.0.tgz", + "integrity": "sha512-bs2yO68JDFOm6rKv8f0EnrM2cENduhRkpqOtt/s5l5JBA/eqGBZCzLPmdYoHtJ6utgLGgcBajFsEQbl12pT0lQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dompurify": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.3.tgz", + "integrity": "sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.6.tgz", + "integrity": "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.354", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.354.tgz", + "integrity": "sha512-JaBHwWcfIdmSAfWM5l3uwjGd431j8YEMikZ+K/2nXVuBqJKyZ0f+2h4n4JY5AyNiZmnY9qQr2RU3v9DxDmHMNg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "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-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", + "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fake-indexeddb": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz", + "integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "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==", + "dev": true, + "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-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/goober": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.19.tgz", + "integrity": "sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "license": "Apache-2.0" + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ids": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/ids/-/ids-3.0.2.tgz", + "integrity": "sha512-t6YJP4mdC+GHF96Nbis/4FEANhP/8VWmYMvUuYpXvSdrhg5hpIVbq2XZlOA3UWTbtdwPCi0q7jEXOdHkAnqOnw==", + "license": "MIT", + "engines": { + "node": ">= 20.12" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits-browser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/inherits-browser/-/inherits-browser-0.1.0.tgz", + "integrity": "sha512-CJHHvW3jQ6q7lzsXPpapLdMx5hDpSF3FSh45pwsj6bKxJJ8Nl8v43i5yXnr3BdfOimGHKyniewQtnAIp3vyJJw==", + "license": "ISC" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbot": { + "version": "5.1.40", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.40.tgz", + "integrity": "sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "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/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.7.tgz", + "integrity": "sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==", + "license": "MIT", + "peerDependencies": { + "jspdf": "^2 || ^3 || ^4" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/lucide-react": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/min-dash": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-5.0.0.tgz", + "integrity": "sha512-EGuoBnVL7/Fnv2sqakpX5WGmZehZ3YMmLayT7sM8E9DRU74kkeyMg4Rik1lsOkR2GbFNeBca4/L+UfU6gF0Edw==", + "license": "MIT" + }, + "node_modules/min-dom": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/min-dom/-/min-dom-5.3.0.tgz", + "integrity": "sha512-0w5FEBgPAyHhmFojW3zxd7we3D+m5XYS3E/06OyvxmbHJoiQVa4Nagj6RWvoAKYRw5xth6cP5TMePc5cR1M9hA==", + "license": "MIT", + "dependencies": { + "domify": "^3.0.0", + "min-dash": "^5.0.0" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/moddle": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/moddle/-/moddle-8.1.0.tgz", + "integrity": "sha512-dBddc1CNuZHgro8nQWwfPZ2BkyLWdnxoNpPu9d+XKPN96DAiiBOeBw527ft++ebDuFez5PMdaR3pgUgoOaUGrA==", + "license": "MIT", + "dependencies": { + "min-dash": "^5.0.0" + } + }, + "node_modules/moddle-xml": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/moddle-xml/-/moddle-xml-12.0.0.tgz", + "integrity": "sha512-NJc2+sCe4tvuGlaUBcoZcYf6j9f+z+qxHOyGm/LB3ZrlJXVPPHoBTg/KXgDRCufdBJhJ3AheFs3QU/abABNzRg==", + "license": "MIT", + "dependencies": { + "min-dash": "^5.0.0", + "saxen": "^11.0.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "moddle": ">= 6.2.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-refs": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-refs/-/object-refs-0.4.0.tgz", + "integrity": "sha512-6kJqKWryKZmtte6QYvouas0/EIJKPI1/MMIuRsiBlNuhIMfqYTggzX2F1AJ2+cDs288xyi9GL7FyasHINR98BQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-intersection": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/path-intersection/-/path-intersection-4.1.0.tgz", + "integrity": "sha512-urUP6WvhnxbHPdHYl6L7Yrc6+1ny6uOFKPCzPxTSUSYGHG0o94RmI7SvMMaScNAM5RtTf08bg4skc6/kjfne3A==", + "license": "MIT", + "engines": { + "node": ">= 14.20" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/pdf-parse": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz", + "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@napi-rs/canvas": "0.1.80", + "pdfjs-dist": "5.4.296" + }, + "bin": { + "pdf-parse": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.16.0 <21 || >=22.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/mehmet-kozan" + } + }, + "node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/preact": { + "version": "10.29.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "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/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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "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-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/rolldown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", + "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.129.0", + "@rolldown/pluginutils": "1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0", + "@rolldown/binding-darwin-arm64": "1.0.0", + "@rolldown/binding-darwin-x64": "1.0.0", + "@rolldown/binding-freebsd-x64": "1.0.0", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", + "@rolldown/binding-linux-arm64-gnu": "1.0.0", + "@rolldown/binding-linux-arm64-musl": "1.0.0", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0", + "@rolldown/binding-linux-s390x-gnu": "1.0.0", + "@rolldown/binding-linux-x64-gnu": "1.0.0", + "@rolldown/binding-linux-x64-musl": "1.0.0", + "@rolldown/binding-openharmony-arm64": "1.0.0", + "@rolldown/binding-wasm32-wasi": "1.0.0", + "@rolldown/binding-win32-arm64-msvc": "1.0.0", + "@rolldown/binding-win32-x64-msvc": "1.0.0" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", + "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "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/saxen": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/saxen/-/saxen-11.0.2.tgz", + "integrity": "sha512-WDb4gqac8uiJzOdOdVpr9NWh9NrJMm7Brn5GX2Poj+mjE/QTXqYQENr8T/mom54dDDgbd3QjwTg23TRHYiWXRA==", + "license": "MIT", + "engines": { + "node": ">= 20.12" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "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/seroval": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz", + "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz", + "integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/size-sensor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz", + "integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==", + "license": "ISC" + }, + "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/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/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-svg": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tiny-svg/-/tiny-svg-4.1.4.tgz", + "integrity": "sha512-cBaEACCbouYrQc9RG+eTXnPYosX1Ijqty/I6DdXovwDd89Pwu4jcmpOR7BuFEF9YCcd7/AWwasE0207WMK7hdw==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "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": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.30" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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==", + "dev": true, + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vite": { + "version": "8.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", + "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.0", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "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": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/zustand": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", + "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..39cb160 --- /dev/null +++ b/package.json @@ -0,0 +1,81 @@ +{ + "name": "process-cost-platform", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toast": "^1.2.15", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-router": "^1.169.2", + "@tanstack/router-devtools": "^1.166.13", + "@types/html2canvas": "^0.5.35", + "bpmn-js": "^18.16.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "d3": "^7.9.0", + "dexie": "^4.4.2", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.6", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.7", + "lucide-react": "^1.14.0", + "papaparse": "^5.5.3", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "tailwind-merge": "^3.6.0", + "uuid": "^14.0.0", + "zod": "^4.4.3", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.60.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/d3": "^7.4.3", + "@types/node": "^24.12.3", + "@types/papaparse": "^5.5.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/uuid": "^10.0.0", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "fake-indexeddb": "^6.2.5", + "globals": "^17.6.0", + "jsdom": "^29.1.1", + "pdf-lib": "^1.17.1", + "pdf-parse": "^2.4.5", + "postcss": "^8.5.14", + "prettier": "^3.8.3", + "sharp": "^0.34.5", + "tailwindcss": "^3.4.19", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12", + "vitest": "^4.1.6" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..b0ea3a2 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 180_000, + use: { + baseURL: 'http://localhost:4173', + headless: true, + viewport: { width: 1440, height: 900 }, + }, + webServer: { + command: 'npm run preview', + url: 'http://localhost:4173', + reuseExistingServer: true, + timeout: 30_000, + }, +}) diff --git a/playwright.prod.config.ts b/playwright.prod.config.ts new file mode 100644 index 0000000..b66f424 --- /dev/null +++ b/playwright.prod.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './tests/e2e', + testMatch: '**/prod-smoke.spec.ts', + timeout: 180_000, + use: { + baseURL: 'https://process-cost-platform.pages.dev', + headless: true, + viewport: { width: 1440, height: 900 }, + }, +}) diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..bbc5747 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/sample-processes/accent-test.bpmn b/public/sample-processes/accent-test.bpmn new file mode 100644 index 0000000..d44bf50 --- /dev/null +++ b/public/sample-processes/accent-test.bpmn @@ -0,0 +1,86 @@ + + + + + + + flow1 + + + + flow1 + flow2 + + + + flow2 + flow3 + + + + flow3 + flow4 + + + + flow4 + flow5 + + + + flow5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/sample-processes/complex-with-loop.bpmn b/public/sample-processes/complex-with-loop.bpmn new file mode 100644 index 0000000..37dd5d2 --- /dev/null +++ b/public/sample-processes/complex-with-loop.bpmn @@ -0,0 +1,136 @@ + + + + + + + flow_start_inspeccion + + + + flow_start_inspeccion + flow_preparar_inspeccion + + + + flow_preparar_inspeccion + flow_correc_inspeccion + flow_inspeccion_gw + + + + + flow_inspeccion_gw + flow_gw_aprobado + flow_gw_obs_menores + flow_gw_rechazo + + + + flow_gw_aprobado + flow_documentar_entrega + + + + + flow_gw_obs_menores + flow_correc_inspeccion + + + + + flow_gw_rechazo + flow_rechazo_informe + + + + flow_rechazo_informe + flow_informe_merge + + + + flow_documentar_entrega + flow_entrega_certificado + + + + flow_entrega_certificado + flow_cert_merge + + + + + flow_cert_merge + flow_informe_merge + flow_merge_registro + + + + flow_merge_registro + flow_registro_end + + + + flow_registro_end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/sample-processes/medium-with-gateways.bpmn b/public/sample-processes/medium-with-gateways.bpmn new file mode 100644 index 0000000..6f9cc57 --- /dev/null +++ b/public/sample-processes/medium-with-gateways.bpmn @@ -0,0 +1,168 @@ + + + + + + + flow_start_recv + + + + flow_start_recv + flow_recv_valid + + + + flow_recv_valid + flow_valid_gw1 + + + + + flow_valid_gw1 + flow_gw1_completo + flow_gw1_incompleto + + + + flow_gw1_incompleto + flow_docs_join + + + + flow_gw1_completo + flow_score_parallel + + + + + flow_score_parallel + flow_para_bureau + flow_para_empleador + + + + flow_para_bureau + flow_bureau_join + + + + flow_para_empleador + flow_empleador_join + + + + + flow_bureau_join + flow_empleador_join + flow_join_analisis + + + + flow_join_analisis + flow_docs_join + flow_analisis_decision + + + + + flow_analisis_decision + flow_dec_aprobado + flow_dec_rechazado + + + + flow_dec_aprobado + flow_aprobar_desembolso + + + + flow_dec_rechazado + flow_rechazar_fin + + + + flow_aprobar_desembolso + flow_desembolso_archivo + + + + flow_desembolso_archivo + flow_rechazar_fin + flow_archivo_end + + + + flow_archivo_end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/sample-processes/simple-linear.bpmn b/public/sample-processes/simple-linear.bpmn new file mode 100644 index 0000000..b8d8167 --- /dev/null +++ b/public/sample-processes/simple-linear.bpmn @@ -0,0 +1,98 @@ + + + + + + + flow1 + + + + flow1 + flow2 + + + + flow2 + flow3 + + + + flow3 + flow4 + + + + flow4 + flow5 + + + + flow5 + flow6 + + + + flow6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..6e7961c --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,14 @@ +import { RouterProvider } from '@tanstack/react-router' +import { router } from './router' +import { Toaster } from '@/components/ui/toaster' + +export function App() { + return ( + <> + + + + ) +} + +export default App diff --git a/src/components/AppFooter.tsx b/src/components/AppFooter.tsx new file mode 100644 index 0000000..958231c --- /dev/null +++ b/src/components/AppFooter.tsx @@ -0,0 +1,9 @@ +export function AppFooter() { + return ( +
+

+ Process Cost Platform · v0.1.0 · Hecho desde Paraguay 🇵🇾 +

+
+ ) +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..2307a34 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,30 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + { + variants: { + variant: { + default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80', + secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', + destructive: 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', + outline: 'text-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
+} + +export { Badge, badgeVariants } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..62234d4 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,48 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) +Button.displayName = 'Button' + +export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..5164a50 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,55 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardFooter.displayName = 'CardFooter' + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..37a6c8b --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export type InputProps = React.InputHTMLAttributes + +const Input = React.forwardRef(({ className, type, ...props }, ref) => { + return ( + + ) +}) +Input.displayName = 'Input' + +export { Input } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..5771a41 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const labelVariants = cva( + 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..06045ec --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area' +import { cn } from '@/lib/utils' + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = 'vertical', ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..f93eec2 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,147 @@ +import * as React from 'react' +import * as SelectPrimitive from '@radix-ui/react-select' +import { Check, ChevronDown, ChevronUp } from 'lucide-react' +import { cn } from '@/lib/utils' + +const Select = SelectPrimitive.Root +const SelectGroup = SelectPrimitive.Group +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1', + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..3d53692 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import * as SeparatorPrimitive from '@radix-ui/react-separator' +import { cn } from '@/lib/utils' + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => ( + +)) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/src/components/ui/slider.tsx b/src/components/ui/slider.tsx new file mode 100644 index 0000000..0cc8d55 --- /dev/null +++ b/src/components/ui/slider.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' +import * as SliderPrimitive from '@radix-ui/react-slider' +import { cn } from '@/lib/utils' + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)) +Slider.displayName = SliderPrimitive.Root.displayName + +export { Slider } diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..b534f5f --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const Table = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ) +) +Table.displayName = 'Table' + +const TableHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ) +) +TableHeader.displayName = 'TableHeader' + +const TableBody = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ) +) +TableBody.displayName = 'TableBody' + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ) +) +TableRow.displayName = 'TableRow' + +const TableHead = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +TableHead.displayName = 'TableHead' + +const TableCell = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ) +) +TableCell.displayName = 'TableCell' + +export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..c99280c --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import * as TabsPrimitive from '@radix-ui/react-tabs' +import { cn } from '@/lib/utils' + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 0000000..c7bf88d --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,98 @@ +import * as React from 'react' +import * as ToastPrimitives from '@radix-ui/react-toast' +import { cva, type VariantProps } from 'class-variance-authority' +import { X } from 'lucide-react' +import { cn } from '@/lib/utils' + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + 'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full', + { + variants: { + variant: { + default: 'border bg-background text-foreground', + destructive: 'destructive group border-destructive bg-destructive text-destructive-foreground', + }, + }, + defaultVariants: { variant: 'default' }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & VariantProps +>(({ className, variant, ...props }, ref) => ( + +)) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef +type ToastActionElement = React.ReactElement + +export { + type ToastProps, type ToastActionElement, + ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastAction, ToastClose, +} diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx new file mode 100644 index 0000000..5de539a --- /dev/null +++ b/src/components/ui/toaster.tsx @@ -0,0 +1,20 @@ +import { useToast } from './use-toast' +import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from './toast' + +export function Toaster() { + const { toasts } = useToast() + return ( + + {toasts.map(({ id, title, description, ...props }) => ( + +
+ {title && {title}} + {description && {description}} +
+ +
+ ))} + +
+ ) +} diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..0d2f62f --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,27 @@ +import * as React from 'react' +import * as TooltipPrimitive from '@radix-ui/react-tooltip' +import { cn } from '@/lib/utils' + +const TooltipProvider = TooltipPrimitive.Provider +const Tooltip = TooltipPrimitive.Root +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts new file mode 100644 index 0000000..19f461c --- /dev/null +++ b/src/components/ui/use-toast.ts @@ -0,0 +1,70 @@ +import * as React from 'react' +import type { ToastProps } from './toast' + +const TOAST_LIMIT = 3 +const TOAST_REMOVE_DELAY = 4000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode +} + +type Action = + | { type: 'ADD_TOAST'; toast: ToasterToast } + | { type: 'DISMISS_TOAST'; toastId?: string } + | { type: 'REMOVE_TOAST'; toastId?: string } + +interface State { toasts: ToasterToast[] } + +const toastTimeouts = new Map>() + +function reducer(state: State, action: Action): State { + switch (action.type) { + case 'ADD_TOAST': + return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT) } + case 'DISMISS_TOAST': { + const { toastId } = action + if (toastId) { + if (!toastTimeouts.has(toastId)) { + toastTimeouts.set(toastId, setTimeout(() => { + dispatch({ type: 'REMOVE_TOAST', toastId }) + toastTimeouts.delete(toastId) + }, TOAST_REMOVE_DELAY)) + } + } + return { ...state, toasts: state.toasts.map((t) => t.id === toastId || !toastId ? { ...t, open: false } : t) } + } + case 'REMOVE_TOAST': + return { ...state, toasts: action.toastId ? state.toasts.filter((t) => t.id !== action.toastId) : [] } + } +} + +const listeners: ((state: State) => void)[] = [] +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((l) => l(memoryState)) +} + +let count = 0 +function genId() { return `${++count}` } + +function toast(props: Omit) { + const id = genId() + const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id }) + dispatch({ type: 'ADD_TOAST', toast: { ...props, id, open: true, onOpenChange: (open) => { if (!open) dismiss() } } }) + return { id, dismiss } +} + +export function useToast() { + const [state, setState] = React.useState(memoryState) + React.useEffect(() => { + listeners.push(setState) + return () => { const idx = listeners.indexOf(setState); if (idx > -1) listeners.splice(idx, 1) } + }, []) + return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }) } +} + +export { toast } diff --git a/src/domain/bpmn-parser.ts b/src/domain/bpmn-parser.ts new file mode 100644 index 0000000..d8d3c3b --- /dev/null +++ b/src/domain/bpmn-parser.ts @@ -0,0 +1,153 @@ +import type { + ProcessGraph, + ProcessGraphNode, + ProcessGraphFlow, + BpmnNodeType, + BpmnElementId, +} from './types' + +// Mapeo de tags XML BPMN → nuestro tipo normalizado +const TAG_TO_TYPE: Record = { + startevent: 'startEvent', + endevent: 'endEvent', + task: 'task', + usertask: 'userTask', + servicetask: 'serviceTask', + scripttask: 'scriptTask', + manualtask: 'manualTask', + businessruletask: 'businessRuleTask', + subprocess: 'subprocess', + exclusivegateway: 'exclusiveGateway', + parallelgateway: 'parallelGateway', + inclusivegateway: 'inclusiveGateway', + eventbasedgateway: 'eventBasedGateway', + intermediatethrowevent: 'intermediateThrowEvent', + intermediatecatchevent: 'intermediateCatchEvent', +} + +function normalizeLocalName(tagName: string): string { + // Eliminar namespace prefix (bpmn:Task → task) + const local = tagName.includes(':') ? tagName.split(':')[1] : tagName + return local.toLowerCase() +} + +function getAttr(el: Element, name: string): string { + return el.getAttribute(name) ?? '' +} + +function parseFlowRefs(el: Element, tagName: string): string[] { + return Array.from(el.getElementsByTagNameNS('*', tagName)).map((ref) => ref.textContent?.trim() ?? '') +} + +export class BpmnParseError extends Error { + constructor(message: string) { + super(message) + this.name = 'BpmnParseError' + } +} + +export function parseBpmnXml(xml: string): ProcessGraph { + let doc: Document + try { + const parser = new DOMParser() + doc = parser.parseFromString(xml, 'application/xml') + } catch { + throw new BpmnParseError('El archivo no es un XML válido') + } + + const parserError = doc.querySelector('parsererror') + if (parserError) { + throw new BpmnParseError('XML malformado: ' + parserError.textContent?.slice(0, 200)) + } + + const nodes = new Map() + const flows = new Map() + const startEventIds: BpmnElementId[] = [] + + // Buscar el proceso principal (puede estar anidado bajo definitions) + const processEl = doc.querySelector('[*|localName="process"], process') + if (!processEl) { + throw new BpmnParseError('No se encontró un elemento en el BPMN') + } + + // Iterar todos los hijos directos del proceso + for (const child of Array.from(processEl.children)) { + const local = normalizeLocalName(child.tagName) + const type: BpmnNodeType = TAG_TO_TYPE[local] ?? 'unknown' + + if (local === 'sequenceflow') { + const flow: ProcessGraphFlow = { + id: getAttr(child, 'id'), + sourceRef: getAttr(child, 'sourceRef'), + targetRef: getAttr(child, 'targetRef'), + name: getAttr(child, 'name') || undefined, + } + if (flow.id) flows.set(flow.id, flow) + continue + } + + if (type === 'unknown') continue + + const id = getAttr(child, 'id') + if (!id) continue + + const node: ProcessGraphNode = { + id, + name: getAttr(child, 'name'), + type, + outgoing: parseFlowRefs(child, 'outgoing'), + incoming: parseFlowRefs(child, 'incoming'), + } + + nodes.set(id, node) + if (type === 'startEvent') startEventIds.push(id) + } + + if (nodes.size === 0) { + throw new BpmnParseError('El proceso no contiene elementos reconocibles') + } + + if (startEventIds.length === 0) { + throw new BpmnParseError('El proceso no tiene evento de inicio (StartEvent)') + } + + return { nodes, flows, startEventIds } +} + +// Extrae solo los IDs y nombres de los task/subprocess — util para inicializar actividades +export function extractActivityElements( + graph: ProcessGraph +): Array<{ bpmnElementId: BpmnElementId; name: string; type: 'task' | 'subprocess' }> { + const result = [] + for (const node of graph.nodes.values()) { + if ( + ['task', 'userTask', 'serviceTask', 'scriptTask', 'manualTask', 'businessRuleTask'].includes(node.type) + ) { + result.push({ bpmnElementId: node.id, name: node.name || node.id, type: 'task' as const }) + } else if (node.type === 'subprocess') { + result.push({ bpmnElementId: node.id, name: node.name || node.id, type: 'subprocess' as const }) + } + } + return result +} + +// Extrae gateways divergentes (los que tienen >1 outgoing) +export function extractGatewayElements( + graph: ProcessGraph +): Array<{ bpmnElementId: BpmnElementId; name: string; gatewayType: string; outgoing: string[] }> { + const result = [] + for (const node of graph.nodes.values()) { + if ( + ['exclusiveGateway', 'parallelGateway', 'inclusiveGateway', 'eventBasedGateway'].includes(node.type) && + node.outgoing.length > 1 + ) { + result.push({ + bpmnElementId: node.id, + name: node.name || node.id, + gatewayType: node.type, + outgoing: node.outgoing, + }) + } + } + return result +} diff --git a/src/domain/schemas.ts b/src/domain/schemas.ts new file mode 100644 index 0000000..73da67e --- /dev/null +++ b/src/domain/schemas.ts @@ -0,0 +1,54 @@ +import { z } from 'zod' + +export const ResourceSchema = z.object({ + id: z.string().uuid(), + processId: z.string().uuid(), + name: z.string().min(1, 'El nombre es requerido'), + type: z.enum(['role', 'person', 'system', 'equipment', 'supply']), + costPerHour: z.number().min(0, 'El costo no puede ser negativo'), +}) + +export const ActivityResourceAssignmentSchema = z.object({ + resourceId: z.string().uuid(), + utilizationPercent: z.number().min(0).max(1), +}) + +export const ActivitySchema = z.object({ + id: z.string().uuid(), + processId: z.string().uuid(), + bpmnElementId: z.string().min(1), + name: z.string(), + type: z.enum(['task', 'subprocess']), + directCostFixed: z.number().min(0), + executionTimeMinutes: z.number().min(0), + assignedResources: z.array(ActivityResourceAssignmentSchema), +}) + +export const GatewayBranchSchema = z.object({ + flowId: z.string(), + targetElementId: z.string(), + probability: z.number().min(0).max(1), +}) + +export const GatewayConfigSchema = z.object({ + id: z.string().uuid(), + processId: z.string().uuid(), + bpmnElementId: z.string().min(1), + gatewayType: z.enum(['exclusive', 'parallel', 'inclusive', 'event-based']), + branches: z.array(GatewayBranchSchema), +}) + +export const ProcessSchema = z.object({ + id: z.string().uuid(), + name: z.string().min(1, 'El nombre del proceso es requerido'), + clientName: z.string(), + bpmnXml: z.string().min(1), + currency: z.string().length(3, 'Código ISO de 3 letras'), + overheadPercentage: z.number().min(0).max(1), + createdAt: z.number(), + updatedAt: z.number(), +}) + +export type ResourceInput = z.infer +export type ActivityInput = z.infer +export type ProcessInput = z.infer diff --git a/src/domain/simulation.ts b/src/domain/simulation.ts new file mode 100644 index 0000000..4ee9bca --- /dev/null +++ b/src/domain/simulation.ts @@ -0,0 +1,255 @@ +import { parseBpmnXml } from './bpmn-parser' +import type { + SimulationInput, + SimulationResult, + ActivitySimResult, + ResourceSimResult, + ProcessGraph, + BpmnElementId, +} from './types' +import { isGatewayNode } from './types' + +// ─── Detección de back-edges (loops) via DFS con coloreado ────────────────── + +function findBackEdges(graph: ProcessGraph): Set { + const backEdges = new Set() + const WHITE = 0, GRAY = 1, BLACK = 2 + const color = new Map() + + function dfs(nodeId: BpmnElementId) { + color.set(nodeId, GRAY) + const node = graph.nodes.get(nodeId) + if (!node) { color.set(nodeId, BLACK); return } + + for (const flowId of node.outgoing) { + const flow = graph.flows.get(flowId) + if (!flow) continue + const targetColor = color.get(flow.targetRef) ?? WHITE + if (targetColor === GRAY) { + backEdges.add(flowId) + } else if (targetColor === WHITE) { + dfs(flow.targetRef) + } + } + color.set(nodeId, BLACK) + } + + for (const startId of graph.startEventIds) { + if ((color.get(startId) ?? 0) === 0) dfs(startId) + } + return backEdges +} + +// ─── Detección de loops con mensajes accionables ────────────────────────────── + +function detectLoops(graph: ProcessGraph, backEdges: Set): string[] { + const warnings: string[] = [] + const seen = new Set() + + for (const flowId of backEdges) { + const flow = graph.flows.get(flowId) + if (!flow || seen.has(flow.targetRef)) continue + seen.add(flow.targetRef) + const name = graph.nodes.get(flow.targetRef)?.name || flow.targetRef + warnings.push( + `Loop detectado en el proceso: el nodo "${name}" forma un ciclo. Los costos se calcularán asumiendo una sola ejecución del ciclo.` + ) + } + return warnings +} + +// ─── Sort topológico (Kahn's algorithm, ignora back-edges) ─────────────────── + +function topologicalSort(graph: ProcessGraph, backEdges: Set): BpmnElementId[] { + const inDegree = new Map() + for (const nodeId of graph.nodes.keys()) inDegree.set(nodeId, 0) + + for (const [flowId, flow] of graph.flows) { + if (backEdges.has(flowId)) continue + inDegree.set(flow.targetRef, (inDegree.get(flow.targetRef) ?? 0) + 1) + } + + const queue: BpmnElementId[] = [] + for (const [nodeId, deg] of inDegree) { + if (deg === 0) queue.push(nodeId) + } + + const order: BpmnElementId[] = [] + while (queue.length > 0) { + const nodeId = queue.shift()! + order.push(nodeId) + const node = graph.nodes.get(nodeId) + if (!node) continue + for (const flowId of node.outgoing) { + if (backEdges.has(flowId)) continue + const flow = graph.flows.get(flowId) + if (!flow) continue + const newDeg = (inDegree.get(flow.targetRef) ?? 0) - 1 + inDegree.set(flow.targetRef, newDeg) + if (newDeg === 0) queue.push(flow.targetRef) + } + } + return order +} + +// ─── Propagación de probabilidades en orden topológico ─────────────────────── +// +// Cada nodo se visita exactamente una vez, con su probabilidad ya acumulada. +// Esto evita el overcounting del BFS (donde nodos convergentes se visitaban +// múltiples veces con probabilidades parciales). +// +// Reglas de acumulación en el nodo TARGET: +// AND-join (parallelGateway convergente): max(existing, branchProb) +// — las ramas paralelas son instancias del mismo flujo, no alternativas. +// Todo lo demás (XOR-join, secuencia, eventos): sum clampeado a 1.0 +// — representan caminos mutuamente excluyentes. + +function computeExecutionProbabilities( + graph: ProcessGraph, + gatewayProbs: Map>, + backEdges: Set +): Map { + + const andJoinIds = new Set() + for (const node of graph.nodes.values()) { + if (node.type === 'parallelGateway' && node.incoming.length > 1 && node.outgoing.length <= 1) { + andJoinIds.add(node.id) + } + } + + const probs = new Map() + for (const startId of graph.startEventIds) probs.set(startId, 1.0) + + const topoOrder = topologicalSort(graph, backEdges) + + for (const nodeId of topoOrder) { + const node = graph.nodes.get(nodeId) + if (!node) continue + const nodeProb = probs.get(nodeId) ?? 0 + + for (const flowId of node.outgoing) { + if (backEdges.has(flowId)) continue + const flow = graph.flows.get(flowId) + if (!flow) continue + + let branchProb = nodeProb + + if (isGatewayNode(node.type)) { + const gwFlowProbs = gatewayProbs.get(nodeId) + if (gwFlowProbs) { + branchProb = nodeProb * (gwFlowProbs.get(flowId) ?? 0) + } else { + branchProb = node.type === 'parallelGateway' + ? nodeProb + : nodeProb / Math.max(node.outgoing.length, 1) + } + } + + const targetId = flow.targetRef + const existing = probs.get(targetId) ?? 0 + const accumulated = andJoinIds.has(targetId) + ? Math.max(existing, branchProb) + : Math.min(existing + branchProb, 1.0) + + probs.set(targetId, accumulated) + } + } + + return probs +} + +// ─── Motor de simulación principal ─────────────────────────────────────────── + +export function runSimulation(input: SimulationInput): SimulationResult { + const { processXml, activities, gateways, resources, globalSettings } = input + const warnings: string[] = [] + + const graph = parseBpmnXml(processXml) + + const backEdges = findBackEdges(graph) + warnings.push(...detectLoops(graph, backEdges)) + + const gatewayFlowProbs = new Map>() + for (const [elemId, gwConfig] of gateways.entries()) { + const flowMap = new Map() + for (const branch of gwConfig.branches) flowMap.set(branch.flowId, branch.probability) + gatewayFlowProbs.set(elemId, flowMap) + } + + const execProbs = computeExecutionProbabilities(graph, gatewayFlowProbs, backEdges) + + const perActivity: ActivitySimResult[] = [] + const resourceTotals = new Map() + + for (const activity of activities.values()) { + const execProb = execProbs.get(activity.bpmnElementId) ?? 0 + const node = graph.nodes.get(activity.bpmnElementId) + + if (!node) { + warnings.push( + `La actividad "${activity.name}" (${activity.bpmnElementId}) no se encontró en el diagrama y se omitirá del cálculo.` + ) + continue + } + + const resourceBreakdown: ActivitySimResult['resourceCostBreakdown'] = [] + let resourceCostDirect = 0 + + for (const assignment of activity.assignedResources) { + const resource = resources.get(assignment.resourceId) + if (!resource) continue + const hoursUsed = (activity.executionTimeMinutes / 60) * assignment.utilizationPercent + const cost = resource.costPerHour * hoursUsed * execProb + resourceBreakdown.push({ resourceId: resource.id, resourceName: resource.name, cost }) + resourceCostDirect += cost + + const prev = resourceTotals.get(resource.id) ?? { cost: 0, minutes: 0, name: resource.name } + resourceTotals.set(resource.id, { + cost: prev.cost + cost, + minutes: prev.minutes + activity.executionTimeMinutes * assignment.utilizationPercent * execProb, + name: resource.name, + }) + } + + const fixedCostExpected = activity.directCostFixed * execProb + const totalExpectedDirectCost = fixedCostExpected + resourceCostDirect + + perActivity.push({ + activityId: activity.id, + bpmnElementId: activity.bpmnElementId, + activityName: activity.name || activity.bpmnElementId, + expectedDirectCost: totalExpectedDirectCost, + expectedIndirectCost: 0, + expectedTotalCost: 0, + percentOfTotal: 0, + expectedExecutions: execProb, + executionProbability: execProb, + resourceCostBreakdown: resourceBreakdown, + executionTimeMinutes: activity.executionTimeMinutes, + }) + } + + const totalDirectCost = perActivity.reduce((s, a) => s + a.expectedDirectCost, 0) + const totalIndirectCost = totalDirectCost * globalSettings.overheadPercentage + const totalCost = totalDirectCost + totalIndirectCost + + for (const act of perActivity) { + const directRatio = totalDirectCost > 0 ? act.expectedDirectCost / totalDirectCost : 0 + act.expectedIndirectCost = totalIndirectCost * directRatio + act.expectedTotalCost = act.expectedDirectCost + act.expectedIndirectCost + act.percentOfTotal = totalCost > 0 ? (act.expectedTotalCost / totalCost) * 100 : 0 + } + + perActivity.sort((a, b) => b.expectedTotalCost - a.expectedTotalCost) + + const perResource: ResourceSimResult[] = Array.from(resourceTotals.entries()).map(([id, data]) => ({ + resourceId: id, resourceName: data.name, totalCost: data.cost, totalMinutesUsed: data.minutes, + })) + + const totalTimeMinutes = perActivity.reduce( + (s, a) => s + a.executionTimeMinutes * a.executionProbability, + 0 + ) + + return { totalCost, totalDirectCost, totalIndirectCost, totalTimeMinutes, perActivity, perResource, warnings } +} diff --git a/src/domain/types.ts b/src/domain/types.ts new file mode 100644 index 0000000..9d57366 --- /dev/null +++ b/src/domain/types.ts @@ -0,0 +1,175 @@ +// Tipos de identificadores +export type UUID = string +export type BpmnElementId = string + +// ─── Entidades del modelo de dominio ───────────────────────────────────────── + +export interface Process { + id: UUID + name: string + clientName: string + bpmnXml: string + currency: string + overheadPercentage: number + createdAt: number + updatedAt: number +} + +export type ActivityType = 'task' | 'subprocess' + +export interface Activity { + id: UUID + processId: UUID + bpmnElementId: BpmnElementId + name: string + type: ActivityType + directCostFixed: number + executionTimeMinutes: number + assignedResources: ActivityResourceAssignment[] +} + +export interface ActivityResourceAssignment { + resourceId: UUID + utilizationPercent: number +} + +export type GatewayType = 'exclusive' | 'parallel' | 'inclusive' | 'event-based' + +export interface GatewayBranch { + flowId: string + targetElementId: BpmnElementId + probability: number +} + +export interface GatewayConfig { + id: UUID + processId: UUID + bpmnElementId: BpmnElementId + gatewayType: GatewayType + branches: GatewayBranch[] +} + +export type ResourceType = 'role' | 'person' | 'system' | 'equipment' | 'supply' + +export interface Resource { + id: UUID + processId: UUID + name: string + type: ResourceType + costPerHour: number +} + +export interface Simulation { + id: UUID + processId: UUID + executedAt: number + result: SimulationResult +} + +// ─── Motor de simulación: Input/Output ─────────────────────────────────────── + +export interface SimulationInput { + processXml: string + activities: Map + gateways: Map + resources: Map + globalSettings: { + currency: string + overheadPercentage: number + } +} + +export interface ActivitySimResult { + activityId: UUID + bpmnElementId: BpmnElementId + activityName: string + expectedDirectCost: number + expectedIndirectCost: number + expectedTotalCost: number + percentOfTotal: number + expectedExecutions: number + executionProbability: number + resourceCostBreakdown: Array<{ resourceId: UUID; resourceName: string; cost: number }> + executionTimeMinutes: number +} + +export interface ResourceSimResult { + resourceId: UUID + resourceName: string + totalCost: number + totalMinutesUsed: number +} + +export interface SimulationResult { + totalCost: number + totalDirectCost: number + totalIndirectCost: number + totalTimeMinutes: number + perActivity: ActivitySimResult[] + perResource: ResourceSimResult[] + warnings: string[] +} + +// ─── Grafo en memoria (solo para el parser/motor) ──────────────────────────── + +export type BpmnNodeType = + | 'startEvent' + | 'endEvent' + | 'task' + | 'userTask' + | 'serviceTask' + | 'scriptTask' + | 'manualTask' + | 'businessRuleTask' + | 'subprocess' + | 'exclusiveGateway' + | 'parallelGateway' + | 'inclusiveGateway' + | 'eventBasedGateway' + | 'intermediateThrowEvent' + | 'intermediateCatchEvent' + | 'unknown' + +export interface ProcessGraphNode { + id: BpmnElementId + name: string + type: BpmnNodeType + outgoing: string[] + incoming: string[] +} + +export interface ProcessGraphFlow { + id: string + sourceRef: BpmnElementId + targetRef: BpmnElementId + name?: string +} + +export interface ProcessGraph { + nodes: Map + flows: Map + startEventIds: BpmnElementId[] +} + +// ─── Helpers de clasificación ───────────────────────────────────────────────── + +export function isTaskNode(type: BpmnNodeType): boolean { + return ['task', 'userTask', 'serviceTask', 'scriptTask', 'manualTask', 'businessRuleTask', 'subprocess'].includes(type) +} + +export function isGatewayNode(type: BpmnNodeType): boolean { + return ['exclusiveGateway', 'parallelGateway', 'inclusiveGateway', 'eventBasedGateway'].includes(type) +} + +export function bpmnTypeToActivityType(type: BpmnNodeType): ActivityType { + return type === 'subprocess' ? 'subprocess' : 'task' +} + +export function bpmnTypeToGatewayType(type: BpmnNodeType): GatewayType { + switch (type) { + case 'parallelGateway': return 'parallel' + case 'inclusiveGateway': return 'inclusive' + case 'eventBasedGateway': return 'event-based' + default: return 'exclusive' + } +} diff --git a/src/features/import/ImportPage.tsx b/src/features/import/ImportPage.tsx new file mode 100644 index 0000000..9d2232c --- /dev/null +++ b/src/features/import/ImportPage.tsx @@ -0,0 +1,277 @@ +import { useNavigate } from '@tanstack/react-router' +import { useCallback, useState } from 'react' +import { UploadCloud, FileText, Loader2, FlaskConical } from 'lucide-react' +import { v4 as uuidv4 } from 'uuid' +import { Card, CardContent } from '@/components/ui/card' +import { parseBpmnXml, extractActivityElements, extractGatewayElements, BpmnParseError } from '@/domain/bpmn-parser' +import { processRepo, activityRepo, gatewayRepo } from '@/persistence/repositories' +import type { Process, Activity, GatewayConfig } from '@/domain/types' +import { bpmnTypeToGatewayType } from '@/domain/types' +import { useToast } from '@/components/ui/use-toast' +import { AppFooter } from '@/components/AppFooter' + +const SAMPLE_PROCESSES = [ + { + id: 'simple-linear', + fileName: 'simple-linear.bpmn', + name: 'Proceso lineal simple', + tags: ['5 tareas', 'Sin gateways'], + }, + { + id: 'medium-with-gateways', + fileName: 'medium-with-gateways.bpmn', + name: 'Aprobación de crédito', + tags: ['XOR', 'Paralelo', '11 tareas'], + }, + { + id: 'complex-with-loop', + fileName: 'complex-with-loop.bpmn', + name: 'Control de calidad con revisión', + tags: ['Loop', 'XOR', '9 tareas'], + }, +] + +export function ImportPage() { + const navigate = useNavigate() + const { toast } = useToast() + const [isDragging, setIsDragging] = useState(false) + const [isProcessing, setIsProcessing] = useState(false) + const [loadingSampleId, setLoadingSampleId] = useState(null) + + const processFile = useCallback(async (file: File) => { + if (!file.name.endsWith('.bpmn') && !file.name.endsWith('.xml')) { + toast({ + title: 'Formato no válido', + description: 'El archivo debe tener extensión .bpmn o .xml', + variant: 'destructive', + }) + return + } + + setIsProcessing(true) + + try { + const xml = await file.text() + const graph = parseBpmnXml(xml) + + const processId = uuidv4() + const now = Date.now() + + const process: Process = { + id: processId, + name: file.name.replace(/\.(bpmn|xml)$/, ''), + clientName: '', + bpmnXml: xml, + currency: 'USD', + overheadPercentage: 0.2, + createdAt: now, + updatedAt: now, + } + + const activityElements = extractActivityElements(graph) + const activities: Activity[] = activityElements.map((el) => ({ + id: uuidv4(), + processId, + bpmnElementId: el.bpmnElementId, + name: el.name, + type: el.type, + directCostFixed: 0, + executionTimeMinutes: 0, + assignedResources: [], + })) + + const gatewayElements = extractGatewayElements(graph) + const gateways: GatewayConfig[] = gatewayElements.map((el) => { + const gwType = bpmnTypeToGatewayType(el.gatewayType as any) + const n = el.outgoing.length + return { + id: uuidv4(), + processId, + bpmnElementId: el.bpmnElementId, + gatewayType: gwType, + branches: el.outgoing.map((flowId) => ({ + flowId, + targetElementId: graph.flows.get(flowId)?.targetRef ?? '', + probability: gwType === 'parallel' ? 1.0 : parseFloat((1 / n).toFixed(4)), + })), + } + }) + + await Promise.all([ + processRepo.save(process), + activityRepo.saveMany(activities), + gatewayRepo.saveMany(gateways), + ]) + + toast({ + title: 'Proceso importado', + description: `${activityElements.length} ${activityElements.length === 1 ? 'actividad' : 'actividades'} detectadas. Configurá los costos para simular.`, + }) + + navigate({ to: '/workspace/$processId', params: { processId } }) + } catch (err) { + setIsProcessing(false) + if (err instanceof BpmnParseError) { + toast({ + title: 'BPMN 2.0 no válido', + description: 'El archivo no pudo ser interpretado como un diagrama BPMN 2.0 estándar.', + variant: 'destructive', + }) + } else { + toast({ + title: 'Error al procesar el archivo', + description: 'Ocurrió un error inesperado. Verificá que el archivo no esté dañado.', + variant: 'destructive', + }) + console.error(err) + } + } + }, [navigate, toast]) + + const onDrop = useCallback((e: React.DragEvent) => { + e.preventDefault() + setIsDragging(false) + const file = e.dataTransfer.files[0] + if (file) processFile(file) + }, [processFile]) + + const onFileInput = useCallback((e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) processFile(file) + }, [processFile]) + + async function handleSampleLoad(sample: typeof SAMPLE_PROCESSES[0]) { + setLoadingSampleId(sample.id) + try { + const response = await fetch(`/sample-processes/${sample.fileName}`) + if (!response.ok) throw new Error('No se pudo cargar el proceso de ejemplo') + const text = await response.text() + const file = new File([text], sample.fileName, { type: 'text/xml' }) + await processFile(file) + } catch (err) { + toast({ + title: 'Error al cargar el ejemplo', + description: 'No se pudo descargar el proceso. Intentá de nuevo.', + variant: 'destructive', + }) + console.error(err) + } finally { + setLoadingSampleId(null) + } + } + + return ( +
+ {/* Header */} +
+
+ + Process Cost Platform · MVP Fase 0 +
+

+ Cuantificá el costo operativo
de tus procesos +

+

+ Importá un archivo BPMN 2.0 y obtené un reporte visual de costos + listo para presentar a tu cliente en minutos. +

+
+ + {/* Drop zone */} + + + + + + + {/* Procesos de ejemplo — visibles directamente */} +
+

+ O cargá un proceso de ejemplo con un click +

+
+ {SAMPLE_PROCESSES.map((sample) => { + const isLoading = loadingSampleId === sample.id + return ( + + ) + })} +
+
+ +

+ 100% en tu navegador · Sin backend · Sin registro · Datos guardados localmente +

+ + +
+ ) +} diff --git a/src/features/import/SampleProcessLoader.tsx b/src/features/import/SampleProcessLoader.tsx new file mode 100644 index 0000000..e24cbf2 --- /dev/null +++ b/src/features/import/SampleProcessLoader.tsx @@ -0,0 +1,119 @@ +import { useState } from 'react' +import { ArrowLeft, FileText, Loader2 } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' + +interface SampleProcess { + id: string + fileName: string + name: string + description: string + tags: string[] +} + +const SAMPLE_PROCESSES: SampleProcess[] = [ + { + id: 'simple-linear', + fileName: 'simple-linear.bpmn', + name: 'Proceso Lineal Simple', + description: '5 tareas en secuencia sin gateways. Ideal para validar el flujo básico de importación y costeo.', + tags: ['Lineal', '5 tareas', 'Sin gateways'], + }, + { + id: 'medium-with-gateways', + fileName: 'medium-with-gateways.bpmn', + name: 'Aprobación de Crédito', + description: '12 tareas con un gateway exclusivo (XOR) y un gateway paralelo (AND). Caso de negocio real.', + tags: ['XOR', 'Paralelo', '12 tareas'], + }, + { + id: 'complex-with-loop', + fileName: 'complex-with-loop.bpmn', + name: 'Control de Calidad con Revisión', + description: 'Proceso con un loop de correcciones que vuelve a la inspección. Valida la detección de ciclos.', + tags: ['Loop', 'XOR', '9 tareas'], + }, +] + +interface SampleProcessLoaderProps { + onBack: () => void + onLoad: (file: File) => void +} + +export function SampleProcessLoader({ onBack, onLoad }: SampleProcessLoaderProps) { + const [loadingId, setLoadingId] = useState(null) + + async function handleSelect(sample: SampleProcess) { + setLoadingId(sample.id) + try { + const response = await fetch(`/sample-processes/${sample.fileName}`) + if (!response.ok) throw new Error('No se pudo cargar el archivo de ejemplo') + const text = await response.text() + const file = new File([text], sample.fileName, { type: 'text/xml' }) + onLoad(file) + } catch (err) { + console.error('Error cargando proceso de ejemplo:', err) + setLoadingId(null) + } + } + + return ( +
+
+ + +
+

Procesos de ejemplo

+

Seleccioná un proceso para comenzar la demo sin necesitar un archivo propio.

+
+ +
+ {SAMPLE_PROCESSES.map((sample) => ( + handleSelect(sample)} + > + +
+
+
+ +
+
+ {sample.name} + {sample.fileName} +
+
+ {loadingId === sample.id ? ( + + ) : ( + + )} +
+
+ +

{sample.description}

+
+ {sample.tags.map((tag) => ( + + {tag} + + ))} +
+
+
+ ))} +
+
+
+ ) +} diff --git a/src/features/report/ActivitiesTable.tsx b/src/features/report/ActivitiesTable.tsx new file mode 100644 index 0000000..0705f16 --- /dev/null +++ b/src/features/report/ActivitiesTable.tsx @@ -0,0 +1,133 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { activityColor } from '@/lib/colors' +import { formatCurrency, formatPercent, formatMinutes } from '@/lib/format' +import type { HeatmapMode } from '@/lib/colors' +import type { ActivitySimResult } from '@/domain/types' + +type SortKey = 'activityName' | 'expectedDirectCost' | 'expectedIndirectCost' | 'expectedTotalCost' | 'percentOfTotal' | 'executionTimeMinutes' | 'executionProbability' + +interface ActivitiesTableProps { + perActivity: ActivitySimResult[] + mode: HeatmapMode + currency: string + highlightedId: string | null + onRowClick: (bpmnElementId: string) => void +} + +function SortIcon({ column, sortKey, dir }: { column: SortKey; sortKey: SortKey; dir: 'asc' | 'desc' }) { + if (column !== sortKey) return + return dir === 'asc' + ? + : +} + +export function ActivitiesTable({ perActivity, mode, currency, highlightedId, onRowClick }: ActivitiesTableProps) { + const [sortKey, setSortKey] = useState('expectedTotalCost') + const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc') + const rowRefs = useRef(new Map()) + + function handleSort(key: SortKey) { + if (key === sortKey) { + setSortDir((d) => d === 'asc' ? 'desc' : 'asc') + } else { + setSortKey(key) + setSortDir('desc') + } + } + + const sorted = useMemo(() => { + return [...perActivity].sort((a, b) => { + const av = a[sortKey] + const bv = b[sortKey] + const mult = sortDir === 'asc' ? 1 : -1 + if (typeof av === 'string') return (av as string).localeCompare(bv as string) * mult + return ((av as number) - (bv as number)) * mult + }) + }, [perActivity, sortKey, sortDir]) + + // Scroll a la fila resaltada + useEffect(() => { + if (!highlightedId) return + const row = rowRefs.current.get(highlightedId) + if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' }) + }, [highlightedId]) + + function Th({ label, column }: { label: string; column: SortKey }) { + return ( + handleSort(column)} + > + + {label} + + + + ) + } + + return ( +
+ + + + + + + + {sorted.map((act) => { + const color = activityColor(act, perActivity, mode) + const isHighlighted = act.bpmnElementId === highlightedId + + return ( + { if (el) rowRefs.current.set(act.bpmnElementId, el) }} + onClick={() => onRowClick(act.bpmnElementId)} + className={`cursor-pointer ${isHighlighted ? 'bg-primary/5 ring-1 ring-inset ring-primary/20' : ''}`} + data-state={isHighlighted ? 'selected' : undefined} + > + +
+ + + {act.activityName} + + + {formatCurrency(act.expectedDirectCost, currency)} + + + {formatCurrency(act.expectedIndirectCost, currency)} + + + {formatCurrency(act.expectedTotalCost, currency)} + + + {formatPercent(act.percentOfTotal)} + + + {act.executionTimeMinutes > 0 ? formatMinutes(act.executionTimeMinutes) : '—'} + + + {formatPercent(act.executionProbability * 100)} + + + ) + })} + +
+ + + + + + +
+
+ ) +} diff --git a/src/features/report/CostByResourceChart.tsx b/src/features/report/CostByResourceChart.tsx new file mode 100644 index 0000000..d57b5d6 --- /dev/null +++ b/src/features/report/CostByResourceChart.tsx @@ -0,0 +1,28 @@ +import { useMemo } from 'react' +import ReactECharts from 'echarts-for-react' +import { buildCostByResourceOption } from '@/lib/chart-options' +import type { ActivitySimResult, Resource } from '@/domain/types' + +interface CostByResourceChartProps { + perActivity: ActivitySimResult[] + resources: Resource[] + currency: string +} + +export function CostByResourceChart({ perActivity, resources, currency }: CostByResourceChartProps) { + const option = useMemo( + () => buildCostByResourceOption(perActivity, resources, currency), + [perActivity, resources, currency] + ) + + if (!option) { + return ( +
+

Sin recursos asignados

+

Asigná recursos a las actividades en el workspace para ver este gráfico

+
+ ) + } + + return +} diff --git a/src/features/report/CostCompositionChart.tsx b/src/features/report/CostCompositionChart.tsx new file mode 100644 index 0000000..d02d961 --- /dev/null +++ b/src/features/report/CostCompositionChart.tsx @@ -0,0 +1,18 @@ +import { useMemo } from 'react' +import ReactECharts from 'echarts-for-react' +import { buildCostCompositionOption } from '@/lib/chart-options' +import type { SimulationResult } from '@/domain/types' + +interface CostCompositionChartProps { + result: SimulationResult + currency: string +} + +export function CostCompositionChart({ result, currency }: CostCompositionChartProps) { + const option = useMemo( + () => buildCostCompositionOption(result, currency), + [result, currency] + ) + + return +} diff --git a/src/features/report/HeatmapCanvas.tsx b/src/features/report/HeatmapCanvas.tsx new file mode 100644 index 0000000..6c007f5 --- /dev/null +++ b/src/features/report/HeatmapCanvas.tsx @@ -0,0 +1,279 @@ +import { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react' +import BpmnViewer from 'bpmn-js/lib/Viewer' +import { activityColor } from '@/lib/colors' +import { formatCurrency, formatPercent, formatMinutes } from '@/lib/format' +import type { HeatmapMode } from '@/lib/colors' +import type { ActivitySimResult } from '@/domain/types' + +interface TooltipData { + visible: boolean + x: number + y: number + activity: ActivitySimResult | null +} + +interface HeatmapCanvasProps { + xml: string + perActivity: ActivitySimResult[] + mode: HeatmapMode + currency: string + selectedId: string | null + onActivityClick: (bpmnElementId: string) => void + minHeight?: number +} + +export interface HeatmapCanvasHandle { + highlightElement: (bpmnElementId: string | null) => void + captureImage: () => Promise +} + +function applyHeatmapToViewer( + viewer: any, + perActivity: ActivitySimResult[], + mode: HeatmapMode +) { + const elementRegistry = viewer.get('elementRegistry') as any + if (!elementRegistry) return + + for (const act of perActivity) { + const gfx: SVGElement | null = elementRegistry.getGraphics(act.bpmnElementId) + if (!gfx) continue + + // activityColor devuelve NEUTRAL_HEATMAP_COLOR cuando no hay varianza significativa, + // evitando una falsa diferenciación en procesos con costos uniformes. + const color = activityColor(act, perActivity, mode) + + const shape = gfx.querySelector('.djs-visual rect') as SVGElement | null + if (shape) { + // Usar style.fill (inline CSS) en lugar de setAttribute: el CSS inline tiene + // mayor especificidad que los estilos de bpmn-js, garantizando que html2canvas + // capture el color real (getAttribute devuelve el atributo, pero el estilo + // computado — que usa html2canvas — podría estar sobreescrito por CSS de bpmn-js). + ;(shape as any).style.fill = color + ;(shape as any).style.fillOpacity = '0.82' + ;(shape as any).style.transition = 'fill 0.35s ease, fill-opacity 0.35s ease' + } + } +} + + +export const HeatmapCanvas = forwardRef( + ({ xml, perActivity, mode, currency, selectedId, onActivityClick, minHeight = 500 }, ref) => { + const containerRef = useRef(null) + const viewerRef = useRef(null) + const [isImported, setIsImported] = useState(false) + // Ref que espeja isImported — permite que captureImage() lea el valor live + // desde dentro de una closure asíncrona (las closures capturan state por valor). + const isImportedRef = useRef(false) + const [tooltip, setTooltip] = useState({ visible: false, x: 0, y: 0, activity: null }) + + const activityMap = useRef(new Map()) + useEffect(() => { + activityMap.current = new Map(perActivity.map((a) => [a.bpmnElementId, a])) + }, [perActivity]) + + // Exponer métodos para resaltar y capturar imagen (para el PDF) + useImperativeHandle(ref, () => ({ + highlightElement: (bpmnElementId: string | null) => { + const viewer = viewerRef.current + if (!viewer) return + const canvas = viewer.get('canvas') as any + const reg = viewer.get('elementRegistry') as any + reg.forEach((el: any) => canvas.removeMarker(el.id, 'bpmn-selected')) + if (bpmnElementId) { + try { canvas.addMarker(bpmnElementId, 'bpmn-selected') } catch { /* ignorar */ } + } + }, + captureImage: async () => { + if (!containerRef.current) return null + + // Esperar a que bpmn-js termine de importar y renderizar (máx. 5 s). + // isImportedRef.current se actualiza sincrónicamente en el .then() de importXML, + // por lo que es visible aquí aunque esta closure se haya creado antes. + if (!isImportedRef.current) { + const TIMEOUT_MS = 5_000 + const POLL_MS = 100 + let elapsed = 0 + while (!isImportedRef.current && elapsed < TIMEOUT_MS) { + await new Promise((resolve) => setTimeout(resolve, POLL_MS)) + elapsed += POLL_MS + } + if (!isImportedRef.current) { + throw new Error( + 'El diagrama aún no terminó de cargar. Esperá unos segundos y reintentá.' + ) + } + } + + // Esperar a que React corra el useEffect de applyHeatmapToViewer Y que la + // transición CSS de los fills termine (fill 0.35s ease → necesitamos > 350ms). + await new Promise((resolve) => setTimeout(resolve, 500)) + + const { default: html2canvas } = await import('html2canvas') + try { + const canvas = await html2canvas(containerRef.current, { + scale: 1.5, + useCORS: true, + allowTaint: true, + backgroundColor: '#f8fafc', + logging: false, + }) + return canvas.toDataURL('image/jpeg', 0.82) + } catch (err) { + console.warn('html2canvas error:', err) + return null + } + }, + })) + + // Crear viewer + useEffect(() => { + if (!containerRef.current) return + const viewer = new BpmnViewer({ container: containerRef.current }) + viewerRef.current = viewer + return () => { + viewer.destroy() + viewerRef.current = null + isImportedRef.current = false + setIsImported(false) + } + }, []) + + // Importar XML. + // bpmn-js 18 puede rechazar la promesa de importXML (error interno de canvas) + // pero igualmente renderiza el diagrama parcialmente y dispara 'import.done' + // en el eventBus antes de rechazar. Escuchamos 'import.done' para setear + // isImported de forma confiable sin depender de que la promesa resuelva. + useEffect(() => { + const viewer = viewerRef.current + if (!viewer || !xml) return + isImportedRef.current = false + setIsImported(false) + + const eventBus = viewer.get('eventBus') as any + + const onImportDone = () => { + eventBus.off('import.done', onImportDone) + const canvas = viewer.get('canvas') as any + try { canvas.zoom('fit-viewport', 'auto') } catch { /* canvas puede no estar listo */ } + isImportedRef.current = true + setIsImported(true) + } + // Usamos .on + .off manual en lugar de .once para compatibilidad con mocks de test. + eventBus.on('import.done', onImportDone) + + viewer.importXML(xml).catch((err: unknown) => { + // Puede rechazar aunque el diagrama ya se haya renderizado (error de root layer en bpmn-js 18). + // 'import.done' ya habrá disparado antes de este catch, por lo que isImported ya es true. + console.warn('HeatmapCanvas importXML error (diagrama puede estar parcialmente renderizado):', err) + }) + + return () => { + eventBus.off('import.done', onImportDone) + } + }, [xml]) + + // Aplicar heatmap cuando el XML está listo o el modo cambia + useEffect(() => { + const viewer = viewerRef.current + if (!isImported || !viewer || perActivity.length === 0) return + applyHeatmapToViewer(viewer, perActivity, mode) + }, [isImported, perActivity, mode]) + + // Resaltar elemento seleccionado externamente + useEffect(() => { + const viewer = viewerRef.current + if (!isImported || !viewer) return + const canvas = viewer.get('canvas') as any + const reg = viewer.get('elementRegistry') as any + reg.forEach((el: any) => canvas.removeMarker(el.id, 'bpmn-selected')) + if (selectedId) { + try { canvas.addMarker(selectedId, 'bpmn-selected') } catch { /* ignorar */ } + } + }, [isImported, selectedId]) + + // Event listeners de bpmn-js para click y hover + useEffect(() => { + const viewer = viewerRef.current + if (!isImported || !viewer) return + + const eventBus = viewer.get('eventBus') as any + const TASK_TYPES = new Set([ + 'bpmn:Task', 'bpmn:UserTask', 'bpmn:ServiceTask', 'bpmn:ScriptTask', + 'bpmn:ManualTask', 'bpmn:BusinessRuleTask', 'bpmn:SubProcess', + ]) + + const onHover = (event: any) => { + const el = event.element + if (!TASK_TYPES.has(el?.type)) { setTooltip((t) => ({ ...t, visible: false })); return } + const act = activityMap.current.get(el.id) + if (!act) return + setTooltip((t) => ({ ...t, visible: true, activity: act })) + } + const onOut = () => setTooltip((t) => ({ ...t, visible: false })) + const onClick = (event: any) => { + const el = event.element + if (!el || !TASK_TYPES.has(el.type)) return + onActivityClick(el.id) + } + + eventBus.on('element.hover', onHover) + eventBus.on('element.out', onOut) + eventBus.on('element.click', onClick) + return () => { + eventBus.off('element.hover', onHover) + eventBus.off('element.out', onOut) + eventBus.off('element.click', onClick) + } + }, [isImported, onActivityClick]) + + const onMouseMove = useCallback((e: React.MouseEvent) => { + setTooltip((t) => ({ ...t, x: e.clientX, y: e.clientY })) + }, []) + + return ( +
+ {/* Canvas bpmn-js */} +
+ + {/* Tooltip custom */} + {tooltip.visible && tooltip.activity && ( +
+

+ {tooltip.activity.activityName} +

+
+
+ Costo esperado + {formatCurrency(tooltip.activity.expectedTotalCost, currency)} +
+
+ % del total + {formatPercent(tooltip.activity.percentOfTotal)} +
+
+ Prob. ejecución + {formatPercent(tooltip.activity.executionProbability * 100)} +
+ {tooltip.activity.executionTimeMinutes > 0 && ( +
+ Tiempo + {formatMinutes(tooltip.activity.executionTimeMinutes)} +
+ )} +
+
+ )} +
+ ) + } +) +HeatmapCanvas.displayName = 'HeatmapCanvas' diff --git a/src/features/report/HeatmapLegend.tsx b/src/features/report/HeatmapLegend.tsx new file mode 100644 index 0000000..b2ead6f --- /dev/null +++ b/src/features/report/HeatmapLegend.tsx @@ -0,0 +1,35 @@ +import { formatCurrency, formatPercent } from '@/lib/format' +import { heatmapLegendBounds } from '@/lib/colors' +import type { HeatmapMode } from '@/lib/colors' +import type { ActivitySimResult } from '@/domain/types' + +interface HeatmapLegendProps { + perActivity: ActivitySimResult[] + mode: HeatmapMode + currency: string +} + +export function HeatmapLegend({ perActivity, mode, currency }: HeatmapLegendProps) { + const bounds = heatmapLegendBounds(perActivity, mode) + + function formatBound(value: number): string { + if (bounds.unit === 'percent') return formatPercent(value, value % 1 === 0 ? 0 : 1) + return formatCurrency(value, currency) + } + + return ( +
+ Bajo costo +
+ Alto costo +
+
+ {formatBound(bounds.min)} + {formatBound(bounds.mid)} + {formatBound(bounds.max)} +
+
+ ) +} diff --git a/src/features/report/HeatmapModeToggle.tsx b/src/features/report/HeatmapModeToggle.tsx new file mode 100644 index 0000000..3acdb50 --- /dev/null +++ b/src/features/report/HeatmapModeToggle.tsx @@ -0,0 +1,49 @@ +import { cn } from '@/lib/utils' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import type { HeatmapMode } from '@/lib/colors' + +interface HeatmapModeToggleProps { + mode: HeatmapMode + onChange: (mode: HeatmapMode) => void +} + +const MODE_LABELS: Record = { + relative: { + label: 'Costo relativo (%)', + tip: 'Colorea cada actividad en función de su % del costo total. Útil para ver proporciones.', + }, + absolute: { + label: 'Costo absoluto', + tip: 'Colorea según el costo esperado en la moneda del proceso. Útil para comparar magnitudes reales.', + }, +} + +export function HeatmapModeToggle({ mode, onChange }: HeatmapModeToggleProps) { + return ( +
+ {(['relative', 'absolute'] as HeatmapMode[]).map((m) => { + const { label, tip } = MODE_LABELS[m] + return ( + + + + + + {tip} + + + ) + })} +
+ ) +} diff --git a/src/features/report/KpiBar.tsx b/src/features/report/KpiBar.tsx new file mode 100644 index 0000000..09db2ac --- /dev/null +++ b/src/features/report/KpiBar.tsx @@ -0,0 +1,72 @@ +import { DollarSign, TrendingUp, Clock, LayoutList } from 'lucide-react' +import { formatCurrency, formatPercent, formatMinutes } from '@/lib/format' +import type { SimulationResult } from '@/domain/types' + +interface KpiBarProps { + result: SimulationResult + currency: string +} + +interface KpiCardProps { + label: string + value: string + sub?: string + icon: React.ReactNode + iconColor: string +} + +function KpiCard({ label, value, sub, icon, iconColor }: KpiCardProps) { + return ( +
+
+ {icon} +
+
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+
+ ) +} + +export function KpiBar({ result, currency }: KpiBarProps) { + const directPct = result.totalCost > 0 + ? formatPercent(result.totalDirectCost / result.totalCost * 100) + : '—' + const indirectPct = result.totalCost > 0 + ? formatPercent(result.totalIndirectCost / result.totalCost * 100) + : '—' + + return ( +
+ } + iconColor="bg-primary/10" + /> + } + iconColor="bg-emerald-50" + /> + } + iconColor="bg-amber-50" + /> + } + iconColor="bg-slate-100" + /> +
+ ) +} diff --git a/src/features/report/MethodologyFooter.tsx b/src/features/report/MethodologyFooter.tsx new file mode 100644 index 0000000..11fff1c --- /dev/null +++ b/src/features/report/MethodologyFooter.tsx @@ -0,0 +1,36 @@ +import type { Process } from '@/domain/types' +import { formatPercent } from '@/lib/format' + +interface MethodologyFooterProps { + process: Process +} + +export function MethodologyFooter({ process }: MethodologyFooterProps) { + return ( +
+

Nota metodológica

+
+

+ Costo total = Σ(actividades) costo_directo_esperado + costo_indirecto_esperado. +

+

+ Costo directo por actividad = (costo_fijo_por_ejecución + Σ(recurso × costo_por_hora × tiempo_de_ejecución × porcentaje_utilización / 60)) × probabilidad_de_ejecución. +

+

+ Costo indirecto = costo_directo_total × overhead_global ({formatPercent(process.overheadPercentage * 100, 0)}). + Representa costos estructurales no imputables directamente a actividades individuales (alquiler, administración, infraestructura). +

+

+ Probabilidad de ejecución: calculada mediante propagación hacia adelante desde el evento de inicio, + considerando las probabilidades configuradas en cada gateway de decisión. + Las actividades en el camino principal tienen probabilidad 1 (100%). + Las actividades en ramas condicionales tienen la probabilidad acumulada del camino que las incluye. +

+

+ Modelo determinístico agregado — Noche Cero MVP. Los tiempos y costos son estimaciones. + Para análisis de sensibilidad y distribuciones probabilísticas, ver V1.0. +

+
+
+ ) +} diff --git a/src/features/report/ReportHeader.tsx b/src/features/report/ReportHeader.tsx new file mode 100644 index 0000000..d680c21 --- /dev/null +++ b/src/features/report/ReportHeader.tsx @@ -0,0 +1,67 @@ +import { ArrowLeft, Download, FileText, Loader2 } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { formatSimulationTimestamp } from '@/lib/format' +import type { Process } from '@/domain/types' + +interface ReportHeaderProps { + process: Process + simulatedAt: number + onBack: () => void + onExportPdf: () => void + onExportCsv: () => void + isExportingPdf?: boolean + isExportingCsv?: boolean +} + +export function ReportHeader({ + process, simulatedAt, onBack, + onExportPdf, onExportCsv, + isExportingPdf = false, isExportingCsv = false, +}: ReportHeaderProps) { + const { date, time } = formatSimulationTimestamp(simulatedAt) + + return ( +
+
+ +

{process.name}

+

+ {process.clientName && <>{process.clientName} · } + Simulado el {date} a las {time} · Moneda: {process.currency} +

+
+ +
+ + +
+
+ ) +} diff --git a/src/features/report/ReportPage.tsx b/src/features/report/ReportPage.tsx new file mode 100644 index 0000000..7dfe77f --- /dev/null +++ b/src/features/report/ReportPage.tsx @@ -0,0 +1,278 @@ +import { useCallback, useRef, useState } from 'react' +import { useParams, useNavigate } from '@tanstack/react-router' +import { AlertTriangle, Home } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Separator } from '@/components/ui/separator' +import { TooltipProvider } from '@/components/ui/tooltip' +import { useToast } from '@/components/ui/use-toast' +import { AppFooter } from '@/components/AppFooter' +import { useReportData } from './useReportData' +import { ReportHeader } from './ReportHeader' +import { KpiBar } from './KpiBar' +import { HeatmapCanvas, type HeatmapCanvasHandle } from './HeatmapCanvas' +import { HeatmapLegend } from './HeatmapLegend' +import { HeatmapModeToggle } from './HeatmapModeToggle' +import { WarningsBanner } from './WarningsBanner' +import { TopActivitiesChart } from './TopActivitiesChart' +import { CostCompositionChart } from './CostCompositionChart' +import { CostByResourceChart } from './CostByResourceChart' +import { ActivitiesTable } from './ActivitiesTable' +import { MethodologyFooter } from './MethodologyFooter' +import type { HeatmapMode } from '@/lib/colors' + +export function ReportPage() { + const { processId } = useParams({ from: '/report/$processId' }) + const navigate = useNavigate() + const { process, simulation, resources, isLoading, error } = useReportData(processId) + + const { toast } = useToast() + const [heatmapMode, setHeatmapMode] = useState('relative') + const [selectedId, setSelectedId] = useState(null) + const [isExportingPdf, setIsExportingPdf] = useState(false) + const [isExportingCsv, setIsExportingCsv] = useState(false) + + const heatmapRef = useRef(null) + const heatmapSectionRef = useRef(null) + const tableSectionRef = useRef(null) + + // Click en heatmap → resaltar fila en tabla + scroll a tabla + const handleHeatmapClick = useCallback((bpmnElementId: string) => { + setSelectedId(bpmnElementId) + setTimeout(() => { + tableSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + }, 50) + }, []) + + // Click en fila de tabla → resaltar en canvas + scroll a heatmap + const handleTableRowClick = useCallback((bpmnElementId: string) => { + setSelectedId(bpmnElementId) + heatmapRef.current?.highlightElement(bpmnElementId) + setTimeout(() => { + heatmapSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + }, 50) + }, []) + + const handleExportPdf = useCallback(async () => { + if (!process || !simulation || isExportingPdf) return + setIsExportingPdf(true) + try { + const heatmapImageData = await heatmapRef.current?.captureImage() ?? null + const { exportToPdf } = await import('@/lib/export/pdf-export') + await exportToPdf({ process, simulation, resources, heatmapImageData }) + toast({ title: 'PDF generado', description: 'El reporte se descargó correctamente.' }) + } catch (err) { + console.error('PDF export error:', err) + toast({ title: 'Error al generar el PDF', description: String(err), variant: 'destructive' }) + } finally { + setIsExportingPdf(false) + } + }, [process, simulation, resources, isExportingPdf, toast]) + + const handleExportCsv = useCallback(async () => { + if (!process || !simulation || isExportingCsv) return + setIsExportingCsv(true) + try { + const { exportToCsv } = await import('@/lib/export/csv-export') + exportToCsv({ process, simulation, resources }) + toast({ title: 'CSV generado', description: 'Los datos se descargaron correctamente.' }) + } catch (err) { + console.error('CSV export error:', err) + toast({ title: 'Error al exportar CSV', description: String(err), variant: 'destructive' }) + } finally { + setIsExportingCsv(false) + } + }, [process, simulation, resources, isExportingCsv, toast]) + + // ── Loading — skeleton en lugar de pantalla en blanco ──────────────────── + if (isLoading) { + return ( +
+
+ {/* Header skeleton */} +
+
+
+
+
+
+
+
+
+
+
+ {/* KPI skeleton */} +
+ {[0, 1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+ ))} +
+ {/* Canvas skeleton */} +
+
+
+
+ {/* Table skeleton */} +
+
+ {[0, 1, 2, 3].map((i) => ( +
+
+
+
+
+ ))} +
+
+
+ ) + } + + // ── Error ───────────────────────────────────────────────────────────────── + if (error || !process || !simulation) { + return ( +
+ +
+

No se pudo cargar el reporte

+

+ {error ?? 'Simulá el proceso desde el workspace para generar el reporte.'} +

+
+
+ + +
+
+ ) + } + + const result = simulation.result + const perActivity = result.perActivity + + return ( + +
+
+ + {/* ── 1. Header ─────────────────────────────────────────────── */} + navigate({ to: '/workspace/$processId', params: { processId } })} + onExportPdf={handleExportPdf} + onExportCsv={handleExportCsv} + isExportingPdf={isExportingPdf} + isExportingCsv={isExportingCsv} + /> + + {/* ── 2. KPI Bar ────────────────────────────────────────────── */} + + + {/* ── 3. Mapa de Calor ──────────────────────────────────────── */} +
+
+
+

Mapa de calor de costos

+

+ Hacé click en una actividad para ver su detalle en la tabla +

+
+ +
+ + + + +
+ + {/* ── 4. Warnings ───────────────────────────────────────────── */} + + + {/* ── 5. Análisis — 3 gráficos ──────────────────────────────── */} +
+

Análisis de composición

+
+
+

+ Top actividades por costo +

+ +
+ +
+

+ Composición directo / overhead +

+ +
+ +
+

+ Costo por tipo de recurso +

+ +
+
+
+ + {/* ── 6. Tabla detalle ──────────────────────────────────────── */} +
+
+

+ Detalle por actividad + ({perActivity.length} actividades) +

+
+ +
+ + + + {/* ── 7. Footer metodológico ────────────────────────────────── */} + + + +
+
+
+ ) +} diff --git a/src/features/report/TopActivitiesChart.tsx b/src/features/report/TopActivitiesChart.tsx new file mode 100644 index 0000000..58b103d --- /dev/null +++ b/src/features/report/TopActivitiesChart.tsx @@ -0,0 +1,24 @@ +import { useMemo } from 'react' +import ReactECharts from 'echarts-for-react' +import { buildTopActivitiesOption } from '@/lib/chart-options' +import type { HeatmapMode } from '@/lib/colors' +import type { ActivitySimResult } from '@/domain/types' + +interface TopActivitiesChartProps { + perActivity: ActivitySimResult[] + mode: HeatmapMode + currency: string +} + +export function TopActivitiesChart({ perActivity, mode, currency }: TopActivitiesChartProps) { + const option = useMemo( + () => buildTopActivitiesOption(perActivity, mode, currency), + [perActivity, mode, currency] + ) + + if (perActivity.length === 0) { + return
Sin datos de actividades
+ } + + return +} diff --git a/src/features/report/WarningsBanner.tsx b/src/features/report/WarningsBanner.tsx new file mode 100644 index 0000000..3712fda --- /dev/null +++ b/src/features/report/WarningsBanner.tsx @@ -0,0 +1,36 @@ +import { AlertTriangle } from 'lucide-react' + +interface WarningsBannerProps { + warnings: string[] +} + +export function WarningsBanner({ warnings }: WarningsBannerProps) { + if (warnings.length === 0) return null + + const hasLoop = warnings.some((w) => w.toLowerCase().includes('loop') || w.toLowerCase().includes('ciclo')) + + return ( +
+
+ +
+

+ {warnings.length === 1 ? 'Advertencia del motor de simulación' : `${warnings.length} advertencias del motor de simulación`} +

+
    + {warnings.map((w, i) => ( +
  • • {w}
  • + ))} +
+ {hasLoop && ( +

+ Impacto en los resultados: Los procesos con ciclos se calculan asumiendo una sola ejecución del loop. + Los costos reales pueden ser mayores si el loop se repite múltiples veces en cada instancia del proceso. + Para modelado probabilístico de loops, se implementará distribuciones en V1.0. +

+ )} +
+
+
+ ) +} diff --git a/src/features/report/useReportData.ts b/src/features/report/useReportData.ts new file mode 100644 index 0000000..53d11b2 --- /dev/null +++ b/src/features/report/useReportData.ts @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react' +import { processRepo, activityRepo, resourceRepo, simulationRepo } from '@/persistence/repositories' +import type { Process, Activity, Resource, Simulation } from '@/domain/types' + +interface ReportData { + process: Process | null + simulation: Simulation | null + activities: Activity[] + resources: Resource[] + isLoading: boolean + error: string | null +} + +export function useReportData(processId: string): ReportData { + const [process, setProcess] = useState(null) + const [simulation, setSimulation] = useState(null) + const [activities, setActivities] = useState([]) + const [resources, setResources] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + + async function load() { + setIsLoading(true) + setError(null) + try { + const [proc, sim, acts, res] = await Promise.all([ + processRepo.getById(processId), + simulationRepo.getLatestByProcess(processId), + activityRepo.getByProcess(processId), + resourceRepo.getByProcess(processId), + ]) + if (cancelled) return + if (!proc) throw new Error('Proceso no encontrado en la base de datos local') + if (!sim) throw new Error('Sin simulación disponible. Volvé al workspace y ejecutá "Simular" primero.') + setProcess(proc) + setSimulation(sim) + setActivities(acts) + setResources(res) + } catch (err) { + if (!cancelled) setError(err instanceof Error ? err.message : 'Error al cargar el reporte') + } finally { + if (!cancelled) setIsLoading(false) + } + } + + load() + return () => { cancelled = true } + }, [processId]) + + return { process, simulation, activities, resources, isLoading, error } +} diff --git a/src/features/simulation/useSimulate.ts b/src/features/simulation/useSimulate.ts new file mode 100644 index 0000000..b1bc466 --- /dev/null +++ b/src/features/simulation/useSimulate.ts @@ -0,0 +1,56 @@ +import { useCallback } from 'react' +import { v4 as uuidv4 } from 'uuid' +import { useProcessStore } from '@/store/process-store' +import { useSimulationStore } from '@/store/simulation-store' +import { runSimulation } from '@/domain/simulation' +import { simulationRepo } from '@/persistence/repositories' +import type { SimulationInput } from '@/domain/types' + +export function useSimulate() { + const { currentProcess, activities, gateways, resources } = useProcessStore() + const { setResult, setRunning, setError } = useSimulationStore() + + const simulate = useCallback(async () => { + if (!currentProcess) { + setError('No hay proceso cargado') + return null + } + + setRunning(true) + setError(null) + + try { + const input: SimulationInput = { + processXml: currentProcess.bpmnXml, + activities: new Map(activities.map((a) => [a.bpmnElementId, a])), + gateways: new Map(gateways.map((g) => [g.bpmnElementId, g])), + resources: new Map(resources.map((r) => [r.id, r])), + globalSettings: { + currency: currentProcess.currency, + overheadPercentage: currentProcess.overheadPercentage, + }, + } + + const result = runSimulation(input) + + const simulation = { + id: uuidv4(), + processId: currentProcess.id, + executedAt: Date.now(), + result, + } + + await simulationRepo.save(simulation) + setResult(result) + setRunning(false) + return result + } catch (err) { + const message = err instanceof Error ? err.message : 'Error en la simulación' + setError(message) + setRunning(false) + return null + } + }, [currentProcess, activities, gateways, resources, setResult, setRunning, setError]) + + return { simulate } +} diff --git a/src/features/workspace/ActivityPanel.tsx b/src/features/workspace/ActivityPanel.tsx new file mode 100644 index 0000000..2582373 --- /dev/null +++ b/src/features/workspace/ActivityPanel.tsx @@ -0,0 +1,241 @@ +import { useEffect, useState } from 'react' +import { PlusCircle, Trash2, Info } from 'lucide-react' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Slider } from '@/components/ui/slider' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { ScrollArea } from '@/components/ui/scroll-area' +import { useProcessStore } from '@/store/process-store' +import { formatCurrency } from '@/lib/format' +import type { Activity } from '@/domain/types' + +interface ActivityPanelProps { + selectedElementId: string | null +} + +export function ActivityPanel({ selectedElementId }: ActivityPanelProps) { + const { activities, resources, updateActivity } = useProcessStore() + const [localActivity, setLocalActivity] = useState(null) + const [isDirty, setIsDirty] = useState(false) + + const activity = activities.find((a) => a.bpmnElementId === selectedElementId) ?? null + + // Sincronizar con el store cuando cambia la selección + useEffect(() => { + setLocalActivity(activity ? { ...activity, assignedResources: [...activity.assignedResources] } : null) + setIsDirty(false) + }, [selectedElementId, activities]) + + function handleChange(key: K, value: Activity[K]) { + if (!localActivity) return + setLocalActivity((prev) => prev ? { ...prev, [key]: value } : null) + setIsDirty(true) + } + + async function handleSave() { + if (!localActivity) return + await updateActivity(localActivity) + setIsDirty(false) + } + + function addResource(resourceId: string) { + if (!localActivity) return + const already = localActivity.assignedResources.some((r) => r.resourceId === resourceId) + if (already) return + handleChange('assignedResources', [ + ...localActivity.assignedResources, + { resourceId, utilizationPercent: 1.0 }, + ]) + } + + function removeResource(resourceId: string) { + if (!localActivity) return + handleChange( + 'assignedResources', + localActivity.assignedResources.filter((r) => r.resourceId !== resourceId) + ) + } + + function setUtilization(resourceId: string, value: number) { + if (!localActivity) return + handleChange( + 'assignedResources', + localActivity.assignedResources.map((r) => + r.resourceId === resourceId ? { ...r, utilizationPercent: value } : r + ) + ) + } + + if (!selectedElementId) { + return ( +
+
+ + + +
+

Seleccioná una actividad

+

Hacé click sobre una tarea en el diagrama para editar sus atributos de costo

+
+ ) + } + + if (!localActivity) { + return ( +
+

Esta actividad no tiene configuración todavía

+
+ ) + } + + const availableResources = resources.filter( + (r) => !localActivity.assignedResources.some((ar) => ar.resourceId === r.id) + ) + + return ( + +
+ {/* Nombre (read-only, viene del BPMN) */} +
+ +

{localActivity.name || localActivity.bpmnElementId}

+

{localActivity.bpmnElementId}

+
+ + {/* Costo directo fijo */} +
+
+ + + + + + + Costo fijo por cada ejecución de esta actividad (materiales, licencias, etc.) — independiente del tiempo + + +
+ handleChange('directCostFixed', Math.max(0, parseFloat(e.target.value) || 0))} + className="font-mono h-8 text-sm" + placeholder="0" + /> +
+ + {/* Tiempo de ejecución */} +
+
+ + + + + + + Tiempo promedio estimado para completar esta actividad + + +
+ handleChange('executionTimeMinutes', Math.max(0, parseFloat(e.target.value) || 0))} + className="font-mono h-8 text-sm" + placeholder="0" + /> +
+ + {/* Recursos asignados */} +
+ + + {localActivity.assignedResources.length === 0 ? ( +

Sin recursos asignados

+ ) : ( +
+ {localActivity.assignedResources.map((assignment) => { + const resource = resources.find((r) => r.id === assignment.resourceId) + if (!resource) return null + const hourlyShare = (resource.costPerHour * assignment.utilizationPercent).toFixed(2) + + return ( +
+
+
+

{resource.name}

+

{formatCurrency(resource.costPerHour)}/h · {formatCurrency(parseFloat(hourlyShare))}/h efectivo

+
+ +
+
+
+ Utilización + {Math.round(assignment.utilizationPercent * 100)}% +
+ setUtilization(assignment.resourceId, v / 100)} + /> +
+
+ ) + })} +
+ )} + + {/* Agregar recurso */} + {availableResources.length > 0 && ( + + )} + + {resources.length === 0 && ( +

Creá recursos en la tab "Recursos" para asignarlos

+ )} +
+ + {/* Guardar */} + +
+
+ ) +} diff --git a/src/features/workspace/BpmnCanvas.tsx b/src/features/workspace/BpmnCanvas.tsx new file mode 100644 index 0000000..4587563 --- /dev/null +++ b/src/features/workspace/BpmnCanvas.tsx @@ -0,0 +1,82 @@ +import { useEffect, useRef } from 'react' +import BpmnViewer from 'bpmn-js/lib/Viewer' + +export type BpmnElementCategory = 'activity' | 'gateway' | 'other' + +const ACTIVITY_TYPES = new Set([ + 'bpmn:Task', 'bpmn:UserTask', 'bpmn:ServiceTask', 'bpmn:ScriptTask', + 'bpmn:ManualTask', 'bpmn:BusinessRuleTask', 'bpmn:SubProcess', +]) + +const GATEWAY_TYPES = new Set([ + 'bpmn:ExclusiveGateway', 'bpmn:ParallelGateway', + 'bpmn:InclusiveGateway', 'bpmn:EventBasedGateway', +]) + +function classifyElement(type: string): BpmnElementCategory { + if (ACTIVITY_TYPES.has(type)) return 'activity' + if (GATEWAY_TYPES.has(type)) return 'gateway' + return 'other' +} + +interface BpmnCanvasProps { + xml: string + onElementClick?: (elementId: string, elementName: string, category: BpmnElementCategory) => void + selectedElementId?: string | null + className?: string +} + +export function BpmnCanvas({ xml, onElementClick, selectedElementId, className }: BpmnCanvasProps) { + const containerRef = useRef(null) + const viewerRef = useRef | null>(null) + + useEffect(() => { + if (!containerRef.current) return + const viewer = new BpmnViewer({ container: containerRef.current }) + viewerRef.current = viewer + return () => { + viewer.destroy() + viewerRef.current = null + } + }, []) + + useEffect(() => { + const viewer = viewerRef.current + if (!viewer || !xml) return + viewer.importXML(xml).then(({ warnings }) => { + if (warnings.length) console.warn('bpmn-js warnings:', warnings) + const canvas = viewer.get('canvas') as any + canvas.zoom('fit-viewport', 'auto') + }).catch((err: Error) => console.error('Error cargando BPMN:', err)) + }, [xml]) + + useEffect(() => { + const viewer = viewerRef.current + if (!viewer || !onElementClick) return + const eventBus = viewer.get('eventBus') as any + + const handler = (event: any) => { + const el = event.element + if (!el) return + const category = classifyElement(el.type) + if (category === 'other') return + onElementClick(el.id, el.businessObject?.name ?? el.id, category) + } + + eventBus.on('element.click', handler) + return () => eventBus.off('element.click', handler) + }, [onElementClick]) + + useEffect(() => { + const viewer = viewerRef.current + if (!viewer) return + const canvas = viewer.get('canvas') as any + const reg = viewer.get('elementRegistry') as any + reg.forEach((el: any) => canvas.removeMarker(el.id, 'bpmn-selected')) + if (selectedElementId) { + try { canvas.addMarker(selectedElementId, 'bpmn-selected') } catch { /* ignorar */ } + } + }, [selectedElementId]) + + return
+} diff --git a/src/features/workspace/GatewayPanel.tsx b/src/features/workspace/GatewayPanel.tsx new file mode 100644 index 0000000..82d0c6a --- /dev/null +++ b/src/features/workspace/GatewayPanel.tsx @@ -0,0 +1,277 @@ +import { useEffect, useMemo, useState } from 'react' +import { Info, AlertTriangle, RotateCcw, Check, GitMerge } from 'lucide-react' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' +import { Slider } from '@/components/ui/slider' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { ScrollArea } from '@/components/ui/scroll-area' +import { useProcessStore } from '@/store/process-store' +import { parseBpmnXml } from '@/domain/bpmn-parser' +import { formatPercent } from '@/lib/format' +import type { GatewayConfig } from '@/domain/types' + +interface GatewayPanelProps { + selectedElementId: string | null +} + +// Tolerancia para validar que la suma de probs XOR = 1.0 +const XOR_SUM_TOLERANCE = 0.005 + +function sumBranches(gw: GatewayConfig): number { + return gw.branches.reduce((s, b) => s + b.probability, 0) +} + +export function isXorSumValid(gw: GatewayConfig): boolean { + if (gw.gatewayType !== 'exclusive') return true + return Math.abs(sumBranches(gw) - 1.0) <= XOR_SUM_TOLERANCE +} + +export function GatewayPanel({ selectedElementId }: GatewayPanelProps) { + const { gateways, currentProcess, updateGateway, activities } = useProcessStore() + const [localGw, setLocalGw] = useState(null) + const [isDirty, setIsDirty] = useState(false) + + // Mapa bpmnElementId → nombre legible (parseamos el XML una sola vez) + const elementNames = useMemo(() => { + if (!currentProcess) return new Map() + try { + const graph = parseBpmnXml(currentProcess.bpmnXml) + return new Map(Array.from(graph.nodes.values()).map((n) => [n.id, n.name || n.id])) + } catch { + return new Map() + } + }, [currentProcess?.bpmnXml]) + + const gateway = gateways.find((g) => g.bpmnElementId === selectedElementId) ?? null + + useEffect(() => { + setLocalGw(gateway ? structuredClone(gateway) : null) + setIsDirty(false) + }, [selectedElementId, gateways]) + + function targetLabel(targetId: string): string { + // Primero buscar en actividades (tienen nombre más descriptivo) + const act = activities.find((a) => a.bpmnElementId === targetId) + if (act?.name) return act.name + // Luego en el mapa del grafo + const fromGraph = elementNames.get(targetId) + if (fromGraph) return fromGraph + return targetId + } + + function setProbability(branchIndex: number, value: number) { + if (!localGw) return + const updated = structuredClone(localGw) + updated.branches[branchIndex].probability = value + setLocalGw(updated) + setIsDirty(true) + } + + function distributeUniformly() { + if (!localGw) return + const n = localGw.branches.length + const equalProb = parseFloat((1 / n).toFixed(4)) + const updated = structuredClone(localGw) + updated.branches = updated.branches.map((b, i) => ({ + ...b, + probability: i === n - 1 ? parseFloat((1 - equalProb * (n - 1)).toFixed(4)) : equalProb, + })) + setLocalGw(updated) + setIsDirty(true) + } + + async function handleSave() { + if (!localGw) return + await updateGateway(localGw) + setIsDirty(false) + } + + // ── Empty state ────────────────────────────────────────────────────────── + if (!selectedElementId) { + return ( +
+
+ +
+

Seleccioná un gateway

+

Hacé click sobre un gateway en el diagrama para configurar sus probabilidades

+
+ ) + } + + if (!localGw) { + return ( +
+

Gateway sin configuración

+
+ ) + } + + // ── AND Paralelo: informativo, sin sliders ──────────────────────────────── + if (localGw.gatewayType === 'parallel') { + return ( + +
+
+
+ Gateway Paralelo (AND) +
+

{localGw.bpmnElementId}

+
+ +
+
+ +
+

Ejecución paralela

+

+ En un gateway AND todas las salidas se ejecutan simultáneamente. + No hay probabilidades configurables — la probabilidad de cada rama + es igual a la probabilidad de llegada al gateway. +

+
+
+
+ +
+ + {localGw.branches.map((branch) => ( +
+
+

{targetLabel(branch.targetElementId)}

+

{branch.flowId}

+
+ 100% +
+ ))} +
+
+
+ ) + } + + // ── XOR Exclusivo / OR Inclusivo (con aviso) ────────────────────────────── + const isInclusive = localGw.gatewayType === 'inclusive' + const currentSum = sumBranches(localGw) + const xorValid = isInclusive || Math.abs(currentSum - 1.0) <= XOR_SUM_TOLERANCE + const sumColor = xorValid ? 'text-green-600' : 'text-destructive' + + return ( + +
+ {/* Header */} +
+ + {isInclusive ? 'Gateway Inclusivo (OR)' : 'Gateway Exclusivo (XOR)'} + +

{localGw.bpmnElementId}

+
+ + {/* Aviso para OR inclusivo */} + {isInclusive && ( +
+ +

+ Gateway Inclusivo (OR) — En el MVP se simula como + XOR: solo una rama se toma por instancia. Las probabilidades deben sumar 1. + El soporte OR nativo llega en V1.0. +

+
+ )} + + {/* Suma actual + botón distribuir */} +
+
+ Suma de probabilidades: + + {formatPercent(currentSum * 100, 1)} + + {!xorValid && ( + + + + + + La suma debe ser exactamente 100% para poder simular + + + )} + {xorValid && ( + + + + + + Suma 100% — listo para simular + + + )} +
+ + + + + + Reparte el 100% en partes iguales entre todas las ramas del gateway + + +
+ + {/* Sliders por rama */} +
+ {localGw.branches.map((branch, idx) => ( +
+
+
+

+ {targetLabel(branch.targetElementId)} +

+

{branch.flowId}

+
+ + {formatPercent(branch.probability * 100, 1)} + +
+ setProbability(idx, parseFloat((v / 100).toFixed(4)))} + /> +
+ ))} +
+ + {/* Error de suma — banner prominente */} + {!xorValid && ( +
+ +

+ La suma es {formatPercent(currentSum * 100, 1)} — debe ser 100%. + Ajustá los sliders o usá "Distribuir" para repartir uniformemente. + El botón Simular quedará deshabilitado hasta corregirlo. +

+
+ )} + + +
+
+ ) +} diff --git a/src/features/workspace/GlobalSettingsPanel.tsx b/src/features/workspace/GlobalSettingsPanel.tsx new file mode 100644 index 0000000..d0671f4 --- /dev/null +++ b/src/features/workspace/GlobalSettingsPanel.tsx @@ -0,0 +1,140 @@ +import { useState, useEffect } from 'react' +import { Info } from 'lucide-react' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Slider } from '@/components/ui/slider' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { useProcessStore } from '@/store/process-store' +import { formatPercent } from '@/lib/format' + +const CURRENCIES = [ + { code: 'USD', label: 'USD — Dólar estadounidense' }, + { code: 'PYG', label: 'PYG — Guaraní paraguayo' }, + { code: 'BRL', label: 'BRL — Real brasileño' }, + { code: 'ARS', label: 'ARS — Peso argentino' }, + { code: 'EUR', label: 'EUR — Euro' }, + { code: 'COP', label: 'COP — Peso colombiano' }, + { code: 'MXN', label: 'MXN — Peso mexicano' }, + { code: 'CLP', label: 'CLP — Peso chileno' }, +] + +export function GlobalSettingsPanel() { + const { currentProcess, updateProcess } = useProcessStore() + const [localName, setLocalName] = useState('') + const [localClient, setLocalClient] = useState('') + const [localCurrency, setLocalCurrency] = useState('USD') + const [localOverhead, setLocalOverhead] = useState(20) + const [isDirty, setIsDirty] = useState(false) + + useEffect(() => { + if (!currentProcess) return + setLocalName(currentProcess.name) + setLocalClient(currentProcess.clientName) + setLocalCurrency(currentProcess.currency) + setLocalOverhead(Math.round(currentProcess.overheadPercentage * 100)) + setIsDirty(false) + }, [currentProcess]) + + function markDirty() { setIsDirty(true) } + + async function handleSave() { + await updateProcess({ + name: localName.trim() || 'Proceso sin nombre', + clientName: localClient.trim(), + currency: localCurrency, + overheadPercentage: localOverhead / 100, + }) + setIsDirty(false) + } + + if (!currentProcess) return null + + return ( + +
+ {/* Nombre del proceso */} +
+ + { setLocalName(e.target.value); markDirty() }} + placeholder="ej. Proceso de ventas" + className="h-8 text-sm" + /> +
+ + {/* Cliente */} +
+ + { setLocalClient(e.target.value); markDirty() }} + placeholder="ej. Empresa ABC" + className="h-8 text-sm" + /> +
+ + {/* Moneda */} +
+ + +
+ + {/* Overhead */} +
+
+ + + + + + + Porcentaje adicional sobre los costos directos que representa los costos indirectos: alquiler, administración, infraestructura general. + + +
+
+ { setLocalOverhead(v); markDirty() }} + className="flex-1" + /> + + {formatPercent(localOverhead, 0)} + +
+

+ Típicamente entre 15% y 40% para empresas de servicios +

+
+ + {/* Guardar */} + +
+
+ ) +} diff --git a/src/features/workspace/ResourcesPanel.tsx b/src/features/workspace/ResourcesPanel.tsx new file mode 100644 index 0000000..1b833f8 --- /dev/null +++ b/src/features/workspace/ResourcesPanel.tsx @@ -0,0 +1,201 @@ +import { useState } from 'react' +import { PlusCircle, Pencil, Trash2, Check, X } from 'lucide-react' +import { v4 as uuidv4 } from 'uuid' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { ScrollArea } from '@/components/ui/scroll-area' +import { useProcessStore } from '@/store/process-store' +import { formatCurrency } from '@/lib/format' +import type { Resource, ResourceType } from '@/domain/types' + +const RESOURCE_TYPE_LABELS: Record = { + role: 'Rol', + person: 'Persona', + system: 'Sistema', + equipment: 'Equipo', + supply: 'Insumo', +} + +const RESOURCE_TYPE_COLORS: Record = { + role: 'bg-blue-100 text-blue-700', + person: 'bg-green-100 text-green-700', + system: 'bg-purple-100 text-purple-700', + equipment: 'bg-amber-100 text-amber-700', + supply: 'bg-slate-100 text-slate-700', +} + +interface ResourceFormState { + name: string + type: ResourceType + costPerHour: string +} + +const EMPTY_FORM: ResourceFormState = { name: '', type: 'role', costPerHour: '' } + +export function ResourcesPanel() { + const { currentProcess, resources, addResource, updateResource, deleteResource } = useProcessStore() + const [showForm, setShowForm] = useState(false) + const [editingId, setEditingId] = useState(null) + const [form, setForm] = useState(EMPTY_FORM) + + function startAdd() { + setForm(EMPTY_FORM) + setEditingId(null) + setShowForm(true) + } + + function startEdit(resource: Resource) { + setForm({ name: resource.name, type: resource.type, costPerHour: resource.costPerHour.toString() }) + setEditingId(resource.id) + setShowForm(true) + } + + function cancelForm() { + setShowForm(false) + setEditingId(null) + setForm(EMPTY_FORM) + } + + async function handleSubmit() { + if (!form.name.trim() || !currentProcess) return + const costPerHour = parseFloat(form.costPerHour) || 0 + + if (editingId) { + const existing = resources.find((r) => r.id === editingId) + if (!existing) return + await updateResource({ ...existing, name: form.name.trim(), type: form.type, costPerHour }) + } else { + const newResource: Resource = { + id: uuidv4(), + processId: currentProcess.id, + name: form.name.trim(), + type: form.type, + costPerHour, + } + await addResource(newResource) + } + cancelForm() + } + + return ( + +
+ {/* Header */} +
+
+

Recursos del proceso

+

{resources.length} definido{resources.length !== 1 ? 's' : ''}

+
+ {!showForm && ( + + )} +
+ + {/* Formulario */} + {showForm && ( +
+

{editingId ? 'Editar recurso' : 'Nuevo recurso'}

+ +
+ + setForm((f) => ({ ...f, name: e.target.value }))} + className="h-8 text-xs" + /> +
+ +
+ + +
+ +
+ + setForm((f) => ({ ...f, costPerHour: e.target.value }))} + className="h-8 text-xs font-mono" + /> +
+ +
+ + +
+
+ )} + + {/* Lista */} + {resources.length === 0 && !showForm ? ( +
+

Sin recursos definidos

+

Los recursos (roles, sistemas, equipos) se asignan a actividades para calcular su costo por hora

+
+ ) : ( +
+ {resources.map((resource) => ( +
+
+
+

{resource.name}

+ + {RESOURCE_TYPE_LABELS[resource.type]} + +
+

{formatCurrency(resource.costPerHour)}/hora

+
+
+ + +
+
+ ))} +
+ )} +
+
+ ) +} diff --git a/src/features/workspace/WorkspacePage.tsx b/src/features/workspace/WorkspacePage.tsx new file mode 100644 index 0000000..79eb6a1 --- /dev/null +++ b/src/features/workspace/WorkspacePage.tsx @@ -0,0 +1,293 @@ +import { useEffect, useRef, useState, useCallback, useMemo } from 'react' +import { useParams, useNavigate } from '@tanstack/react-router' +import { + Loader2, BarChart3, Play, AlertTriangle, + PanelRightClose, PanelRightOpen, Home, GitMerge, MousePointer2 +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip' +import { Separator } from '@/components/ui/separator' +import { useToast } from '@/components/ui/use-toast' +import { BpmnCanvas, type BpmnElementCategory } from './BpmnCanvas' +import { ActivityPanel } from './ActivityPanel' +import { GatewayPanel, isXorSumValid } from './GatewayPanel' +import { ResourcesPanel } from './ResourcesPanel' +import { GlobalSettingsPanel } from './GlobalSettingsPanel' +import { useProcessStore } from '@/store/process-store' +import { useSimulationStore } from '@/store/simulation-store' +import { useSimulate } from '../simulation/useSimulate' + +type SelectedCategory = 'activity' | 'gateway' | null + +export function WorkspacePage() { + const { processId } = useParams({ from: '/workspace/$processId' }) + const navigate = useNavigate() + const { currentProcess, isLoading, error, loadProcess, gateways } = useProcessStore() + const { isRunning, result, error: simError, selectActivity } = useSimulationStore() + const { simulate } = useSimulate() + const { toast } = useToast() + + const [sidebarOpen, setSidebarOpen] = useState(true) + const [selectedElementId, setSelectedElementId] = useState(null) + const [selectedElementName, setSelectedElementName] = useState('') + const [selectedCategory, setSelectedCategory] = useState(null) + const [activeTab, setActiveTab] = useState('elemento') + + // Usamos un ref para saber si loadProcess ya terminó al menos una vez. + // Esto evita que el redirect se dispare en el render inicial donde + // isLoading=false y currentProcess=null (antes del primer efecto). + const hasLoadedOnce = useRef(false) + + useEffect(() => { loadProcess(processId) }, [processId, loadProcess]) + + useEffect(() => { + if (isLoading) { hasLoadedOnce.current = true; return } + if (!hasLoadedOnce.current) return // carga no iniciada aún + if ((error || !currentProcess) && processId) { + toast({ + title: 'Proceso no encontrado', + description: 'El proceso que buscás no existe o fue eliminado.', + variant: 'destructive', + }) + navigate({ to: '/' }) + } + }, [isLoading, error, currentProcess, processId, toast, navigate]) + + // Validación XOR: todos los gateways exclusivos deben sumar 1.0 + const xorValidation = useMemo(() => { + const invalidIds = gateways + .filter((gw) => gw.gatewayType === 'exclusive' && !isXorSumValid(gw)) + .map((gw) => gw.bpmnElementId) + return { valid: invalidIds.length === 0, invalidIds } + }, [gateways]) + + const canSimulate = !isRunning && xorValidation.valid + + const handleElementClick = useCallback(( + elementId: string, + elementName: string, + category: BpmnElementCategory + ) => { + if (category === 'other') return + setSelectedElementId(elementId) + setSelectedElementName(elementName) + setSelectedCategory(category) + selectActivity(category === 'activity' ? elementId : null) + setActiveTab('elemento') + if (!sidebarOpen) setSidebarOpen(true) + }, [selectActivity, sidebarOpen]) + + function clearSelection() { + setSelectedElementId(null) + setSelectedElementName('') + setSelectedCategory(null) + selectActivity(null) + } + + async function handleSimulate() { + if (!canSimulate) return + const simResult = await simulate() + if (simResult) navigate({ to: '/report/$processId', params: { processId } }) + } + + if (isLoading) { + return ( +
+ +
+ ) + } + + if (error || !currentProcess) { + return null // el useEffect ya inició el redirect con toast + } + + // Label del tab "elemento" según lo seleccionado + const elementTabLabel = selectedCategory === 'gateway' ? 'Gateway' : 'Actividad' + const elementTabIcon = selectedCategory === 'gateway' + ? + : + + return ( + +
+ + {/* ── Topbar ─────────────────────────────────────────────────── */} +
+ + + + + Inicio + + + + +
+

{currentProcess.name}

+ {currentProcess.clientName && ( +

{currentProcess.clientName}

+ )} +
+ + {/* Errores de simulación */} + {simError && ( +
+ + {simError} +
+ )} + + {/* Advertencia de XOR inválido */} + {!xorValidation.valid && ( + + +
+ + {xorValidation.invalidIds.length} gateway{xorValidation.invalidIds.length > 1 ? 's' : ''} con probabilidades inválidas +
+
+ +

Gateways XOR que no suman 100%:

+ {xorValidation.invalidIds.map((id) => ( +

• {id}

+ ))} +

Ajustá las probabilidades para habilitar la simulación

+
+
+ )} + + {/* Ver reporte si ya hay simulación */} + {result && ( + + )} + + {/* Simular */} + + + + + + + {!xorValidation.valid && ( + + Corregí las probabilidades de los gateways XOR primero + + )} + + + + + + + + + {sidebarOpen ? 'Cerrar panel' : 'Abrir panel'} + +
+ + {/* ── Main ───────────────────────────────────────────────────── */} +
+ + {/* Canvas BPMN */} +
+ + + {/* Chip de elemento seleccionado */} + {selectedElementId && ( +
+
+ {selectedElementName || selectedElementId} + +
+ )} + + {/* Hint inicial */} + {!selectedElementId && ( +
+

Hacé click en una tarea o gateway para configurarlo

+
+ )} +
+ + {/* Panel lateral */} +
+ {sidebarOpen && ( + + + + {elementTabIcon}{elementTabLabel} + + Recursos + Global + + + + {selectedCategory === 'gateway' + ? + : + } + + + + + + + + + + + )} +
+ + {/* Toggle flotante cuando sidebar cerrado */} + {!sidebarOpen && ( + + )} +
+
+ + ) +} diff --git a/src/lib/chart-options.ts b/src/lib/chart-options.ts new file mode 100644 index 0000000..9dfd18b --- /dev/null +++ b/src/lib/chart-options.ts @@ -0,0 +1,242 @@ +import { activityColor, type HeatmapMode } from './colors' +import { formatCurrency } from './format' +import type { ActivitySimResult, Resource, ResourceType, SimulationResult } from '@/domain/types' + +// Nombres y colores por tipo de recurso — única fuente de verdad +export const RESOURCE_TYPE_LABELS: Record = { + role: 'Roles', person: 'Personas', system: 'Sistemas', + equipment: 'Equipos', supply: 'Insumos', +} + +export const RESOURCE_TYPE_COLORS: Record = { + role: '#3b82f6', person: '#10b981', system: '#8b5cf6', + equipment: '#f59e0b', supply: '#64748b', +} + +function truncate(s: string, max: number): string { + return s.length > max ? s.slice(0, max - 1) + '…' : s +} + +// ─── Top actividades por costo — barras horizontales ───────────────────────── + +export function buildTopActivitiesOption( + perActivity: ActivitySimResult[], + mode: HeatmapMode, + currency: string +): Record { + const top10 = perActivity.slice(0, 10) + + return { + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' }, + formatter: (params: unknown[]) => { + const p = params[0] as { dataIndex: number; value: number } + const act = top10[p.dataIndex] + return `
+
${act.activityName}
+
Costo esperado: ${formatCurrency(act.expectedTotalCost, currency)}
+
% del total: ${act.percentOfTotal.toFixed(1)}%
+
Directo: ${formatCurrency(act.expectedDirectCost, currency)}
+
Indirecto: ${formatCurrency(act.expectedIndirectCost, currency)}
+
` + }, + }, + grid: { left: 12, right: 24, top: 8, bottom: 8, containLabel: true }, + xAxis: { + type: 'value', + axisLabel: { + fontSize: 11, + formatter: (v: number) => { + if (v === 0) return '0' + if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M` + if (v >= 1_000) return `${(v / 1_000).toFixed(0)}k` + return String(v) + }, + }, + splitLine: { lineStyle: { color: '#f1f5f9' } }, + }, + yAxis: { + type: 'category', + data: top10.map((a) => truncate(a.activityName, 28)), + inverse: true, + axisLabel: { fontSize: 11, color: '#475569', width: 160, overflow: 'truncate' }, + axisTick: { show: false }, + axisLine: { show: false }, + }, + series: [{ + type: 'bar', + data: top10.map((a) => ({ + value: a.expectedTotalCost, + itemStyle: { color: activityColor(a, perActivity, mode) }, + })), + barMaxWidth: 28, + label: { + show: true, + position: 'right', + fontSize: 10, + color: '#64748b', + formatter: (p: { value: number }) => formatCurrency(p.value, currency), + }, + }], + } +} + +// ─── Composición directo / indirecto — donut ────────────────────────────────── + +export function buildCostCompositionOption( + result: Pick, + currency: string +): Record { + const hasIndirect = result.totalIndirectCost > 0 + + return { + tooltip: { + trigger: 'item', + formatter: (p: { name: string; value: number; percent: number }) => + `
+
${p.name}
+
${formatCurrency(p.value, currency)} (${p.percent.toFixed(1)}%)
+
`, + }, + legend: { + bottom: '2%', + left: 'center', + itemWidth: 12, + itemHeight: 12, + textStyle: { fontSize: 11, color: '#475569' }, + }, + series: [{ + type: 'pie', + radius: ['48%', '68%'], + center: ['50%', '44%'], + avoidLabelOverlap: true, + itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 }, + label: { + show: true, + fontSize: 11, + formatter: (p: { percent: number }) => `${p.percent.toFixed(1)}%`, + }, + labelLine: { length: 10, length2: 8 }, + emphasis: { label: { fontSize: 13, fontWeight: 'bold' } }, + data: [ + { value: result.totalDirectCost, name: 'Costo directo', itemStyle: { color: '#3b82f6' } }, + ...(hasIndirect ? [{ value: result.totalIndirectCost, name: 'Costo indirecto (overhead)', itemStyle: { color: '#94a3b8' } }] : []), + ], + }], + graphic: [{ + type: 'text', + left: 'center', + top: '38%', + style: { + text: formatCurrency(result.totalCost, currency), + textAlign: 'center', + fontSize: 13, + fontWeight: 'bold', + fill: '#1e293b', + fontFamily: 'JetBrains Mono, monospace', + }, + }], + } +} + +// ─── Costo por tipo de recurso — barras verticales ──────────────────────────── + +interface ResourceTypeTotals { + byType: Partial> + byTypeDetails: Partial> + activeTypes: ResourceType[] +} + +export function aggregateResourceCosts( + perActivity: ActivitySimResult[], + resources: Resource[] +): ResourceTypeTotals { + const resourceById = new Map(resources.map((r) => [r.id, r])) + const byType: Partial> = {} + const byTypeDetails: Partial> = {} + + for (const act of perActivity) { + for (const rb of act.resourceCostBreakdown) { + if (rb.cost <= 0) continue + const res = resourceById.get(rb.resourceId) + if (!res) continue + byType[res.type] = (byType[res.type] ?? 0) + rb.cost + if (!byTypeDetails[res.type]) byTypeDetails[res.type] = [] + const existing = byTypeDetails[res.type]!.find((d) => d.name === res.name) + if (existing) existing.cost += rb.cost + else byTypeDetails[res.type]!.push({ name: res.name, cost: rb.cost }) + } + } + + const activeTypes = (Object.keys(RESOURCE_TYPE_LABELS) as ResourceType[]).filter( + (t) => (byType[t] ?? 0) > 0 + ) + + return { byType, byTypeDetails, activeTypes } +} + +export function buildCostByResourceOption( + perActivity: ActivitySimResult[], + resources: Resource[], + currency: string +): Record | null { + const { byType, byTypeDetails, activeTypes } = aggregateResourceCosts(perActivity, resources) + if (activeTypes.length === 0) return null + + return { + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' }, + formatter: (params: unknown[]) => { + const p = params[0] as { dataIndex: number; value: number } + const type = activeTypes[p.dataIndex] + const dets = byTypeDetails[type] ?? [] + const detHtml = dets + .sort((a, b) => b.cost - a.cost) + .map((d) => `
• ${d.name}: ${formatCurrency(d.cost, currency)}
`) + .join('') + return `
+
${RESOURCE_TYPE_LABELS[type]}
+
Total: ${formatCurrency(p.value, currency)}
${detHtml} +
` + }, + }, + grid: { left: 12, right: 12, top: 12, bottom: 8, containLabel: true }, + xAxis: { + type: 'category', + data: activeTypes.map((t) => RESOURCE_TYPE_LABELS[t]), + axisLabel: { fontSize: 11, color: '#475569' }, + axisTick: { show: false }, + axisLine: { lineStyle: { color: '#e2e8f0' } }, + }, + yAxis: { + type: 'value', + axisLabel: { + fontSize: 10, + color: '#94a3b8', + formatter: (v: number) => { + if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M` + if (v >= 1_000) return `${(v / 1_000).toFixed(0)}k` + return String(v) + }, + }, + splitLine: { lineStyle: { color: '#f1f5f9' } }, + }, + series: [{ + type: 'bar', + data: activeTypes.map((t) => ({ + value: byType[t] ?? 0, + itemStyle: { color: RESOURCE_TYPE_COLORS[t], borderRadius: [4, 4, 0, 0] }, + })), + barMaxWidth: 48, + label: { + show: true, + position: 'top', + fontSize: 10, + color: '#64748b', + formatter: (p: { value: number }) => formatCurrency(p.value, currency), + }, + }], + } +} diff --git a/src/lib/colors.ts b/src/lib/colors.ts new file mode 100644 index 0000000..e94dd0d --- /dev/null +++ b/src/lib/colors.ts @@ -0,0 +1,149 @@ +// Paleta heatmap: verde → amarillo → rojo +const HEATMAP_LOW = { r: 16, g: 185, b: 129 } // #10b981 +const HEATMAP_MID = { r: 245, g: 158, b: 11 } // #f59e0b +const HEATMAP_HIGH = { r: 239, g: 68, b: 68 } // #ef4444 + +function lerp(a: number, b: number, t: number): number { + return Math.round(a + (b - a) * t) +} + +// t en [0, 1] +export function heatmapColor(t: number): string { + const clamped = Math.max(0, Math.min(1, t)) + let r: number, g: number, b: number + + if (clamped <= 0.5) { + const localT = clamped * 2 + r = lerp(HEATMAP_LOW.r, HEATMAP_MID.r, localT) + g = lerp(HEATMAP_LOW.g, HEATMAP_MID.g, localT) + b = lerp(HEATMAP_LOW.b, HEATMAP_MID.b, localT) + } else { + const localT = (clamped - 0.5) * 2 + r = lerp(HEATMAP_MID.r, HEATMAP_HIGH.r, localT) + g = lerp(HEATMAP_MID.g, HEATMAP_HIGH.g, localT) + b = lerp(HEATMAP_MID.b, HEATMAP_HIGH.b, localT) + } + + return `rgb(${r}, ${g}, ${b})` +} + +export function heatmapColorHex(t: number): string { + const clamped = Math.max(0, Math.min(1, t)) + let r: number, g: number, b: number + + if (clamped <= 0.5) { + const localT = clamped * 2 + r = lerp(HEATMAP_LOW.r, HEATMAP_MID.r, localT) + g = lerp(HEATMAP_LOW.g, HEATMAP_MID.g, localT) + b = lerp(HEATMAP_LOW.b, HEATMAP_MID.b, localT) + } else { + const localT = (clamped - 0.5) * 2 + r = lerp(HEATMAP_MID.r, HEATMAP_HIGH.r, localT) + g = lerp(HEATMAP_MID.g, HEATMAP_HIGH.g, localT) + b = lerp(HEATMAP_MID.b, HEATMAP_HIGH.b, localT) + } + + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}` +} + +// Retorna opacidad para colores con alpha (útil para overlays sobre bpmn-js) +export function heatmapColorWithAlpha(t: number, alpha = 0.75): string { + const clamped = Math.max(0, Math.min(1, t)) + let r: number, g: number, b: number + + if (clamped <= 0.5) { + const localT = clamped * 2 + r = lerp(HEATMAP_LOW.r, HEATMAP_MID.r, localT) + g = lerp(HEATMAP_LOW.g, HEATMAP_MID.g, localT) + b = lerp(HEATMAP_LOW.b, HEATMAP_MID.b, localT) + } else { + const localT = (clamped - 0.5) * 2 + r = lerp(HEATMAP_MID.r, HEATMAP_HIGH.r, localT) + g = lerp(HEATMAP_MID.g, HEATMAP_HIGH.g, localT) + b = lerp(HEATMAP_MID.b, HEATMAP_HIGH.b, localT) + } + + return `rgba(${r}, ${g}, ${b}, ${alpha})` +} + +// Determina si el texto sobre el color debe ser claro u oscuro (WCAG contrast) +export function getContrastText(t: number): 'white' | 'black' { + return t > 0.5 ? 'white' : 'black' +} + +// ─── Helpers para el heatmap del reporte ────────────────────────────────────── + +export type HeatmapMode = 'relative' | 'absolute' + +export interface ActivityCostData { + expectedTotalCost: number + percentOfTotal: number +} + +// Color neutro (gris claro) cuando no hay varianza significativa entre actividades. +// Evita comunicar una falsa diferenciación cuando todos los costos son similares. +export const NEUTRAL_HEATMAP_COLOR = '#cbd5e1' // slate-300 + +// Umbral: si el rango relativo (max-min)/max < 5%, la varianza no es significativa. +export const VARIANCE_THRESHOLD = 0.05 + +export function hasSignificantCostVariance(activities: ActivityCostData[]): boolean { + if (activities.length < 2) return false + const costs = activities.map((a) => a.expectedTotalCost) + const max = Math.max(...costs) + const min = Math.min(...costs) + if (max <= 0) return false + return (max - min) / max > VARIANCE_THRESHOLD +} + +// Calcula t ∈ [0,1] para una actividad dada la lista completa y el modo. +// Modo relativo: normaliza sobre la actividad más costosa (t=1 = la que más pesa). +// Modo absoluto: normaliza sobre el rango min-max del proceso. +// Pre-condición: llamar solo cuando hasSignificantCostVariance = true. +export function computeActivityT( + activity: ActivityCostData, + allActivities: ActivityCostData[], + mode: HeatmapMode +): number { + if (allActivities.length === 0) return 0 + + if (mode === 'relative') { + const maxPercent = Math.max(...allActivities.map((a) => a.percentOfTotal), 0.001) + return Math.max(0, Math.min(1, activity.percentOfTotal / maxPercent)) + } + + const costs = allActivities.map((a) => a.expectedTotalCost) + const minCost = Math.min(...costs) + const maxCost = Math.max(...costs) + if (maxCost <= minCost) return 0.5 + return Math.max(0, Math.min(1, (activity.expectedTotalCost - minCost) / (maxCost - minCost))) +} + +// Función unificada: devuelve el color correcto considerando varianza. +// Usar en HeatmapCanvas, ActivitiesTable y los gráficos para consistencia. +export function activityColor( + activity: ActivityCostData, + allActivities: ActivityCostData[], + mode: HeatmapMode +): string { + if (!hasSignificantCostVariance(allActivities)) return NEUTRAL_HEATMAP_COLOR + return heatmapColorHex(computeActivityT(activity, allActivities, mode)) +} + +// Valores de los extremos para la leyenda del heatmap +export function heatmapLegendBounds( + allActivities: ActivityCostData[], + mode: HeatmapMode +): { min: number; mid: number; max: number; unit: 'currency' | 'percent' } { + if (allActivities.length === 0) return { min: 0, mid: 50, max: 100, unit: 'percent' } + + if (mode === 'relative') { + const maxPercent = Math.max(...allActivities.map((a) => a.percentOfTotal)) + return { min: 0, mid: maxPercent / 2, max: maxPercent, unit: 'percent' } + } + + const costs = allActivities.map((a) => a.expectedTotalCost) + const minCost = Math.min(...costs) + const maxCost = Math.max(...costs) + return { min: minCost, mid: (minCost + maxCost) / 2, max: maxCost, unit: 'currency' } +} diff --git a/src/lib/export/csv-export.ts b/src/lib/export/csv-export.ts new file mode 100644 index 0000000..3724bdf --- /dev/null +++ b/src/lib/export/csv-export.ts @@ -0,0 +1,115 @@ +import Papa from 'papaparse' +import { formatSimulationTimestamp } from '@/lib/format' +import { buildFileName } from './slug' +import type { Process, Simulation, Resource, ActivitySimResult } from '@/domain/types' + +// Monedas que usan coma como separador decimal (necesitan punto y coma en el CSV) +const COMMA_DECIMAL_CURRENCIES = new Set(['PYG', 'BRL', 'EUR', 'ARS', 'COP', 'MXN', 'CLP']) + +function getDelimiter(currency: string): string { + return COMMA_DECIMAL_CURRENCIES.has(currency) ? ';' : ',' +} + +function formatDecimalForCsv(value: number, currency: string): string { + const formatted = value.toFixed(2) + if (COMMA_DECIMAL_CURRENCIES.has(currency)) { + return formatted.replace('.', ',') + } + return formatted +} + +function buildResourcesCell( + act: ActivitySimResult, + resources: Resource[] +): string { + if (act.resourceCostBreakdown.length === 0) return '' + const resourceById = new Map(resources.map((r) => [r.id, r])) + return act.resourceCostBreakdown + .map((rb) => { + const res = resourceById.get(rb.resourceId) + if (!res) return rb.resourceName + // Calcular % de utilización desde el costo relativo + return res.name + }) + .join('; ') +} + +export interface CsvExportParams { + process: Process + simulation: Simulation + resources: Resource[] +} + +export function generateCsvContent(params: CsvExportParams): string { + const { process, simulation, resources } = params + const { currency } = process + const result = simulation.result + const delimiter = getDelimiter(currency) + const { date } = formatSimulationTimestamp(simulation.executedAt) + + // ── Filas de metadata como comentarios ────────────────────────────────── + const meta = [ + `# Proceso: ${process.name}`, + `# Cliente: ${process.clientName || 'N/A'}`, + `# Fecha simulación: ${date}`, + `# Moneda: ${currency}`, + `# Costo total (${currency}): ${formatDecimalForCsv(result.totalCost, currency)}`, + `# Costo directo: ${formatDecimalForCsv(result.totalDirectCost, currency)}`, + `# Costo indirecto (overhead ${(process.overheadPercentage * 100).toFixed(0)}%): ${formatDecimalForCsv(result.totalIndirectCost, currency)}`, + '', + ].join('\r\n') + + // ── Headers ────────────────────────────────────────────────────────────── + const headers = [ + 'ID BPMN', + 'Actividad', + 'Tipo', + `Costo directo (${currency})`, + `Costo indirecto (${currency})`, + `Costo total (${currency})`, + '% del total', + 'Tiempo (min)', + 'Ejecuciones esperadas', + 'Recursos asignados', + ] + + // ── Filas ───────────────────────────────────────────────────────────────── + const rows = result.perActivity.map((act) => [ + act.bpmnElementId, + act.activityName, + act.bpmnElementId.includes('subprocess') ? 'subproceso' : 'tarea', + formatDecimalForCsv(act.expectedDirectCost, currency), + formatDecimalForCsv(act.expectedIndirectCost, currency), + formatDecimalForCsv(act.expectedTotalCost, currency), + formatDecimalForCsv(act.percentOfTotal, currency), + formatDecimalForCsv(act.executionTimeMinutes, currency), + formatDecimalForCsv(act.executionProbability * 100, currency), + buildResourcesCell(act, resources), + ]) + + const csvBody = Papa.unparse( + { fields: headers, data: rows }, + { delimiter, newline: '\r\n', quotes: true } + ) + + // BOM UTF-8 + metadata + datos + return '' + meta + csvBody +} + +export function exportToCsv(params: CsvExportParams): void { + const content = generateCsvContent(params) + const fileName = buildFileName( + params.process.name, + params.process.clientName, + new Date(params.simulation.executedAt), + 'csv' + ) + + const blob = new Blob([content], { type: 'text/csv;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = fileName + a.click() + URL.revokeObjectURL(url) +} diff --git a/src/lib/export/pdf-export.ts b/src/lib/export/pdf-export.ts new file mode 100644 index 0000000..48793de --- /dev/null +++ b/src/lib/export/pdf-export.ts @@ -0,0 +1,131 @@ +import { buildFileName } from './slug' +import { + drawHeader, drawKpiCards, drawHeatmapImage, drawHeatmapLegend, + drawAnalysisSection, drawMethodologySection, addPageNumbers, PDF, +} from './pdf-sections' +import type { Process, Simulation, Resource } from '@/domain/types' + +export interface PdfExportParams { + process: Process + simulation: Simulation + resources: Resource[] + heatmapImageData: string | null // data URL de la imagen capturada del canvas +} + +export async function exportToPdf(params: PdfExportParams): Promise { + const { process, simulation, resources, heatmapImageData } = params + const result = simulation.result + const currency = process.currency + + // Carga dinámica: jsPDF + autotable solo se descargan al hacer click + const [{ jsPDF }, { default: autoTable }] = await Promise.all([ + import('jspdf'), + import('jspdf-autotable'), + ]) + + const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }) + + // Metadata del PDF + doc.setProperties({ + title: `Análisis de costos - ${process.name}`, + author: process.clientName || '', + subject: 'Simulación de proceso BPMN', + creator: 'Process Cost Platform', + keywords: 'bpmn, costos, simulación', + }) + + // ── PÁGINA 1: Header + KPIs + Heatmap ──────────────────────────────────── + let y = drawHeader(doc as any, process, simulation.executedAt) + y = drawKpiCards(doc as any, result, currency, y) + y += 4 + + // Título del heatmap + doc.setFont('helvetica', 'bold') + doc.setFontSize(10) + doc.setTextColor(...PDF.slate900) + doc.text('Mapa de calor de costos', PDF.margin, y) + y += 5 + + y = drawHeatmapImage(doc as any, heatmapImageData, y) + y = drawHeatmapLegend(doc as any, y) + y += 4 + + // ── PÁGINA 2: Análisis ──────────────────────────────────────────────────── + doc.addPage() + y = PDF.margin + y = drawAnalysisSection(doc as any, result, currency, y) + y += 6 + + // ── PÁGINA 3+: Tabla detalle (autotable maneja paginación automática) ───── + void resources // disponible para uso futuro (recursos en tabla de actividades) + + const RA = 'right' as const + + const tableHead = [ + [ + { content: 'Actividad', styles: { cellWidth: 48 } }, + { content: `Dir. (${currency})`, styles: { halign: RA, cellWidth: 25 } }, + { content: `Indir. (${currency})`, styles: { halign: RA, cellWidth: 25 } }, + { content: `Total (${currency})`, styles: { halign: RA, cellWidth: 28 } }, + { content: '% total', styles: { halign: RA, cellWidth: 16 } }, + { content: 'Tiempo', styles: { halign: RA, cellWidth: 14 } }, + { content: 'Prob.', styles: { halign: RA, cellWidth: 14 } }, + ], + ] + + const tableBody = result.perActivity.map((act) => [ + { content: act.activityName }, + { content: formatNum(act.expectedDirectCost), styles: { halign: RA, font: 'courier' } }, + { content: formatNum(act.expectedIndirectCost), styles: { halign: RA, font: 'courier' } }, + { content: formatNum(act.expectedTotalCost), styles: { halign: RA, font: 'courier' } }, + { content: `${act.percentOfTotal.toFixed(1)}%`, styles: { halign: RA, font: 'courier' } }, + { content: act.executionTimeMinutes > 0 ? formatTime(act.executionTimeMinutes) : '—', styles: { halign: RA } }, + { content: `${(act.executionProbability * 100).toFixed(0)}%`, styles: { halign: RA } }, + ]) + + autoTable(doc, { + head: tableHead, + body: tableBody, + startY: y, + margin: { left: PDF.margin, right: PDF.margin }, + styles: { fontSize: 8, cellPadding: 2.5, overflow: 'ellipsize' }, + headStyles: { + fillColor: PDF.blue, + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 8, + }, + alternateRowStyles: { fillColor: PDF.slate50 }, + columnStyles: { 0: { cellWidth: 48 } }, + didDrawPage: () => { + // Placeholder — los footers se añaden al final con addPageNumbers + }, + }) + + // ── PÁGINA FINAL: Metodología ──────────────────────────────────────────── + doc.addPage() + drawMethodologySection(doc as any, process, PDF.margin) + + // ── FOOTER en todas las páginas ─────────────────────────────────────────── + addPageNumbers(doc as any, 'Process Cost Platform', simulation.executedAt) + + // ── Descargar ───────────────────────────────────────────────────────────── + const fileName = buildFileName( + process.name, + process.clientName, + new Date(simulation.executedAt), + 'pdf' + ) + doc.save(fileName) +} + +function formatNum(n: number): string { + return n.toLocaleString('es-PY', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) +} + +function formatTime(minutes: number): string { + if (minutes < 60) return `${Math.round(minutes)}m` + const h = Math.floor(minutes / 60) + const m = Math.round(minutes % 60) + return m > 0 ? `${h}h ${m}m` : `${h}h` +} diff --git a/src/lib/export/pdf-sections.ts b/src/lib/export/pdf-sections.ts new file mode 100644 index 0000000..a41fcde --- /dev/null +++ b/src/lib/export/pdf-sections.ts @@ -0,0 +1,291 @@ +import { formatCurrency, formatMinutes, formatSimulationTimestamp } from '@/lib/format' +import type { SimulationResult, Process } from '@/domain/types' + +// Unidades: mm. A4 = 210 × 297mm, márgenes 20mm, área útil 170 × 257mm +export const PDF = { + pageW: 210, pageH: 297, + margin: 20, + contentW: 170, + blue: [30, 64, 175] as [number, number, number], + slate900: [15, 23, 42] as [number, number, number], + slate700: [51, 65, 85] as [number, number, number], + slate500: [100, 116, 139] as [number, number, number], + slate200: [226, 232, 240] as [number, number, number], + slate50: [248, 250, 252] as [number, number, number], + heatLow: [16, 185, 129] as [number, number, number], + heatMid: [245, 158, 11] as [number, number, number], + heatHigh: [239, 68, 68] as [number, number, number], +} + +// Tipo mínimo de doc para los helpers (evitar importar jsPDF en este archivo) +type DocLike = { + setFont: (name: string, style: string) => void + setFontSize: (size: number) => void + setTextColor: (...args: number[]) => void + setFillColor: (...args: number[]) => void + setDrawColor: (...args: number[]) => void + setLineWidth: (w: number) => void + text: (text: string | string[], x: number, y: number, opts?: Record) => void + rect: (x: number, y: number, w: number, h: number, style?: string) => void + roundedRect: (x: number, y: number, w: number, h: number, rx: number, ry: number, style?: string) => void + line: (x1: number, y1: number, x2: number, y2: number) => void + addImage: (img: string, format: string, x: number, y: number, w: number, h: number) => void + addPage: () => void + setPage: (page: number) => void + internal: { getNumberOfPages: () => number } + splitTextToSize: (text: string, maxWidth: number) => string[] +} + +export function drawHeader(doc: DocLike, process: Process, simulatedAt: number): number { + let y = PDF.margin + + // Nombre del proceso + doc.setFont('helvetica', 'bold') + doc.setFontSize(20) + doc.setTextColor(...PDF.slate900) + doc.text(process.name, PDF.margin, y) + y += 8 + + // Cliente + fecha + moneda + const { date, time } = formatSimulationTimestamp(simulatedAt) + const subtitle = [ + process.clientName ? `Cliente: ${process.clientName}` : null, + `Simulado el ${date} a las ${time}`, + `Moneda: ${process.currency}`, + ].filter(Boolean).join(' · ') + + doc.setFont('helvetica', 'normal') + doc.setFontSize(9) + doc.setTextColor(...PDF.slate500) + doc.text(subtitle, PDF.margin, y) + y += 5 + + // Línea separadora + doc.setDrawColor(...PDF.slate200) + doc.setLineWidth(0.3) + doc.line(PDF.margin, y, PDF.margin + PDF.contentW, y) + y += 5 + + return y +} + +export function drawKpiCards( + doc: DocLike, + result: SimulationResult, + currency: string, + startY: number +): number { + const cardW = 80, cardH = 24, gap = 10 + const col1 = PDF.margin, col2 = PDF.margin + cardW + gap + + const cards = [ + { label: 'COSTO TOTAL ESPERADO', value: formatCurrency(result.totalCost, currency), accent: true }, + { label: 'COSTO DIRECTO / OVERHEAD', value: `${formatCurrency(result.totalDirectCost, currency)} / ${formatCurrency(result.totalIndirectCost, currency)}`, accent: false }, + { label: 'TIEMPO TOTAL ESPERADO', value: formatMinutes(result.totalTimeMinutes), accent: false }, + { label: 'ACTIVIDADES COSTEADAS', value: String(result.perActivity.length), accent: false }, + ] + + let maxY = startY + + cards.forEach((card, idx) => { + const x = idx % 2 === 0 ? col1 : col2 + const y = idx < 2 ? startY : startY + cardH + 4 + + // Fondo + doc.setFillColor(...PDF.slate50) + doc.roundedRect(x, y, cardW, cardH, 2, 2, 'F') + + // Borde + doc.setDrawColor(...PDF.slate200) + doc.setLineWidth(0.2) + doc.roundedRect(x, y, cardW, cardH, 2, 2, 'S') + + // Label pequeño + doc.setFont('helvetica', 'normal') + doc.setFontSize(6.5) + doc.setTextColor(...PDF.slate500) + doc.text(card.label, x + 4, y + 6) + + // Valor grande + doc.setFont('helvetica', 'bold') + doc.setFontSize(card.value.length > 16 ? 9 : 12) + doc.setTextColor(card.accent ? PDF.blue[0] : PDF.slate900[0], card.accent ? PDF.blue[1] : PDF.slate900[1], card.accent ? PDF.blue[2] : PDF.slate900[2]) + doc.text(card.value, x + 4, y + 16) + + maxY = Math.max(maxY, y + cardH) + }) + + return maxY + 6 +} + +export function drawHeatmapImage( + doc: DocLike, + imageDataUrl: string | null, + startY: number +): number { + if (!imageDataUrl) return startY + + // Calcular altura proporcional (asumiendo aspect ratio ~2:1 del canvas BPMN) + const imgW = PDF.contentW + const imgH = Math.min(imgW / 2.2, 80) // máximo 80mm de alto + + const x = PDF.margin + if (startY + imgH > PDF.pageH - PDF.margin - 10) { + doc.addPage() + startY = PDF.margin + } + + // Borde sutil + doc.setDrawColor(...PDF.slate200) + doc.setLineWidth(0.2) + doc.rect(x, startY, imgW, imgH, 'S') + + doc.addImage(imageDataUrl, 'JPEG', x, startY, imgW, imgH) + return startY + imgH + 4 +} + +export function drawHeatmapLegend(doc: DocLike, startY: number): number { + const x = PDF.margin, barW = PDF.contentW, barH = 4 + const labelY = startY + barH + 3.5 + + doc.setFontSize(7) + doc.setFont('helvetica', 'normal') + + // Etiqueta izquierda + doc.setTextColor(...PDF.heatLow) + doc.text('Bajo costo', x, labelY) + + // Barra de gradiente: dibujada como N segmentos de colores + const steps = 40 + const segW = barW / steps + for (let i = 0; i < steps; i++) { + const t = i / (steps - 1) + let r: number, g: number, b: number + if (t <= 0.5) { + const lt = t * 2 + r = Math.round(PDF.heatLow[0] + (PDF.heatMid[0] - PDF.heatLow[0]) * lt) + g = Math.round(PDF.heatLow[1] + (PDF.heatMid[1] - PDF.heatLow[1]) * lt) + b = Math.round(PDF.heatLow[2] + (PDF.heatMid[2] - PDF.heatLow[2]) * lt) + } else { + const lt = (t - 0.5) * 2 + r = Math.round(PDF.heatMid[0] + (PDF.heatHigh[0] - PDF.heatMid[0]) * lt) + g = Math.round(PDF.heatMid[1] + (PDF.heatHigh[1] - PDF.heatMid[1]) * lt) + b = Math.round(PDF.heatMid[2] + (PDF.heatHigh[2] - PDF.heatMid[2]) * lt) + } + doc.setFillColor(r, g, b) + doc.rect(x + i * segW, startY, segW + 0.1, barH, 'F') + } + + // Etiqueta derecha + doc.setTextColor(...PDF.heatHigh) + doc.text('Alto costo', x + barW, labelY, { align: 'right' } as any) + + return labelY + 3 +} + +export function drawAnalysisSection( + doc: DocLike, + result: SimulationResult, + currency: string, + startY: number +): number { + let y = startY + + doc.setFont('helvetica', 'bold') + doc.setFontSize(12) + doc.setTextColor(...PDF.slate900) + doc.text('Análisis de composición', PDF.margin, y) + y += 6 + + doc.setDrawColor(...PDF.slate200) + doc.setLineWidth(0.2) + doc.line(PDF.margin, y, PDF.margin + PDF.contentW, y) + y += 5 + + doc.setFont('helvetica', 'normal') + doc.setFontSize(9) + doc.setTextColor(...PDF.slate700) + + // Composición directo/indirecto + const directPct = result.totalCost > 0 + ? ((result.totalDirectCost / result.totalCost) * 100).toFixed(1) + : '0' + const indirectPct = result.totalCost > 0 + ? ((result.totalIndirectCost / result.totalCost) * 100).toFixed(1) + : '0' + doc.text(`Composición del costo: ${directPct}% directo (${formatCurrency(result.totalDirectCost, currency)}) · ${indirectPct}% overhead (${formatCurrency(result.totalIndirectCost, currency)})`, PDF.margin, y) + y += 6 + + // Top 3 actividades + const top3 = result.perActivity.slice(0, 3) + const top3Text = top3.map((a, i) => `${i + 1}. ${a.activityName} (${a.percentOfTotal.toFixed(1)}% — ${formatCurrency(a.expectedTotalCost, currency)})`).join(' ') + const lines = doc.splitTextToSize(`Top 3 actividades por costo: ${top3Text}`, PDF.contentW) + doc.text(lines, PDF.margin, y) + y += lines.length * 4.5 + 2 + + // Recurso más costoso (si hay) + if (result.perResource.length > 0) { + const topResource = [...result.perResource].sort((a, b) => b.totalCost - a.totalCost)[0] + doc.text(`Recurso más costoso: "${topResource.resourceName}" con ${formatCurrency(topResource.totalCost, currency)} en total`, PDF.margin, y) + y += 5 + } else { + doc.setTextColor(...PDF.slate500) + doc.text('Sin recursos asignados (costos son fijos por actividad)', PDF.margin, y) + doc.setTextColor(...PDF.slate700) + y += 5 + } + + return y + 2 +} + +export function drawMethodologySection( + doc: DocLike, + process: Process, + startY: number +): number { + let y = startY + + doc.setFont('helvetica', 'bold') + doc.setFontSize(10) + doc.setTextColor(...PDF.slate900) + doc.text('Nota metodológica', PDF.margin, y) + y += 5 + + doc.setFont('helvetica', 'normal') + doc.setFontSize(8) + doc.setTextColor(...PDF.slate500) + + const lines = [ + `Costo total = Σ(actividades) costo_directo_esperado + costo_indirecto_esperado.`, + `Costo directo por actividad = (costo_fijo + Σ(recurso × costo_hora × tiempo × utilización / 60)) × probabilidad_ejecución.`, + `Costo indirecto = costo_directo_total × overhead_global (${(process.overheadPercentage * 100).toFixed(0)}%).`, + `Probabilidades de ejecución calculadas mediante propagación hacia adelante desde el StartEvent,`, + `considerando gateways XOR y AND configurados. Modelo determinístico agregado — MVP Fase 0.`, + ] + + for (const line of lines) { + const wrapped = doc.splitTextToSize(line, PDF.contentW) + doc.text(wrapped, PDF.margin, y) + y += wrapped.length * 4 + } + + return y +} + +export function addPageNumbers(doc: DocLike, platformName: string, generatedAt: number): void { + const total = doc.internal.getNumberOfPages() + const { date } = formatSimulationTimestamp(generatedAt) + + for (let i = 1; i <= total; i++) { + doc.setPage(i) + doc.setFont('helvetica', 'normal') + doc.setFontSize(7) + doc.setTextColor(180, 180, 180) + doc.text( + `${platformName} · ${date} · Página ${i} de ${total}`, + 105, + PDF.pageH - 8, + { align: 'center' } as any + ) + } +} diff --git a/src/lib/export/slug.ts b/src/lib/export/slug.ts new file mode 100644 index 0000000..3443803 --- /dev/null +++ b/src/lib/export/slug.ts @@ -0,0 +1,28 @@ +export function slugify(text: string): string { + return text + .normalize('NFD') + .replace(/[̀-ͯ]/g, '') // eliminar diacríticos (á→a, ñ→n, ü→u) + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') // todo lo no-alfanumérico → guión + .replace(/^-+|-+$/g, '') // recortar guiones del inicio/fin + .slice(0, 60) // limitar longitud para nombres de archivo +} + +export function buildFileName( + processName: string, + clientName: string | undefined, + date: Date, + ext: 'pdf' | 'csv' +): string { + const dateStr = [ + date.getFullYear(), + String(date.getMonth() + 1).padStart(2, '0'), + String(date.getDate()).padStart(2, '0'), + ].join('') + + const parts = [slugify(processName)] + if (clientName?.trim()) parts.push(slugify(clientName.trim())) + parts.push(dateStr) + + return `${parts.join('_')}.${ext}` +} diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..6ca25cc --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,36 @@ +export function formatCurrency(amount: number, currency = 'USD'): string { + return new Intl.NumberFormat('es-PY', { + style: 'currency', + currency, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount) +} + +export function formatNumber(n: number, decimals = 1): string { + return new Intl.NumberFormat('es-PY', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }).format(n) +} + +export function formatPercent(n: number, decimals = 1): string { + return `${formatNumber(n, decimals)}%` +} + +export function formatMinutes(minutes: number): string { + if (minutes < 60) return `${Math.round(minutes)} min` + const h = Math.floor(minutes / 60) + const m = Math.round(minutes % 60) + return m > 0 ? `${h}h ${m}min` : `${h}h` +} + +// Formato de timestamp de simulación: "13 de mayo de 2026 a las 09:45" +// Locale explícito es-PY — nunca depende del locale del browser del cliente. +export function formatSimulationTimestamp(ts: number): { date: string; time: string } { + const d = new Date(ts) + return { + date: d.toLocaleDateString('es-PY', { day: '2-digit', month: 'long', year: 'numeric' }), + time: d.toLocaleTimeString('es-PY', { hour: '2-digit', minute: '2-digit' }), + } +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..d32b0fe --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..e29c025 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import '@/styles/globals.css' +import { App } from './App' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/persistence/db.ts b/src/persistence/db.ts new file mode 100644 index 0000000..02feff4 --- /dev/null +++ b/src/persistence/db.ts @@ -0,0 +1,24 @@ +import Dexie, { type Table } from 'dexie' +import type { Process, Activity, GatewayConfig, Resource, Simulation } from '@/domain/types' + +export class ProcessCostDb extends Dexie { + processes!: Table + activities!: Table + gateways!: Table + resources!: Table + simulations!: Table + + constructor() { + super('ProcessCostPlatform') + + this.version(1).stores({ + processes: 'id, name, updatedAt', + activities: 'id, processId, bpmnElementId', + gateways: 'id, processId, bpmnElementId', + resources: 'id, processId, name', + simulations: 'id, processId, executedAt', + }) + } +} + +export const db = new ProcessCostDb() diff --git a/src/persistence/repositories.ts b/src/persistence/repositories.ts new file mode 100644 index 0000000..c9ef214 --- /dev/null +++ b/src/persistence/repositories.ts @@ -0,0 +1,114 @@ +import { db } from './db' +import type { Process, Activity, GatewayConfig, Resource, Simulation } from '@/domain/types' + +// ─── Process ───────────────────────────────────────────────────────────────── + +export const processRepo = { + async save(process: Process): Promise { + await db.processes.put(process) + }, + + async getById(id: string): Promise { + return db.processes.get(id) + }, + + async getAll(): Promise { + return db.processes.orderBy('updatedAt').reverse().toArray() + }, + + async delete(id: string): Promise { + await db.transaction('rw', [db.processes, db.activities, db.gateways, db.resources, db.simulations], async () => { + await db.processes.delete(id) + await db.activities.where('processId').equals(id).delete() + await db.gateways.where('processId').equals(id).delete() + await db.resources.where('processId').equals(id).delete() + await db.simulations.where('processId').equals(id).delete() + }) + }, +} + +// ─── Activity ───────────────────────────────────────────────────────────────── + +export const activityRepo = { + async save(activity: Activity): Promise { + await db.activities.put(activity) + }, + + async saveMany(activities: Activity[]): Promise { + await db.activities.bulkPut(activities) + }, + + async getByProcess(processId: string): Promise { + return db.activities.where('processId').equals(processId).toArray() + }, + + async getByBpmnElementId(processId: string, bpmnElementId: string): Promise { + return db.activities + .where('[processId+bpmnElementId]') + .equals([processId, bpmnElementId]) + .first() + }, + + async deleteByProcess(processId: string): Promise { + await db.activities.where('processId').equals(processId).delete() + }, +} + +// ─── GatewayConfig ──────────────────────────────────────────────────────────── + +export const gatewayRepo = { + async save(gateway: GatewayConfig): Promise { + await db.gateways.put(gateway) + }, + + async saveMany(gateways: GatewayConfig[]): Promise { + await db.gateways.bulkPut(gateways) + }, + + async getByProcess(processId: string): Promise { + return db.gateways.where('processId').equals(processId).toArray() + }, + + async deleteByProcess(processId: string): Promise { + await db.gateways.where('processId').equals(processId).delete() + }, +} + +// ─── Resource ───────────────────────────────────────────────────────────────── + +export const resourceRepo = { + async save(resource: Resource): Promise { + await db.resources.put(resource) + }, + + async getByProcess(processId: string): Promise { + return db.resources.where('processId').equals(processId).toArray() + }, + + async delete(id: string): Promise { + await db.resources.delete(id) + }, + + async deleteByProcess(processId: string): Promise { + await db.resources.where('processId').equals(processId).delete() + }, +} + +// ─── Simulation ─────────────────────────────────────────────────────────────── + +export const simulationRepo = { + async save(simulation: Simulation): Promise { + await db.simulations.put(simulation) + }, + + async getLatestByProcess(processId: string): Promise { + return db.simulations + .where('processId') + .equals(processId) + .last() + }, + + async getByProcess(processId: string): Promise { + return db.simulations.where('processId').equals(processId).toArray() + }, +} diff --git a/src/router.tsx b/src/router.tsx new file mode 100644 index 0000000..8708438 --- /dev/null +++ b/src/router.tsx @@ -0,0 +1,51 @@ +import { lazy, Suspense } from 'react' +import { createRouter, createRootRoute, createRoute, Outlet } from '@tanstack/react-router' +import { WorkspacePage } from '@/features/workspace/WorkspacePage' +import { ImportPage } from '@/features/import/ImportPage' +import { Loader2 } from 'lucide-react' + +// ECharts es grande (373KB gzip). Lazy-load del ReportPage para que el +// bundle inicial no lo incluya — se carga solo cuando el usuario llega al reporte. +const ReportPage = lazy(() => + import('@/features/report/ReportPage').then((m) => ({ default: m.ReportPage })) +) + +function ReportPageWithSuspense() { + return ( + + +

Cargando reporte…

+
+ }> + + + ) +} + +const rootRoute = createRootRoute({ component: () => }) + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: ImportPage, +}) + +const workspaceRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/workspace/$processId', + component: WorkspacePage, +}) + +const reportRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/report/$processId', + component: ReportPageWithSuspense, +}) + +const routeTree = rootRoute.addChildren([indexRoute, workspaceRoute, reportRoute]) +export const router = createRouter({ routeTree }) + +declare module '@tanstack/react-router' { + interface Register { router: typeof router } +} diff --git a/src/store/process-store.ts b/src/store/process-store.ts new file mode 100644 index 0000000..b8ed538 --- /dev/null +++ b/src/store/process-store.ts @@ -0,0 +1,99 @@ +import { create } from 'zustand' +import { processRepo, activityRepo, gatewayRepo, resourceRepo } from '@/persistence/repositories' +import type { Process, Activity, GatewayConfig, Resource } from '@/domain/types' + +interface ProcessState { + currentProcess: Process | null + activities: Activity[] + gateways: GatewayConfig[] + resources: Resource[] + isLoading: boolean + error: string | null + + // Acciones + loadProcess: (processId: string) => Promise + setCurrentProcess: (process: Process) => Promise + updateProcess: (updates: Partial) => Promise + updateActivity: (activity: Activity) => Promise + updateGateway: (gateway: GatewayConfig) => Promise + addResource: (resource: Resource) => Promise + updateResource: (resource: Resource) => Promise + deleteResource: (resourceId: string) => Promise + reset: () => void +} + +export const useProcessStore = create((set, get) => ({ + currentProcess: null, + activities: [], + gateways: [], + resources: [], + isLoading: false, + error: null, + + loadProcess: async (processId) => { + set({ isLoading: true, error: null }) + try { + const [process, activities, gateways, resources] = await Promise.all([ + processRepo.getById(processId), + activityRepo.getByProcess(processId), + gatewayRepo.getByProcess(processId), + resourceRepo.getByProcess(processId), + ]) + if (!process) throw new Error('Proceso no encontrado') + set({ currentProcess: process, activities, gateways, resources, isLoading: false }) + } catch (err) { + set({ error: err instanceof Error ? err.message : 'Error desconocido', isLoading: false }) + } + }, + + setCurrentProcess: async (process) => { + await processRepo.save(process) + const [activities, gateways, resources] = await Promise.all([ + activityRepo.getByProcess(process.id), + gatewayRepo.getByProcess(process.id), + resourceRepo.getByProcess(process.id), + ]) + set({ currentProcess: process, activities, gateways, resources }) + }, + + updateProcess: async (updates) => { + const current = get().currentProcess + if (!current) return + const updated: Process = { ...current, ...updates, updatedAt: Date.now() } + await processRepo.save(updated) + set({ currentProcess: updated }) + }, + + updateActivity: async (activity) => { + await activityRepo.save(activity) + set((state) => ({ + activities: state.activities.map((a) => (a.id === activity.id ? activity : a)), + })) + }, + + updateGateway: async (gateway) => { + await gatewayRepo.save(gateway) + set((state) => ({ + gateways: state.gateways.map((g) => (g.id === gateway.id ? gateway : g)), + })) + }, + + addResource: async (resource) => { + await resourceRepo.save(resource) + set((state) => ({ resources: [...state.resources, resource] })) + }, + + updateResource: async (resource) => { + await resourceRepo.save(resource) + set((state) => ({ + resources: state.resources.map((r) => (r.id === resource.id ? resource : r)), + })) + }, + + deleteResource: async (resourceId) => { + await resourceRepo.delete(resourceId) + set((state) => ({ resources: state.resources.filter((r) => r.id !== resourceId) })) + }, + + reset: () => set({ currentProcess: null, activities: [], gateways: [], resources: [], error: null }), +})) diff --git a/src/store/simulation-store.ts b/src/store/simulation-store.ts new file mode 100644 index 0000000..6f70b1a --- /dev/null +++ b/src/store/simulation-store.ts @@ -0,0 +1,28 @@ +import { create } from 'zustand' +import type { SimulationResult } from '@/domain/types' + +interface SimulationState { + result: SimulationResult | null + isRunning: boolean + error: string | null + selectedActivityId: string | null + + setResult: (result: SimulationResult) => void + setRunning: (running: boolean) => void + setError: (error: string | null) => void + selectActivity: (bpmnElementId: string | null) => void + reset: () => void +} + +export const useSimulationStore = create((set) => ({ + result: null, + isRunning: false, + error: null, + selectedActivityId: null, + + setResult: (result) => set({ result, error: null }), + setRunning: (isRunning) => set({ isRunning }), + setError: (error) => set({ error }), + selectActivity: (selectedActivityId) => set({ selectedActivityId }), + reset: () => set({ result: null, isRunning: false, error: null, selectedActivityId: null }), +})) diff --git a/src/styles/globals.css b/src/styles/globals.css new file mode 100644 index 0000000..125e60c --- /dev/null +++ b/src/styles/globals.css @@ -0,0 +1,92 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); +@import 'bpmn-js/dist/assets/diagram-js.css'; +@import 'bpmn-js/dist/assets/bpmn-js.css'; +@import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; + --radius: 0.5rem; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground font-sans; + font-feature-settings: "rlig" 1, "calt" 1; + } + .font-mono { + font-family: 'JetBrains Mono', monospace; + } +} + +/* ── bpmn-js canvas ─────────────────────────────────────────── */ +.bpmn-container .djs-container { + background: #f8fafc; /* slate-50 */ +} + +/* Elemento seleccionado desde nuestro código */ +.bpmn-container .bpmn-selected .djs-visual > :first-child { + stroke: hsl(var(--primary)) !important; + stroke-width: 2.5px !important; +} + +/* Elemento clickeable: cursor pointer en tasks */ +.bpmn-container .djs-element[data-element-id] { + cursor: default; +} + +/* Hover visual en tasks */ +.bpmn-container .djs-shape:hover .djs-visual > rect, +.bpmn-container .djs-shape:hover .djs-visual > path { + filter: brightness(0.97); +} + +/* heatmap overlays — para la Etapa 5 */ +.heatmap-overlay { + pointer-events: none; + transition: fill 0.3s ease; +} + +/* Asegurar que bpmn-js ocupa todo su contenedor */ +.bpmn-container .djs-container svg { + width: 100% !important; + height: 100% !important; +} + +/* Cursor pointer sobre tasks en el heatmap del reporte */ +.bpmn-container .djs-shape[data-element-id] .djs-hit { + cursor: default; +} + +/* Selección en el heatmap del reporte — stroke más prominente */ +.bpmn-container .bpmn-selected .djs-visual > rect { + stroke: hsl(var(--primary)) !important; + stroke-width: 3px !important; + stroke-dasharray: none !important; +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..dae466c --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,62 @@ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: ['class'], + content: [ + './index.html', + './src/**/*.{ts,tsx}', + ], + theme: { + extend: { + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + mono: ['JetBrains Mono', 'monospace'], + }, + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + heatmap: { + low: '#10b981', + mid: '#f59e0b', + high: '#ef4444', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + }, + }, + plugins: [], +} diff --git a/tests/domain/simulation.test.ts b/tests/domain/simulation.test.ts new file mode 100644 index 0000000..de5bc9e --- /dev/null +++ b/tests/domain/simulation.test.ts @@ -0,0 +1,210 @@ +import { describe, it, expect } from 'vitest' +import { parseBpmnXml, extractActivityElements, extractGatewayElements } from '@/domain/bpmn-parser' +import { runSimulation } from '@/domain/simulation' +import type { Activity, Resource, GatewayConfig, SimulationInput } from '@/domain/types' + +// BPMN mínimo para tests — proceso lineal de 2 tareas +const LINEAR_BPMN = ` + + + f1 + f1f2 + f2f3 + f3 + + + + +` + +// BPMN con gateway XOR +const XOR_BPMN = ` + + + f1 + f1f2 + f2f3f4 + f3f5 + f4f6 + f5f6 + + + + + + + +` + +describe('parseBpmnXml', () => { + it('parsea proceso lineal y extrae nodos y flujos', () => { + const graph = parseBpmnXml(LINEAR_BPMN) + expect(graph.nodes.size).toBe(4) // start, taskA, taskB, end + expect(graph.flows.size).toBe(3) + expect(graph.startEventIds).toEqual(['start']) + }) + + it('extrae actividades correctamente', () => { + const graph = parseBpmnXml(LINEAR_BPMN) + const activities = extractActivityElements(graph) + expect(activities).toHaveLength(2) + expect(activities[0].bpmnElementId).toBe('taskA') + expect(activities[1].bpmnElementId).toBe('taskB') + }) + + it('extrae gateways divergentes', () => { + const graph = parseBpmnXml(XOR_BPMN) + const gateways = extractGatewayElements(graph) + expect(gateways).toHaveLength(1) + expect(gateways[0].bpmnElementId).toBe('gw1') + expect(gateways[0].outgoing).toHaveLength(2) + }) + + it('lanza error si el XML no tiene proceso', () => { + expect(() => parseBpmnXml('')).toThrow() + }) +}) + +describe('runSimulation', () => { + const makeResource = (id: string, name: string, costPerHour: number): Resource => ({ + id, + processId: 'proc1', + name, + type: 'role', + costPerHour, + }) + + const makeActivity = ( + bpmnElementId: string, + name: string, + directCost: number, + minutes: number, + resources: Array<{ resourceId: string; utilizationPercent: number }> = [] + ): Activity => ({ + id: `act-${bpmnElementId}`, + processId: 'proc1', + bpmnElementId, + name, + type: 'task', + directCostFixed: directCost, + executionTimeMinutes: minutes, + assignedResources: resources, + }) + + it('calcula costo total sin recursos — proceso lineal', () => { + const activityA = makeActivity('taskA', 'Tarea A', 100, 30) + const activityB = makeActivity('taskB', 'Tarea B', 200, 60) + + const input: SimulationInput = { + processXml: LINEAR_BPMN, + activities: new Map([ + ['taskA', activityA], + ['taskB', activityB], + ]), + gateways: new Map(), + resources: new Map(), + globalSettings: { currency: 'USD', overheadPercentage: 0 }, + } + + const result = runSimulation(input) + expect(result.totalDirectCost).toBe(300) + expect(result.totalIndirectCost).toBe(0) + expect(result.totalCost).toBe(300) + expect(result.warnings).toHaveLength(0) + }) + + it('aplica overhead correctamente', () => { + const activityA = makeActivity('taskA', 'Tarea A', 100, 30) + + const input: SimulationInput = { + processXml: LINEAR_BPMN, + activities: new Map([['taskA', activityA]]), + gateways: new Map(), + resources: new Map(), + globalSettings: { currency: 'USD', overheadPercentage: 0.2 }, + } + + const result = runSimulation(input) + expect(result.totalDirectCost).toBe(100) + expect(result.totalIndirectCost).toBeCloseTo(20) + expect(result.totalCost).toBeCloseTo(120) + }) + + it('incluye costo de recursos asignados', () => { + const resource = makeResource('res1', 'Analista', 60) // $60/hora + const activityA = makeActivity('taskA', 'Tarea A', 0, 60, [ + { resourceId: 'res1', utilizationPercent: 1.0 }, // 1 hora completa + ]) + + const input: SimulationInput = { + processXml: LINEAR_BPMN, + activities: new Map([['taskA', activityA]]), + gateways: new Map(), + resources: new Map([['res1', resource]]), + globalSettings: { currency: 'USD', overheadPercentage: 0 }, + } + + const result = runSimulation(input) + expect(result.totalDirectCost).toBeCloseTo(60) + }) + + it('propaga probabilidad 0.7/0.3 en gateway XOR', () => { + const actA = makeActivity('taskA', 'Evaluar', 100, 30) + const actB = makeActivity('taskB', 'Camino Si', 200, 60) + const actC = makeActivity('taskC', 'Camino No', 100, 30) + + const gw: GatewayConfig = { + id: 'gw-1', + processId: 'proc2', + bpmnElementId: 'gw1', + gatewayType: 'exclusive', + branches: [ + { flowId: 'f3', targetElementId: 'taskB', probability: 0.7 }, + { flowId: 'f4', targetElementId: 'taskC', probability: 0.3 }, + ], + } + + const input: SimulationInput = { + processXml: XOR_BPMN, + activities: new Map([ + ['taskA', actA], + ['taskB', actB], + ['taskC', actC], + ]), + gateways: new Map([['gw1', gw]]), + resources: new Map(), + globalSettings: { currency: 'USD', overheadPercentage: 0 }, + } + + const result = runSimulation(input) + const bResult = result.perActivity.find((a) => a.bpmnElementId === 'taskB') + const cResult = result.perActivity.find((a) => a.bpmnElementId === 'taskC') + + expect(bResult?.executionProbability).toBeCloseTo(0.7, 1) + expect(cResult?.executionProbability).toBeCloseTo(0.3, 1) + // Costo esperado de B = 200 * 0.7 = 140 + expect(bResult?.expectedDirectCost).toBeCloseTo(140) + // Costo esperado de C = 100 * 0.3 = 30 + expect(cResult?.expectedDirectCost).toBeCloseTo(30) + }) + + it('ordena perActivity por costo total descendente', () => { + const actA = makeActivity('taskA', 'Tarea A', 50, 10) + const actB = makeActivity('taskB', 'Tarea B', 500, 60) + + const input: SimulationInput = { + processXml: LINEAR_BPMN, + activities: new Map([ + ['taskA', actA], + ['taskB', actB], + ]), + gateways: new Map(), + resources: new Map(), + globalSettings: { currency: 'USD', overheadPercentage: 0 }, + } + + const result = runSimulation(input) + expect(result.perActivity[0].bpmnElementId).toBe('taskB') + expect(result.perActivity[1].bpmnElementId).toBe('taskA') + }) +}) diff --git a/tests/e2e/export-pdf.spec.ts b/tests/e2e/export-pdf.spec.ts new file mode 100644 index 0000000..cdac093 --- /dev/null +++ b/tests/e2e/export-pdf.spec.ts @@ -0,0 +1,418 @@ +/** + * E2E: Verifica que el export PDF y CSV funcionen de extremo a extremo. + * + * Por cada BPMN: importa → simula → descarga PDF y CSV → verifica tamaño y contenido. + * El test de acentos verifica que los caracteres UTF-8 se preserven en el PDF. + * + * Ejecutar: npx playwright test + */ +import { test, expect } from '@playwright/test' +import { readFileSync, mkdirSync } from 'fs' +import { resolve } from 'path' +import { fileURLToPath } from 'url' +import { PDFParse } from 'pdf-parse' +import sharp from 'sharp' +import { PDFDocument, PDFName, PDFRawStream } from 'pdf-lib' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const BPMN_DIR = resolve(__dirname, '../../public/sample-processes') +const OUTPUT_DIR = resolve(__dirname, '__output__') + +test.beforeAll(() => { + mkdirSync(OUTPUT_DIR, { recursive: true }) +}) + +// ─── Helper: parsea texto de un buffer PDF con pdf-parse v2 ────────────────── + +async function parsePdf(buffer: Buffer): Promise<{ numpages: number; text: string }> { + const parser = new PDFParse({ data: buffer }) + const result = await parser.getText() + await parser.destroy() + return { numpages: result.total, text: result.text } +} + +// ─── Helper: importa un BPMN, simula y llega al reporte ────────────────────── + +async function importAndSimulate(page: import('@playwright/test').Page, bpmnFile: string) { + await page.goto('/') + + // Subir el BPMN vía el input oculto (equivalente a drag-and-drop programático) + const fileInput = page.locator('#bpmn-file-input') + await fileInput.setInputFiles(resolve(BPMN_DIR, bpmnFile)) + + // Esperar navegación al workspace + await page.waitForURL(/\/workspace\//, { timeout: 15_000 }) + + // Esperar a que desaparezca el spinner de carga (workspace renderiza el botón Simular) + await page.waitForSelector('button:has-text("Simular")', { timeout: 15_000 }) + + // Tiempo adicional para que Dexie cargue gateways y actividades en la store + await page.waitForTimeout(1_500) + + // Verificar que el botón no está deshabilitado y hacer clic + const simularBtn = page.getByRole('button', { name: 'Simular' }) + await expect(simularBtn).not.toBeDisabled({ timeout: 5_000 }) + await simularBtn.click() + + // Esperar navegación al reporte + await page.waitForURL(/\/report\//, { timeout: 20_000 }) + + // Esperar a que el reporte cargue y el botón de export sea visible + await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 30_000 }) + // Esperar a que bpmn-js en HeatmapCanvas haya renderizado el diagrama. + // .djs-group es creado por bpmn-js después de importXML() — confirma render real. + await page.waitForSelector('.bpmn-container .djs-group', { timeout: 30_000 }) +} + +// ─── Casos por BPMN ────────────────────────────────────────────────────────── + +const CASES = [ + { + file: 'simple-linear.bpmn', + slug: 'simple-linear', + processNameFragment: 'simple-linear', + currency: 'USD', + }, + { + file: 'medium-with-gateways.bpmn', + slug: 'medium-with-gateways', + processNameFragment: 'medium-with-gateways', + currency: 'USD', + }, + { + file: 'complex-with-loop.bpmn', + slug: 'complex-with-loop', + processNameFragment: 'complex-with-loop', + currency: 'USD', + }, +] + +for (const c of CASES) { + test(`Export PDF + CSV — ${c.file}`, async ({ page }) => { + await importAndSimulate(page, c.file) + + // ── PDF ───────────────────────────────────────────────────────────────── + const pdfPath = resolve(OUTPUT_DIR, `${c.slug}.pdf`) + + const pdfDownload = page.waitForEvent('download', { timeout: 90_000 }) + await page.getByRole('button', { name: 'Exportar PDF' }).click() + const dl = await pdfDownload + await dl.saveAs(pdfPath) + + // Esperar a que el estado "Generando…" desaparezca + await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 30_000 }) + + // Verificar tamaño + const pdfBuffer = readFileSync(pdfPath) + const pdfBytes = pdfBuffer.length + console.log(`\n📄 ${c.file} — PDF: ${pdfBytes} bytes (${(pdfBytes / 1024).toFixed(1)} KB)`) + + // PDF mínimo: debe tener al menos algo de contenido + expect(pdfBytes, `PDF de ${c.file} debe ser > 40 KB`).toBeGreaterThan(40_000) + expect(pdfBytes, `PDF de ${c.file} debe ser < 2 MB`).toBeLessThan(2_000_000) + + // El heatmap JPEG debe estar embebido en el PDF (marker SOI: FF D8 FF). + // Sin actividades con costos reales el JPEG es pequeño (~32 KB, color neutro #cbd5e1) + // pero SIEMPRE debe estar presente cuando el canvas se renderizó correctamente. + const jpegMarker = Buffer.from([0xff, 0xd8, 0xff]) + const hasJpeg = pdfBuffer.includes(jpegMarker) + console.log(` Heatmap JPEG embebido: ${hasJpeg}`) + expect(hasJpeg, 'PDF generado sin heatmap embebido — la race condition de captureImage no fue resuelta').toBe(true) + + // Verificar contenido + const { numpages, text } = await parsePdf(pdfBuffer) + console.log(` Páginas: ${numpages}`) + console.log(` Texto (primeros 200 chars): ${text.slice(0, 200).replace(/\n/g, ' ')}`) + + expect(numpages, 'PDF debe tener al menos 2 páginas').toBeGreaterThanOrEqual(2) + // El KPI de costo aparece en mayúsculas en el PDF: "COSTO TOTAL ESPERADO" + expect(text, 'PDF debe contener "COSTO TOTAL"').toContain('COSTO TOTAL') + expect(text, `PDF debe contener la moneda "${c.currency}"`).toContain(c.currency) + expect(text, 'PDF debe contener el nombre del proceso').toContain(c.processNameFragment) + + // ── CSV ───────────────────────────────────────────────────────────────── + const csvPath = resolve(OUTPUT_DIR, `${c.slug}.csv`) + + const csvDownload = page.waitForEvent('download') + await page.getByRole('button', { name: 'Exportar CSV' }).click() + const dlCsv = await csvDownload + await dlCsv.saveAs(csvPath) + + const csvBuffer = readFileSync(csvPath) + const csvBytes = csvBuffer.length + console.log(` CSV: ${csvBytes} bytes`) + + expect(csvBytes, `CSV de ${c.file} debe ser > 100 bytes`).toBeGreaterThan(100) + + const csvText = csvBuffer.toString('utf-8') + expect(csvText, 'CSV debe tener BOM UTF-8').toMatch(/^/) + expect(csvText, 'CSV debe contener "Costo total"').toContain('Costo total') + expect(csvText, 'CSV debe contener la moneda').toContain(c.currency) + }) +} + +// ─── Test especial: acentos y ñ ─────────────────────────────────────────────── + +test('Export PDF — acentos y ñ se preservan en el PDF (accent-test.bpmn)', async ({ page }) => { + await importAndSimulate(page, 'accent-test.bpmn') + + const pdfPath = resolve(OUTPUT_DIR, 'accent-test.pdf') + + const pdfDownload = page.waitForEvent('download', { timeout: 90_000 }) + await page.getByRole('button', { name: 'Exportar PDF' }).click() + const dl = await pdfDownload + await dl.saveAs(pdfPath) + + const pdfBuffer = readFileSync(pdfPath) + const pdfBytes = pdfBuffer.length + console.log(`\n🔤 accent-test.bpmn — PDF: ${pdfBytes} bytes (${(pdfBytes / 1024).toFixed(1)} KB)`) + + expect(pdfBytes, 'PDF debe ser > 40 KB').toBeGreaterThan(40_000) + + const jpegMarker = Buffer.from([0xff, 0xd8, 0xff]) + const hasJpeg = pdfBuffer.includes(jpegMarker) + console.log(` Heatmap JPEG embebido: ${hasJpeg}`) + expect(hasJpeg, 'PDF generado sin heatmap embebido — la race condition de captureImage no fue resuelta').toBe(true) + + const { numpages, text } = await parsePdf(pdfBuffer) + console.log(` Páginas: ${numpages}`) + console.log(` Texto completo:\n${text}`) + + expect(numpages).toBeGreaterThanOrEqual(2) + + // Verificar que los caracteres acentuados se preservan + expect(text, 'PDF debe contener "Análisis de procesos contables"') + .toContain('Análisis de procesos contables') + + expect(text, 'PDF debe contener "Validación de información"') + .toContain('Validación de información') + + expect(text, 'PDF debe contener "Inspección visual y funcional"') + .toContain('Inspección visual y funcional') + + expect(text, 'PDF debe contener "Diseño de campañas"') + .toContain('Diseño de campañas') + + // CSV del accent-test + await page.waitForSelector('button:has-text("Exportar CSV")', { timeout: 10_000 }) + const csvPath = resolve(OUTPUT_DIR, 'accent-test.csv') + const csvDownload = page.waitForEvent('download') + await page.getByRole('button', { name: 'Exportar CSV' }).click() + const dlCsv = await csvDownload + await dlCsv.saveAs(csvPath) + + const csvText = readFileSync(csvPath, 'utf-8') + console.log(` CSV (primeros 400 chars): ${csvText.slice(0, 400)}`) + + expect(csvText).toContain('Análisis de procesos contables') + expect(csvText).toContain('Validación de información') + expect(csvText).toContain('Inspección visual y funcional') + expect(csvText).toContain('Diseño de campañas') +}) + +// ─── Helpers para extracción y análisis de JPEG ─────────────────────────────── + +/** + * Extrae el primer stream JPEG (DCTDecode) del PDF. + * Intenta con pdf-lib; si falla, usa búsqueda binaria de marcador SOI/EOI. + */ +async function extractJpegFromPdf(pdfBuf: Buffer): Promise { + // Intento 1: pdf-lib (semántico, correcto) + try { + const doc = await PDFDocument.load(pdfBuf, { ignoreEncryption: true }) + for (const [, obj] of doc.context.enumerateIndirectObjects()) { + if (obj instanceof PDFRawStream) { + const subtype = obj.dict.lookupMaybe(PDFName.of('Subtype'), PDFName) + const filter = obj.dict.lookupMaybe(PDFName.of('Filter'), PDFName) + if (subtype?.toString() === '/Image' && filter?.toString() === '/DCTDecode') { + return Buffer.from(obj.contents) + } + } + } + } catch { + // pdf-lib no pudo parsear — caer a método binario + } + + // Intento 2: búsqueda binaria de marcador JPEG SOI (FF D8 FF) + const soi = Buffer.from([0xff, 0xd8, 0xff]) + const start = pdfBuf.indexOf(soi) + if (start === -1) throw new Error('No se encontró ningún JPEG en el PDF') + + const endstreamBuf = Buffer.from('endstream') + const endstreamPos = pdfBuf.indexOf(endstreamBuf, start) + if (endstreamPos === -1) throw new Error('No se encontró "endstream" después del JPEG') + + // Buscar EOI (FF D9) retrocediendo desde 'endstream' + let eoi = endstreamPos - 1 + while (eoi > start + 2) { + if (pdfBuf[eoi] === 0xd9 && pdfBuf[eoi - 1] === 0xff) { eoi++; break } + eoi-- + } + + const jpeg = pdfBuf.subarray(start, eoi) + if (jpeg.length < 100) throw new Error(`JPEG extraído muy pequeño: ${jpeg.length} bytes`) + return jpeg +} + +/** + * Cuenta píxeles por categoría de color en un buffer JPEG. + * Umbrales calibrados para los colores del heatmap con opacity 0.82 sobre #f8fafc. + * + * Verde (#10b981 + bg) → RGB ≈ (58, 197, 152) → G > R·1.4, G > 120 + * Ámbar (#f59e0b + bg) → RGB ≈ (246, 175, 54) → R > 180, G > 120, B < 120, R/G < 1.8 + * Rojo (#ef4444 + bg) → RGB ≈ (241, 101, 101) → R > G·1.8, R > B·1.8, R > 180 + */ +async function analyzeJpegColors(jpegBuf: Buffer): Promise<{ + greenPx: number; amberPx: number; redPx: number; total: number +}> { + const { data, info } = await sharp(jpegBuf).raw().toBuffer({ resolveWithObject: true }) + const ch = info.channels // 3 para JPEG (RGB) + let greenPx = 0, amberPx = 0, redPx = 0 + + for (let i = 0; i < data.length; i += ch) { + const r = data[i], g = data[i + 1], b = data[i + 2] + + if (g > r * 1.4 && g > 120 && g > b) { + greenPx++ // Verde + } else if (r > 180 && g > 120 && b < 120 && r > g && r < g * 1.8) { + amberPx++ // Ámbar / amarillo cálido + } else if (r > g * 1.8 && r > b * 1.8 && r > 180) { + redPx++ // Rojo + } + } + + return { greenPx, amberPx, redPx, total: data.length / ch } +} + +// ─── Test: gradiente real con costos dispares ───────────────────────────────── + +test('Heatmap con gradiente real — medium-with-gateways.bpmn', async ({ page }) => { + // Costos dispares por bpmnElementId para forzar varianza significativa. + // Probabilidades de ejecución del proceso: task_recv/valid/analisis/archivo = 1.0, + // los demás ≈ 0.5. Se asignan costos para que: + // task_recv → expectedCost $10.000 → costo más alto → ROJO + // task_analisis → expectedCost $5.000 → mitad del max → ÁMBAR (t ≈ 0.5) + // resto → expectedCost < $500 → costos bajos → VERDE + const COST_BY_ELEMENT: Record = { + task_recv: 10_000, // prob 1.0 → expected $10.000 → ROJO + task_valid: 200, // prob 1.0 → expected $200 + task_score: 200, // prob 0.5 → expected $100 + task_pedir_docs: 200, // prob 0.5 → expected $100 + task_bureau: 200, // prob 0.5 → expected $100 + task_empleador: 200, // prob 0.5 → expected $100 + task_analisis: 5_000, // prob 1.0 → expected $5.000 → ÁMBAR + task_aprobar: 200, // prob 0.5 → expected $100 + task_rechazar: 200, // prob 0.5 → expected $100 + task_desembolso: 200, // prob 0.5 → expected $100 + task_archivo: 200, // prob 1.0 → expected $200 → VERDE (mínimo) + } + + // ── 1. Importar BPMN y navegar al workspace ───────────────────────────────── + await page.goto('/') + await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, 'medium-with-gateways.bpmn')) + await page.waitForURL(/\/workspace\//) + + const processId = page.url().split('/workspace/')[1] + + // ── 2. Inyectar costos dispares directamente en IndexedDB (Dexie) ─────────── + await page.evaluate(async ({ procId, costs }) => { + const DB_NAME = 'ProcessCostPlatform' + + const db = await new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME) + req.onsuccess = () => resolve(req.result) + req.onerror = () => reject(req.error) + }) + + const activities = await new Promise((resolve, reject) => { + const tx = db.transaction('activities', 'readonly') + const index = tx.objectStore('activities').index('processId') + const req = index.getAll(procId) + req.onsuccess = () => resolve(req.result) + req.onerror = () => reject(req.error) + }) + + await new Promise((resolve, reject) => { + const tx = db.transaction('activities', 'readwrite') + const store = tx.objectStore('activities') + let pending = activities.length + if (pending === 0) { resolve(); return } + + for (const act of activities) { + const req = store.put({ + ...act, + directCostFixed: (costs as Record)[act.bpmnElementId] ?? 200, + executionTimeMinutes: 60, + }) + req.onsuccess = () => { if (--pending === 0) resolve() } + req.onerror = () => reject(req.error) + } + }) + + db.close() + }, { procId: processId, costs: COST_BY_ELEMENT }) + + // ── 3. Recargar workspace para que Zustand relea desde Dexie ──────────────── + await page.reload() + await page.waitForSelector('button:has-text("Simular")', { timeout: 15_000 }) + await page.waitForTimeout(1_500) // esperar carga de store + + // ── 4. Simular y navegar al reporte ──────────────────────────────────────── + const simularBtn = page.getByRole('button', { name: 'Simular' }) + await expect(simularBtn).not.toBeDisabled({ timeout: 5_000 }) + await simularBtn.click() + await page.waitForURL(/\/report\//, { timeout: 20_000 }) + await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 20_000 }) + + // ── 5. Esperar a que el heatmap tenga colores reales (no neutro #cbd5e1) ──── + // Usamos style.fill (inline CSS) porque applyHeatmapToViewer ahora lo usa + // en vez de setAttribute (para garantizar mayor especificidad que bpmn-js CSS). + await page.waitForFunction(() => { + const rects = Array.from(document.querySelectorAll('.bpmn-container .djs-visual rect')) + return rects.some((r) => { + const fill = (r as HTMLElement).style.fill + return fill && fill !== '#cbd5e1' + }) + }, { timeout: 30_000 }) + + // Capturar estado de los fills para diagnóstico + const fillsSnapshot = await page.evaluate(() => + Array.from(document.querySelectorAll('.bpmn-container .djs-visual rect')) + .map((r) => (r as HTMLElement).style.fill || r.getAttribute('fill')) + ) + console.log('\n🎨 Fills del heatmap:', fillsSnapshot) + + // Screenshot para evidencia visual + await page.screenshot({ path: resolve(OUTPUT_DIR, 'medium-with-gradient-report.png'), fullPage: false }) + + // ── 6. Exportar PDF ───────────────────────────────────────────────────────── + const pdfPath = resolve(OUTPUT_DIR, 'medium-with-gradient.pdf') + const pdfDownload = page.waitForEvent('download', { timeout: 90_000 }) + await page.getByRole('button', { name: 'Exportar PDF' }).click() + const dl = await pdfDownload + await dl.saveAs(pdfPath) + + // ── 7. Verificar tamaño ───────────────────────────────────────────────────── + const pdfBuf = readFileSync(pdfPath) + const pdfKB = (pdfBuf.length / 1024).toFixed(1) + console.log(`\n📄 medium-with-gradient.pdf: ${pdfBuf.length} bytes (${pdfKB} KB)`) + + // JPEG de colores sólidos comprime casi igual que el gris neutro. + // El umbral mínimo es > 50 KB (PDF sin JPEG pesa ~28 KB; con JPEG ~60-80 KB). + expect(pdfBuf.length, 'PDF con gradiente real debe ser > 50 KB').toBeGreaterThan(50_000) + + // ── 8. Extraer JPEG y analizar píxeles ────────────────────────────────────── + const jpegBuf = await extractJpegFromPdf(pdfBuf) + console.log(` JPEG extraído: ${jpegBuf.length} bytes`) + + const { greenPx, amberPx, redPx, total } = await analyzeJpegColors(jpegBuf) + console.log(` Total píxeles: ${total.toLocaleString()}`) + console.log(` 🟢 Verde: ${greenPx.toLocaleString()} px`) + console.log(` 🟡 Ámbar: ${amberPx.toLocaleString()} px`) + console.log(` 🔴 Rojo: ${redPx.toLocaleString()} px`) + + const MIN_PX = 100 + expect(greenPx, `Píxeles verdes en heatmap (≥ ${MIN_PX}): ${greenPx}`).toBeGreaterThanOrEqual(MIN_PX) + expect(amberPx, `Píxeles ámbar en heatmap (≥ ${MIN_PX}): ${amberPx}`).toBeGreaterThanOrEqual(MIN_PX) + expect(redPx, `Píxeles rojos en heatmap (≥ ${MIN_PX}): ${redPx}`).toBeGreaterThanOrEqual(MIN_PX) +}) diff --git a/tests/e2e/prod-smoke.spec.ts b/tests/e2e/prod-smoke.spec.ts new file mode 100644 index 0000000..a02fb37 --- /dev/null +++ b/tests/e2e/prod-smoke.spec.ts @@ -0,0 +1,91 @@ +/** + * Smoke test de producción — verifica el flujo completo en la URL live. + * Usa el proceso de ejemplo "Aprobación de crédito" (carga via fetch desde el CDN). + * + * Ejecutar: npx playwright test --config=playwright.prod.config.ts + */ +import { test, expect } from '@playwright/test' +import { readFileSync, mkdirSync } from 'fs' +import { resolve } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const OUTPUT_DIR = resolve(__dirname, '__output__') + +test.beforeAll(() => { + mkdirSync(OUTPUT_DIR, { recursive: true }) +}) + +test('prod-smoke — import → simulate → export PDF', async ({ page }) => { + // ── 1. Landing page carga correctamente ────────────────────────────────── + await page.goto('/') + await expect(page).toHaveTitle(/Process Cost Platform/) + + // Verificar heading principal + await expect(page.getByText('Cuantificá el costo operativo')).toBeVisible({ timeout: 10_000 }) + + // Verificar que los 3 procesos de ejemplo están presentes + await expect(page.getByText('Aprobación de crédito')).toBeVisible() + await expect(page.getByText('Proceso lineal simple')).toBeVisible() + await expect(page.getByText('Control de calidad con revisión')).toBeVisible() + + // ── 2. Cargar proceso de ejemplo "Proceso lineal simple" ────────────────── + await page.getByText('Proceso lineal simple').click() + + // Esperar navegación al workspace + await page.waitForURL(/\/workspace\//, { timeout: 30_000 }) + + // ── 3. Workspace: diagrama BPMN + botón Simular ─────────────────────────── + await page.waitForSelector('button:has-text("Simular")', { timeout: 20_000 }) + await page.waitForTimeout(1_500) + + // Nombre del proceso visible en la topbar + await expect(page.getByText('simple-linear')).toBeVisible() + + const simularBtn = page.getByRole('button', { name: 'Simular' }) + await expect(simularBtn).not.toBeDisabled({ timeout: 5_000 }) + await simularBtn.click() + + // ── 4. Reporte carga ────────────────────────────────────────────────────── + await page.waitForURL(/\/report\//, { timeout: 30_000 }) + await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 30_000 }) + await page.waitForSelector('.bpmn-container .djs-group', { timeout: 30_000 }) + + // KPIs visibles + await expect(page.getByText('COSTO TOTAL ESPERADO')).toBeVisible() + + // ── 5. Export PDF ───────────────────────────────────────────────────────── + const pdfPath = resolve(OUTPUT_DIR, 'prod-smoke.pdf') + + const pdfDownload = page.waitForEvent('download', { timeout: 90_000 }) + await page.getByRole('button', { name: 'Exportar PDF' }).click() + const dl = await pdfDownload + await dl.saveAs(pdfPath) + + const pdfBuffer = readFileSync(pdfPath) + const pdfKB = (pdfBuffer.length / 1024).toFixed(1) + console.log(`\n📄 prod-smoke.pdf: ${pdfBuffer.length} bytes (${pdfKB} KB)`) + + expect(pdfBuffer.length, 'PDF debe ser > 40 KB').toBeGreaterThan(40_000) + expect(pdfBuffer.length, 'PDF debe ser < 2 MB').toBeLessThan(2_000_000) + + // JPEG heatmap embebido + const jpegMarker = Buffer.from([0xff, 0xd8, 0xff]) + const hasJpeg = pdfBuffer.includes(jpegMarker) + console.log(` Heatmap JPEG embebido: ${hasJpeg}`) + expect(hasJpeg, 'PDF de prod debe tener heatmap embebido').toBe(true) + + // ── 6. Export CSV ───────────────────────────────────────────────────────── + const csvPath = resolve(OUTPUT_DIR, 'prod-smoke.csv') + const csvDownload = page.waitForEvent('download') + await page.getByRole('button', { name: 'Exportar CSV' }).click() + const dlCsv = await csvDownload + await dlCsv.saveAs(csvPath) + + const csvText = readFileSync(csvPath, 'utf-8') + expect(csvText, 'CSV debe tener BOM UTF-8').toMatch(/^/) + expect(csvText, 'CSV debe contener encabezado').toContain('Costo total') + + console.log(`\n✅ Prod smoke PASSED — https://process-cost-platform.pages.dev`) + console.log(` PDF: ${pdfKB} KB | CSV: ${csvText.length} chars`) +}) diff --git a/tests/features/report/__snapshots__/derivations.test.ts.snap b/tests/features/report/__snapshots__/derivations.test.ts.snap new file mode 100644 index 0000000..728e896 --- /dev/null +++ b/tests/features/report/__snapshots__/derivations.test.ts.snap @@ -0,0 +1,85 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`buildCostCompositionOption — estructura del objeto ECharts > snapshot: estructura completa del option del donut 1`] = ` +{ + "graphic": [ + { + "left": "center", + "style": { + "fill": "#1e293b", + "fontFamily": "JetBrains Mono, monospace", + "fontSize": 13, + "fontWeight": "bold", + "text": "USD 1.200,00", + "textAlign": "center", + }, + "top": "38%", + "type": "text", + }, + ], + "legend": { + "bottom": "2%", + "itemHeight": 12, + "itemWidth": 12, + "left": "center", + "textStyle": { + "color": "#475569", + "fontSize": 11, + }, + }, + "series": [ + { + "avoidLabelOverlap": true, + "center": [ + "50%", + "44%", + ], + "data": [ + { + "itemStyle": { + "color": "#3b82f6", + }, + "name": "Costo directo", + "value": 1000, + }, + { + "itemStyle": { + "color": "#94a3b8", + }, + "name": "Costo indirecto (overhead)", + "value": 200, + }, + ], + "emphasis": { + "label": { + "fontSize": 13, + "fontWeight": "bold", + }, + }, + "itemStyle": { + "borderColor": "#fff", + "borderRadius": 4, + "borderWidth": 2, + }, + "label": { + "fontSize": 11, + "formatter": [Function], + "show": true, + }, + "labelLine": { + "length": 10, + "length2": 8, + }, + "radius": [ + "48%", + "68%", + ], + "type": "pie", + }, + ], + "tooltip": { + "formatter": [Function], + "trigger": "item", + }, +} +`; diff --git a/tests/features/report/components.test.tsx b/tests/features/report/components.test.tsx new file mode 100644 index 0000000..aa2da73 --- /dev/null +++ b/tests/features/report/components.test.tsx @@ -0,0 +1,409 @@ +/** + * Tests de renderizado DOM de los componentes del reporte. + * bpmn-js y echarts-for-react se mockean (requieren browser/canvas reales). + * useReportData se mockea para inyectar datos controlados. + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import React from 'react' + +// ─── Mocks de dependencias del browser ─────────────────────────────────────── + +vi.mock('bpmn-js/lib/Viewer', () => ({ + default: class MockViewer { + importXML = vi.fn().mockResolvedValue({ warnings: [] }) + get = vi.fn().mockImplementation((svc: string) => { + if (svc === 'canvas') return { zoom: vi.fn(), addMarker: vi.fn(), removeMarker: vi.fn() } + if (svc === 'elementRegistry') return { forEach: vi.fn(), getGraphics: vi.fn().mockReturnValue(null) } + if (svc === 'eventBus') return { on: vi.fn(), off: vi.fn() } + return {} + }) + destroy = vi.fn() + }, +})) + +vi.mock('echarts-for-react', () => ({ + default: ({ style }: { option: unknown; style?: React.CSSProperties }) => ( +
+ ), +})) + +// ─── Fixtures ───────────────────────────────────────────────────────────────── + +import type { ActivitySimResult, SimulationResult } from '@/domain/types' + +const mockActivities: ActivitySimResult[] = [ + { + activityId: 'a1', bpmnElementId: 'task1', activityName: 'Recibir solicitud', + expectedDirectCost: 500, expectedIndirectCost: 100, expectedTotalCost: 600, + percentOfTotal: 60, executionProbability: 1.0, expectedExecutions: 1.0, + executionTimeMinutes: 30, resourceCostBreakdown: [], + }, + { + activityId: 'a2', bpmnElementId: 'task2', activityName: 'Procesar pedido', + expectedDirectCost: 250, expectedIndirectCost: 50, expectedTotalCost: 300, + percentOfTotal: 30, executionProbability: 1.0, expectedExecutions: 1.0, + executionTimeMinutes: 60, resourceCostBreakdown: [], + }, + { + activityId: 'a3', bpmnElementId: 'task3', activityName: 'Notificar cliente', + expectedDirectCost: 83, expectedIndirectCost: 17, expectedTotalCost: 100, + percentOfTotal: 10, executionProbability: 1.0, expectedExecutions: 1.0, + executionTimeMinutes: 15, resourceCostBreakdown: [], + }, +] + +const mockResult: SimulationResult = { + totalCost: 1000, + totalDirectCost: 833, + totalIndirectCost: 167, + totalTimeMinutes: 105, + perActivity: mockActivities, + perResource: [], + warnings: [], +} + +// ─── KpiBar ─────────────────────────────────────────────────────────────────── + +import { KpiBar } from '@/features/report/KpiBar' + +describe('KpiBar', () => { + it('renderiza exactamente 4 KPI cards', () => { + render() + const cards = screen.getAllByTestId('kpi-card') + expect(cards).toHaveLength(4) + }) + + it('la primera card muestra el costo total formateado', () => { + render() + // El valor $1,000.00 debe aparecer en algún lugar del DOM + const cards = screen.getAllByTestId('kpi-card') + const allText = cards.map((c) => c.textContent).join(' ') + expect(allText).toMatch(/1[.,]000/) + }) + + it('con currency PYG: muestra el valor sin crash', () => { + expect(() => render()).not.toThrow() + const cards = screen.getAllByTestId('kpi-card') + expect(cards).toHaveLength(4) + }) + + it('tiempo total aparece formateado en minutos/horas', () => { + render() + // 105 min = 1h 45min + const text = document.body.textContent ?? '' + expect(text).toMatch(/1h|105/) + }) + + it('cantidad de actividades es visible', () => { + render() + const text = document.body.textContent ?? '' + expect(text).toMatch(/3/) + }) +}) + +// ─── WarningsBanner ─────────────────────────────────────────────────────────── + +import { WarningsBanner } from '@/features/report/WarningsBanner' + +describe('WarningsBanner', () => { + it('no renderiza nada cuando no hay warnings', () => { + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it('renderiza el banner cuando hay warnings', () => { + render() + expect(screen.getByText(/Loop detectado/)).toBeInTheDocument() + }) + + it('con múltiples warnings: muestra todos', () => { + render() + expect(screen.getByText(/Warning 1/)).toBeInTheDocument() + expect(screen.getByText(/Warning 2/)).toBeInTheDocument() + expect(screen.getByText(/Warning 3/)).toBeInTheDocument() + }) + + it('warning de loop: muestra nota de impacto en resultados', () => { + render() + expect(screen.getByText(/Impacto en los resultados/i)).toBeInTheDocument() + }) + + it('warning sin loop: NO muestra nota de impacto', () => { + render() + expect(screen.queryByText(/Impacto en los resultados/i)).not.toBeInTheDocument() + }) +}) + +// ─── ActivitiesTable ───────────────────────────────────────────────────────── + +import { ActivitiesTable } from '@/features/report/ActivitiesTable' + +describe('ActivitiesTable', () => { + it('renderiza exactamente N filas donde N = perActivity.length', () => { + render( + + ) + // getAllByRole('row') incluye el header row, entonces total = N + 1 + const rows = screen.getAllByRole('row') + expect(rows).toHaveLength(mockActivities.length + 1) + }) + + it('los nombres de las actividades aparecen en la tabla', () => { + render( + + ) + expect(screen.getByText('Recibir solicitud')).toBeInTheDocument() + expect(screen.getByText('Procesar pedido')).toBeInTheDocument() + expect(screen.getByText('Notificar cliente')).toBeInTheDocument() + }) + + it('click en fila dispara onRowClick con el bpmnElementId correcto', () => { + const onRowClick = vi.fn() + render( + + ) + const row = screen.getByText('Procesar pedido').closest('tr')! + fireEvent.click(row) + expect(onRowClick).toHaveBeenCalledWith('task2') + expect(onRowClick).toHaveBeenCalledTimes(1) + }) + + it('fila resaltada tiene clase/estado visual distinto', () => { + render( + + ) + const highlightedRow = screen.getByText('Recibir solicitud').closest('tr')! + // Verificamos que el atributo data-state='selected' está presente en la fila resaltada + expect(highlightedRow).toHaveAttribute('data-state', 'selected') + }) + + it('tabla sin actividades: renderiza solo el header (0 data rows)', () => { + render( + + ) + const rows = screen.getAllByRole('row') + expect(rows).toHaveLength(1) // solo el header + }) +}) + +// ─── MethodologyFooter ──────────────────────────────────────────────────────── + +import { MethodologyFooter } from '@/features/report/MethodologyFooter' + +describe('MethodologyFooter', () => { + it('renderiza sin crash con un proceso válido', () => { + const proc = { + id: 'p1', name: 'Proceso Test', clientName: '', + bpmnXml: '', currency: 'USD', + overheadPercentage: 0.2, createdAt: 0, updatedAt: 0, + } + expect(() => render()).not.toThrow() + }) + + it('muestra el overhead configurado', () => { + const proc = { + id: 'p1', name: 'Proc', clientName: '', + bpmnXml: '', currency: 'USD', + overheadPercentage: 0.25, createdAt: 0, updatedAt: 0, + } + render() + const text = document.body.textContent ?? '' + expect(text).toMatch(/25%/) + }) + + it('contiene las palabras clave de la metodología', () => { + const proc = { + id: 'p1', name: 'Proc', clientName: '', + bpmnXml: '', currency: 'USD', + overheadPercentage: 0.2, createdAt: 0, updatedAt: 0, + } + render() + const text = document.body.textContent ?? '' + expect(text).toMatch(/Nota metodológica|metodol/i) + expect(text).toMatch(/probabilidad/i) + }) +}) + +// ─── ReportPage — empty state (sin simulación) ───────────────────────────────── + +vi.mock('@/features/report/useReportData', () => ({ + useReportData: vi.fn(), +})) + +import { useReportData } from '@/features/report/useReportData' +import { ReportPage } from '@/features/report/ReportPage' + +// Mock de TanStack Router para tests de ReportPage +vi.mock('@tanstack/react-router', async (importActual) => { + const actual = await importActual() + return { + ...actual, + useParams: vi.fn().mockReturnValue({ processId: 'test-proc-id' }), + useNavigate: vi.fn().mockReturnValue(vi.fn()), + } +}) + +describe('ReportPage — empty state (sin simulación)', () => { + beforeEach(() => { + vi.mocked(useReportData).mockReturnValue({ + process: null, + simulation: null, + activities: [], + resources: [], + isLoading: false, + error: 'Sin simulación disponible. Volvé al workspace y ejecutá "Simular" primero.', + }) + }) + + it('muestra mensaje de error cuando no hay simulación', () => { + render() + expect(screen.getByText(/Sin simulación disponible/i)).toBeInTheDocument() + }) + + it('muestra botón para ir al workspace', () => { + render() + const btn = screen.getByText(/Volver a configurar y simular/i) + expect(btn).toBeInTheDocument() + }) +}) + +describe('ReportPage — loading state', () => { + beforeEach(() => { + vi.mocked(useReportData).mockReturnValue({ + process: null, + simulation: null, + activities: [], + resources: [], + isLoading: true, + error: null, + }) + }) + + it('muestra skeleton de carga (no pantalla en blanco)', () => { + render() + // El skeleton usa animate-pulse en el contenedor + const skeleton = document.querySelector('.animate-pulse') + expect(skeleton).not.toBeNull() + }) +}) + +describe('ReportPage — con datos completos', () => { + const mockProcess = { + id: 'test-proc-id', + name: 'Proceso de Aprobación de Crédito', + clientName: 'Banco ABC', + bpmnXml: 'f1f1f2f2', + currency: 'USD', + overheadPercentage: 0.2, + createdAt: Date.now() - 1000 * 60 * 30, + updatedAt: Date.now() - 1000 * 60 * 30, + } + + const mockSimulation = { + id: 'sim-1', + processId: 'test-proc-id', + executedAt: Date.now() - 1000 * 60 * 10, + result: mockResult, + } + + beforeEach(() => { + vi.mocked(useReportData).mockReturnValue({ + process: mockProcess, + simulation: mockSimulation, + activities: [], + resources: [], + isLoading: false, + error: null, + }) + }) + + it('renderiza el h1 con el nombre del proceso', () => { + render() + const h1 = screen.getByRole('heading', { level: 1 }) + expect(h1).toHaveTextContent('Proceso de Aprobación de Crédito') + }) + + it('hay exactamente 4 KPI cards', () => { + render() + const cards = screen.getAllByTestId('kpi-card') + expect(cards).toHaveLength(4) + }) + + it('hay al menos 2 gráficos ECharts (TopActivities + Donut siempre; CostByResource solo con recursos)', () => { + render() + const charts = screen.getAllByTestId('echarts-chart') + // TopActivities y CostComposition siempre renderizan. + // CostByResource muestra empty-state cuando resources=[] y sin breakdown. + expect(charts.length).toBeGreaterThanOrEqual(2) + }) + + it('la tabla tiene N+1 rows (N actividades + 1 header)', () => { + render() + const rows = screen.getAllByRole('row') + expect(rows).toHaveLength(mockActivities.length + 1) + }) + + it('el nombre del cliente aparece en el reporte', () => { + render() + expect(screen.getByText(/Banco ABC/)).toBeInTheDocument() + }) + + it('el timestamp de simulación es visible', () => { + render() + const text = document.body.textContent ?? '' + // "Simulado el" debe aparecer en el header + expect(text).toMatch(/Simulado el/i) + }) + + it('no hay banner de warnings cuando no hay warnings', () => { + render() + expect(screen.queryByText(/Advertencia/i)).not.toBeInTheDocument() + }) + + it('con warnings: el banner aparece', () => { + vi.mocked(useReportData).mockReturnValue({ + process: mockProcess, + simulation: { + ...mockSimulation, + result: { ...mockResult, warnings: ['Loop detectado en "Inspección"'] }, + }, + activities: [], + resources: [], + isLoading: false, + error: null, + }) + render() + expect(screen.getByText(/Loop detectado/)).toBeInTheDocument() + }) +}) diff --git a/tests/features/report/derivations.test.ts b/tests/features/report/derivations.test.ts new file mode 100644 index 0000000..810a97e --- /dev/null +++ b/tests/features/report/derivations.test.ts @@ -0,0 +1,293 @@ +/** + * Tests de derivación de datos del reporte. + * Para cada uno de los 3 sample BPMNs, simula con costos predefinidos + * y verifica invariantes matemáticos y de presentación. + */ +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'fs' +import { resolve } from 'path' +import { v4 as uuidv4 } from 'uuid' +import { parseBpmnXml, extractActivityElements, extractGatewayElements } from '@/domain/bpmn-parser' +import { runSimulation } from '@/domain/simulation' +import { buildCostCompositionOption, buildTopActivitiesOption, aggregateResourceCosts } from '@/lib/chart-options' +import { formatCurrency } from '@/lib/format' +import { bpmnTypeToGatewayType } from '@/domain/types' +import type { Activity, GatewayConfig, Resource, SimulationInput } from '@/domain/types' + +function loadBpmn(name: string): string { + return readFileSync(resolve(__dirname, '../../../public/sample-processes', name), 'utf-8') +} + +function buildSimInput(xml: string, directCost: number, overhead: number, currency = 'USD'): SimulationInput { + const graph = parseBpmnXml(xml) + const actEls = extractActivityElements(graph) + const gwEls = extractGatewayElements(graph) + const processId = 'report-test' + + const activities = new Map( + actEls.map((el) => [el.bpmnElementId, { + id: uuidv4(), processId, bpmnElementId: el.bpmnElementId, + name: el.name, type: el.type, + directCostFixed: directCost, + executionTimeMinutes: 30, + assignedResources: [], + }]) + ) + + const gateways = new Map( + gwEls.map((el) => { + const gwType = bpmnTypeToGatewayType(el.gatewayType as any) + const n = el.outgoing.length + return [el.bpmnElementId, { + id: uuidv4(), processId, bpmnElementId: el.bpmnElementId, + gatewayType: gwType, + branches: el.outgoing.map((flowId) => ({ + flowId, targetElementId: graph.flows.get(flowId)?.targetRef ?? '', + probability: gwType === 'parallel' ? 1.0 : parseFloat((1 / n).toFixed(4)), + })), + }] + }) + ) + + return { + processXml: xml, + activities, + gateways, + resources: new Map(), + globalSettings: { currency, overheadPercentage: overhead }, + } +} + +// ─── INVARIANTES MATEMÁTICOS ───────────────────────────────────────────────── + +describe('invariantes matemáticos — los 3 BPMNs', () => { + const BPMNS = [ + { name: 'simple-linear.bpmn', overhead: 0.2, label: 'simple-linear' }, + { name: 'medium-with-gateways.bpmn', overhead: 0.15, label: 'medium-gateways' }, + { name: 'complex-with-loop.bpmn', overhead: 0.25, label: 'complex-loop' }, + ] + + for (const { name, overhead, label } of BPMNS) { + describe(label, () => { + const xml = loadBpmn(name) + const result = runSimulation(buildSimInput(xml, 100, overhead)) + + it('directo + indirecto = total (dentro de tolerancia float)', () => { + expect(result.totalDirectCost + result.totalIndirectCost).toBeCloseTo(result.totalCost, 6) + }) + + it('overhead indirecto = directCost × overheadPct', () => { + expect(result.totalIndirectCost).toBeCloseTo(result.totalDirectCost * overhead, 6) + }) + + it('suma de percentOfTotal de todas las actividades ≈ 100%', () => { + const sum = result.perActivity.reduce((s, a) => s + a.percentOfTotal, 0) + expect(sum).toBeCloseTo(100, 1) + }) + + it('suma de expectedTotalCost de actividades ≈ totalCost', () => { + const sum = result.perActivity.reduce((s, a) => s + a.expectedTotalCost, 0) + expect(sum).toBeCloseTo(result.totalCost, 4) + }) + + it('perActivity ordenado desc por expectedTotalCost', () => { + for (let i = 0; i < result.perActivity.length - 1; i++) { + expect(result.perActivity[i].expectedTotalCost).toBeGreaterThanOrEqual( + result.perActivity[i + 1].expectedTotalCost + ) + } + }) + + it('cantidad de filas de tabla = cantidad de Activity parseadas', () => { + const graph = parseBpmnXml(xml) + const actEls = extractActivityElements(graph) + expect(result.perActivity).toHaveLength(actEls.length) + }) + + it('cada actividad: directCost + indirectCost = totalCost (dentro de tolerancia)', () => { + for (const act of result.perActivity) { + expect(act.expectedDirectCost + act.expectedIndirectCost).toBeCloseTo(act.expectedTotalCost, 4) + } + }) + + it('sin recursos: suma de resourceCostBreakdown = 0 para cada actividad', () => { + for (const act of result.perActivity) { + const resourceSum = act.resourceCostBreakdown.reduce((s, r) => s + r.cost, 0) + expect(resourceSum).toBeCloseTo(0, 6) + } + }) + + it('totalTimeMinutes > 0 cuando hay actividades con tiempo > 0', () => { + expect(result.totalTimeMinutes).toBeGreaterThan(0) + }) + }) + } +}) + +// ─── FORMATO DE MONEDA EN EL REPORTE ───────────────────────────────────────── + +describe('formato de moneda en datos del reporte', () => { + it('USD: KPI costo total formateado correctamente ($1,200.00)', () => { + const xml = loadBpmn('simple-linear.bpmn') + const result = runSimulation(buildSimInput(xml, 100, 0.2, 'USD')) + const formatted = formatCurrency(result.totalCost, 'USD') + // Simple-linear: 5 actividades × $100 × 1.2 = $600 + expect(formatted).toMatch(/600/) + expect(formatted).toMatch(/\$|USD/) + }) + + it('PYG: KPI costo total formateado correctamente (sin decimales en PYG)', () => { + const xml = loadBpmn('simple-linear.bpmn') + const result = runSimulation(buildSimInput(xml, 100000, 0.2, 'PYG')) + const formatted = formatCurrency(result.totalCost, 'PYG') + expect(formatted).toBeTruthy() + expect(formatted).toMatch(/600/) + }) + + it('USD vs PYG: misma cantidad produce representaciones distintas', () => { + const amountUsd = formatCurrency(1000, 'USD') + const amountPyg = formatCurrency(1000, 'PYG') + expect(amountUsd).not.toBe(amountPyg) + }) +}) + +// ─── DONUT CHART OPTION — snapshot del objeto ECharts ──────────────────────── + +describe('buildCostCompositionOption — estructura del objeto ECharts', () => { + it('tiene exactamente 1 serie de tipo pie', () => { + const xml = loadBpmn('simple-linear.bpmn') + const result = runSimulation(buildSimInput(xml, 100, 0.2)) + const option = buildCostCompositionOption(result, 'USD') + const series = option.series as any[] + expect(series).toHaveLength(1) + expect(series[0].type).toBe('pie') + }) + + it('con overhead > 0: data tiene 2 items (directo + indirecto)', () => { + const xml = loadBpmn('simple-linear.bpmn') + const result = runSimulation(buildSimInput(xml, 100, 0.2)) + const option = buildCostCompositionOption(result, 'USD') + const series = option.series as any[] + expect(series[0].data).toHaveLength(2) + expect(series[0].data[0].name).toBe('Costo directo') + expect(series[0].data[1].name).toBe('Costo indirecto (overhead)') + }) + + it('con overhead = 0: data tiene solo 1 item (solo directo)', () => { + const xml = loadBpmn('simple-linear.bpmn') + const result = runSimulation(buildSimInput(xml, 100, 0)) + const option = buildCostCompositionOption(result, 'USD') + const series = option.series as any[] + expect(series[0].data).toHaveLength(1) + expect(series[0].data[0].name).toBe('Costo directo') + }) + + it('los valores de data suman al totalCost', () => { + const xml = loadBpmn('medium-with-gateways.bpmn') + const result = runSimulation(buildSimInput(xml, 100, 0.2)) + const option = buildCostCompositionOption(result, 'USD') + const series = option.series as any[] + const dataSum = (series[0].data as { value: number }[]).reduce((s, d) => s + d.value, 0) + expect(dataSum).toBeCloseTo(result.totalCost, 4) + }) + + it('el graphic label contiene el totalCost formateado', () => { + const result = { totalDirectCost: 1000, totalIndirectCost: 200, totalCost: 1200 } + const option = buildCostCompositionOption(result, 'USD') + const graphic = option.graphic as any[] + expect(graphic[0].style.text).toContain('1') + expect(graphic[0].style.text).toContain('200') + }) + + it('snapshot: estructura completa del option del donut', () => { + const result = { totalDirectCost: 1000, totalIndirectCost: 200, totalCost: 1200 } + const option = buildCostCompositionOption(result, 'USD') + expect(option).toMatchSnapshot() + }) +}) + +// ─── TOP ACTIVITIES CHART — estructura ─────────────────────────────────────── + +describe('buildTopActivitiesOption — estructura', () => { + it('serie de tipo bar con hasta 10 items', () => { + const xml = loadBpmn('medium-with-gateways.bpmn') + const result = runSimulation(buildSimInput(xml, 100, 0.2)) + const option = buildTopActivitiesOption(result.perActivity, 'relative', 'USD') + const series = option.series as any[] + expect(series[0].type).toBe('bar') + expect(series[0].data.length).toBeLessThanOrEqual(10) + expect(series[0].data.length).toBeGreaterThan(0) + }) + + it('la primera barra (actividad más costosa) tiene color del extremo cálido', () => { + const xml = loadBpmn('simple-linear.bpmn') + const result = runSimulation(buildSimInput(xml, 200, 0)) + // En modo relativo con varianza real, la 1ra actividad → t=1 → rojo + // En simple-linear todos cuestan igual → NEUTRAL_HEATMAP_COLOR + const option = buildTopActivitiesOption(result.perActivity, 'relative', 'USD') + const series = option.series as any[] + // Verifica que itemStyle.color existe en cada barra + series[0].data.forEach((item: any) => { + expect(item.itemStyle).toBeDefined() + expect(item.itemStyle.color).toMatch(/^#[0-9a-f]{6}$/) + }) + }) + + it('yAxis data contiene los nombres de actividades (truncados)', () => { + const xml = loadBpmn('simple-linear.bpmn') + const result = runSimulation(buildSimInput(xml, 100, 0)) + const option = buildTopActivitiesOption(result.perActivity, 'relative', 'USD') + const yAxis = option.yAxis as any + expect(yAxis.data).toHaveLength(result.perActivity.length) + // Los nombres deben existir (no vacíos) + yAxis.data.forEach((name: string) => expect(name.length).toBeGreaterThan(0)) + }) +}) + +// ─── AGGREGATE RESOURCE COSTS ───────────────────────────────────────────────── + +describe('aggregateResourceCosts', () => { + it('sin recursos: activeTypes vacío, totales en cero', () => { + const xml = loadBpmn('simple-linear.bpmn') + const result = runSimulation(buildSimInput(xml, 100, 0)) + const { byType, activeTypes } = aggregateResourceCosts(result.perActivity, []) + expect(activeTypes).toHaveLength(0) + expect(Object.keys(byType)).toHaveLength(0) + }) + + it('con recursos de tipo role: suma correctamente por tipo', () => { + const resource: Resource = { + id: 'r1', processId: 'p1', name: 'Analista', type: 'role', costPerHour: 60, + } + const perActivity = [{ + activityId: 'a1', bpmnElementId: 't1', activityName: 'Task', type: 'task' as const, + expectedDirectCost: 60, expectedIndirectCost: 12, expectedTotalCost: 72, + percentOfTotal: 100, executionProbability: 1, expectedExecutions: 1, + executionTimeMinutes: 60, + resourceCostBreakdown: [{ resourceId: 'r1', resourceName: 'Analista', cost: 60 }], + }] + const { byType, activeTypes } = aggregateResourceCosts(perActivity, [resource]) + expect(activeTypes).toContain('role') + expect(byType.role).toBeCloseTo(60) + }) +}) + +// ─── EMPTY STATE / ERROR STATE ─────────────────────────────────────────────── + +describe('estados edge del reporte', () => { + it('resultado con 0 actividades: perActivity vacío → totalCost = 0', () => { + const result = runSimulation({ + processXml: ` + f1 + f1 + `, + activities: new Map(), + gateways: new Map(), + resources: new Map(), + globalSettings: { currency: 'USD', overheadPercentage: 0.2 }, + }) + expect(result.perActivity).toHaveLength(0) + expect(result.totalCost).toBe(0) + expect(result.warnings).toHaveLength(0) + }) +}) diff --git a/tests/integration/bpmn-parser.test.ts b/tests/integration/bpmn-parser.test.ts new file mode 100644 index 0000000..4a51cb4 --- /dev/null +++ b/tests/integration/bpmn-parser.test.ts @@ -0,0 +1,251 @@ +/** + * Tests permanentes del parser BPMN. + * Protegen contra regresiones cuando se modifique parseBpmnXml, + * extractActivityElements o extractGatewayElements. + * + * Conteos esperados (fijos, derivados del XML de los sample BPMNs): + * + * simple-linear.bpmn + * nodes total : 7 (1 startEvent + 5 tasks + 1 endEvent) + * flows total : 6 + * tasks : 5 + * gateways : 0 + * events : 2 + * divergentes : 0 + * + * medium-with-gateways.bpmn + * nodes total : 17 (1 start + 11 tasks + 4 gateways + 1 end) + * flows total : 19 + * tasks : 11 + * gateways : 4 (gw_datos XOR, gw_parallel_split AND, gw_parallel_join AND, gw_decision XOR) + * events : 2 + * divergentes : 3 (gw_datos:2, gw_parallel_split:2, gw_decision:2) + * + * complex-with-loop.bpmn + * nodes total : 13 (1 start + 9 tasks + 2 gateways + 1 end) + * flows total : 14 + * tasks : 9 + * gateways : 2 (gw_resultado XOR 3-salidas, gw_merge XOR convergente) + * events : 2 + * divergentes : 1 (gw_resultado:3) + */ + +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'fs' +import { resolve } from 'path' +import { + parseBpmnXml, + extractActivityElements, + extractGatewayElements, + BpmnParseError, +} from '@/domain/bpmn-parser' +import { isTaskNode, isGatewayNode } from '@/domain/types' + +function loadSample(name: string): string { + return readFileSync(resolve(__dirname, '../../public/sample-processes', name), 'utf-8') +} + +function countByCategory(graph: ReturnType) { + let tasks = 0, gateways = 0, events = 0, unknown = 0 + for (const node of graph.nodes.values()) { + if (isTaskNode(node.type)) tasks++ + else if (isGatewayNode(node.type)) gateways++ + else if (['startEvent', 'endEvent', 'intermediateThrowEvent', 'intermediateCatchEvent'].includes(node.type)) events++ + else unknown++ + } + return { tasks, gateways, events, unknown } +} + +// ─── simple-linear.bpmn ────────────────────────────────────────────────────── +describe('parser / simple-linear.bpmn', () => { + const xml = loadSample('simple-linear.bpmn') + + it('parsea sin lanzar error', () => { + expect(() => parseBpmnXml(xml)).not.toThrow() + }) + + it('grafo: 7 nodos y 6 flujos de secuencia', () => { + const g = parseBpmnXml(xml) + expect(g.nodes.size).toBe(7) + expect(g.flows.size).toBe(6) + }) + + it('grafo: 5 tasks | 0 gateways | 2 eventos', () => { + const g = parseBpmnXml(xml) + const { tasks, gateways, events, unknown } = countByCategory(g) + expect(tasks).toBe(5) + expect(gateways).toBe(0) + expect(events).toBe(2) + expect(unknown).toBe(0) + }) + + it('1 startEvent detectado como punto de entrada', () => { + const g = parseBpmnXml(xml) + expect(g.startEventIds).toHaveLength(1) + expect(g.startEventIds[0]).toBe('start') + }) + + it('extractActivityElements: 5 actividades con nombres correctos', () => { + const g = parseBpmnXml(xml) + const acts = extractActivityElements(g) + expect(acts).toHaveLength(5) + const names = acts.map((a) => a.name) + expect(names).toContain('Recibir solicitud') + expect(names).toContain('Verificar documentación') + expect(names).toContain('Procesar pedido') + expect(names).toContain('Notificar al cliente') + expect(names).toContain('Archivar expediente') + acts.forEach((a) => expect(a.type).toBe('task')) + }) + + it('extractGatewayElements: 0 gateways divergentes (proceso lineal)', () => { + const g = parseBpmnXml(xml) + const gws = extractGatewayElements(g) + expect(gws).toHaveLength(0) + }) +}) + +// ─── medium-with-gateways.bpmn ─────────────────────────────────────────────── +describe('parser / medium-with-gateways.bpmn', () => { + const xml = loadSample('medium-with-gateways.bpmn') + + it('parsea sin lanzar error', () => { + expect(() => parseBpmnXml(xml)).not.toThrow() + }) + + it('grafo: 17 nodos y 19 flujos de secuencia', () => { + const g = parseBpmnXml(xml) + expect(g.nodes.size).toBe(17) + expect(g.flows.size).toBe(19) + }) + + it('grafo: 11 tasks | 4 gateways | 2 eventos', () => { + const g = parseBpmnXml(xml) + const { tasks, gateways, events, unknown } = countByCategory(g) + expect(tasks).toBe(11) + expect(gateways).toBe(4) + expect(events).toBe(2) + expect(unknown).toBe(0) + }) + + it('extractActivityElements: exactamente 11 actividades', () => { + const g = parseBpmnXml(xml) + const acts = extractActivityElements(g) + expect(acts).toHaveLength(11) + const ids = acts.map((a) => a.bpmnElementId) + expect(ids).toContain('task_recv') + expect(ids).toContain('task_valid') + expect(ids).toContain('task_pedir_docs') + expect(ids).toContain('task_score') + expect(ids).toContain('task_bureau') + expect(ids).toContain('task_empleador') + expect(ids).toContain('task_analisis') + expect(ids).toContain('task_aprobar') + expect(ids).toContain('task_rechazar') + expect(ids).toContain('task_desembolso') + expect(ids).toContain('task_archivo') + }) + + it('extractGatewayElements: 3 gateways divergentes con salidas correctas', () => { + const g = parseBpmnXml(xml) + const gws = extractGatewayElements(g) + expect(gws).toHaveLength(3) + + const byId = Object.fromEntries(gws.map((gw) => [gw.bpmnElementId, gw])) + + // gw_datos: XOR con 2 salidas + expect(byId['gw_datos']).toBeDefined() + expect(byId['gw_datos'].outgoing).toHaveLength(2) + expect(byId['gw_datos'].gatewayType).toBe('exclusiveGateway') + + // gw_parallel_split: AND con 2 salidas + expect(byId['gw_parallel_split']).toBeDefined() + expect(byId['gw_parallel_split'].outgoing).toHaveLength(2) + expect(byId['gw_parallel_split'].gatewayType).toBe('parallelGateway') + + // gw_decision: XOR con 2 salidas + expect(byId['gw_decision']).toBeDefined() + expect(byId['gw_decision'].outgoing).toHaveLength(2) + expect(byId['gw_decision'].gatewayType).toBe('exclusiveGateway') + + // gw_parallel_join NO debe estar (convergente: 1 salida) + expect(byId['gw_parallel_join']).toBeUndefined() + }) +}) + +// ─── complex-with-loop.bpmn ─────────────────────────────────────────────────── +describe('parser / complex-with-loop.bpmn', () => { + const xml = loadSample('complex-with-loop.bpmn') + + it('parsea sin lanzar error', () => { + expect(() => parseBpmnXml(xml)).not.toThrow() + }) + + it('grafo: 13 nodos y 14 flujos de secuencia', () => { + const g = parseBpmnXml(xml) + expect(g.nodes.size).toBe(13) + expect(g.flows.size).toBe(14) + }) + + it('grafo: 9 tasks | 2 gateways | 2 eventos', () => { + const g = parseBpmnXml(xml) + const { tasks, gateways, events, unknown } = countByCategory(g) + expect(tasks).toBe(9) + expect(gateways).toBe(2) + expect(events).toBe(2) + expect(unknown).toBe(0) + }) + + it('extractActivityElements: exactamente 9 actividades', () => { + const g = parseBpmnXml(xml) + const acts = extractActivityElements(g) + expect(acts).toHaveLength(9) + const ids = acts.map((a) => a.bpmnElementId) + expect(ids).toContain('task_preparar') + expect(ids).toContain('task_inspeccion') + expect(ids).toContain('task_documentar') + expect(ids).toContain('task_correcciones') + expect(ids).toContain('task_rechazo') + expect(ids).toContain('task_informe_rechazo') + expect(ids).toContain('task_entrega') + expect(ids).toContain('task_certificado') + expect(ids).toContain('task_registro') + }) + + it('extractGatewayElements: 1 gateway divergente (gw_resultado con 3 salidas)', () => { + const g = parseBpmnXml(xml) + const gws = extractGatewayElements(g) + expect(gws).toHaveLength(1) + expect(gws[0].bpmnElementId).toBe('gw_resultado') + expect(gws[0].outgoing).toHaveLength(3) + expect(gws[0].gatewayType).toBe('exclusiveGateway') + // gw_merge es convergente (1 salida) — no debe aparecer + }) + + it('el flujo de loop existe en el grafo (correc_inspeccion apunta a task_inspeccion)', () => { + const g = parseBpmnXml(xml) + const loopFlow = g.flows.get('flow_correc_inspeccion') + expect(loopFlow).toBeDefined() + expect(loopFlow!.sourceRef).toBe('task_correcciones') + expect(loopFlow!.targetRef).toBe('task_inspeccion') + }) +}) + +// ─── Casos de error del parser ──────────────────────────────────────────────── +describe('parser / manejo de errores', () => { + it('lanza BpmnParseError si el XML no tiene elemento ', () => { + expect(() => parseBpmnXml('')).toThrow(BpmnParseError) + }) + + it('lanza BpmnParseError si el XML está malformado', () => { + expect(() => parseBpmnXml(' { + const xml = ` + + +` + expect(() => parseBpmnXml(xml)).toThrow(BpmnParseError) + }) +}) diff --git a/tests/integration/gateway-validation.test.ts b/tests/integration/gateway-validation.test.ts new file mode 100644 index 0000000..646c40c --- /dev/null +++ b/tests/integration/gateway-validation.test.ts @@ -0,0 +1,157 @@ +/** + * Tests permanentes de validación de gateways. + * Protegen las reglas de negocio: XOR debe sumar 1, AND no tiene probs editables. + */ + +import { describe, it, expect } from 'vitest' +import { isXorSumValid } from '@/features/workspace/GatewayPanel' +import { runSimulation } from '@/domain/simulation' +import type { GatewayConfig, SimulationInput, Activity } from '@/domain/types' +import { v4 as uuidv4 } from 'uuid' +import { readFileSync } from 'fs' +import { resolve } from 'path' + +function makeBranches(probs: number[]): GatewayConfig['branches'] { + return probs.map((p, i) => ({ flowId: `f${i}`, targetElementId: `t${i}`, probability: p })) +} + +function makeXorGateway(probs: number[]): GatewayConfig { + return { + id: uuidv4(), processId: 'p', bpmnElementId: 'gw1', + gatewayType: 'exclusive', + branches: makeBranches(probs), + } +} + +function makeParallelGateway(n: number): GatewayConfig { + return { + id: uuidv4(), processId: 'p', bpmnElementId: 'gw1', + gatewayType: 'parallel', + branches: makeBranches(Array(n).fill(1.0)), + } +} + +// ─── isXorSumValid ──────────────────────────────────────────────────────────── +describe('isXorSumValid', () => { + it('válido: suma exactamente 1.0', () => { + expect(isXorSumValid(makeXorGateway([0.6, 0.4]))).toBe(true) + }) + + it('válido: suma = 1.0 con 3 ramas (0.5 + 0.3 + 0.2)', () => { + expect(isXorSumValid(makeXorGateway([0.5, 0.3, 0.2]))).toBe(true) + }) + + it('válido: tolerancia de 0.5% — 0.999 se acepta como 1.0', () => { + expect(isXorSumValid(makeXorGateway([0.499, 0.501]))).toBe(true) + }) + + it('inválido: suma = 0.5 (faltan 50%)', () => { + expect(isXorSumValid(makeXorGateway([0.3, 0.2]))).toBe(false) + }) + + it('inválido: suma = 1.5 (excede 100%)', () => { + expect(isXorSumValid(makeXorGateway([0.8, 0.7]))).toBe(false) + }) + + it('inválido: suma = 0 (gateways sin configurar)', () => { + expect(isXorSumValid(makeXorGateway([0, 0]))).toBe(false) + }) + + it('gateway paralelo siempre válido (no aplica regla de suma)', () => { + expect(isXorSumValid(makeParallelGateway(2))).toBe(true) + expect(isXorSumValid(makeParallelGateway(3))).toBe(true) + }) + + it('gateway inclusivo tratado como exclusivo — aplica regla de suma', () => { + const gw: GatewayConfig = { ...makeXorGateway([0.6, 0.4]), gatewayType: 'inclusive' } + // inclusive usa isXorSumValid solo para el display; el motor lo trata igual + // Aquí verificamos que la función no lanza para gateways inclusive + expect(() => isXorSumValid(gw)).not.toThrow() + }) +}) + +// ─── Motor: gateways con distribución 70/30 sobre medium BPMN ──────────────── +describe('motor / propagación con GatewayConfig correcta', () => { + const xml = readFileSync(resolve(__dirname, '../../public/sample-processes/medium-with-gateways.bpmn'), 'utf-8') + + function buildSimInput(datosProbs: [number, number], decisionProbs: [number, number]): SimulationInput { + const processId = 'gv-test' + + const actIds = ['task_recv', 'task_valid', 'task_pedir_docs', 'task_score', + 'task_bureau', 'task_empleador', 'task_analisis', + 'task_aprobar', 'task_rechazar', 'task_desembolso', 'task_archivo'] + + const activities = new Map( + actIds.map((id) => [id, { + id: uuidv4(), processId, bpmnElementId: id, name: id, + type: 'task', directCostFixed: 100, executionTimeMinutes: 30, assignedResources: [], + }]) + ) + + const gateways = new Map([ + ['gw_datos', { + id: uuidv4(), processId, bpmnElementId: 'gw_datos', gatewayType: 'exclusive', + branches: [ + { flowId: 'flow_gw1_completo', targetElementId: 'task_score', probability: datosProbs[0] }, + { flowId: 'flow_gw1_incompleto', targetElementId: 'task_pedir_docs', probability: datosProbs[1] }, + ], + }], + ['gw_parallel_split', { + id: uuidv4(), processId, bpmnElementId: 'gw_parallel_split', gatewayType: 'parallel', + branches: [ + { flowId: 'flow_para_bureau', targetElementId: 'task_bureau', probability: 1.0 }, + { flowId: 'flow_para_empleador', targetElementId: 'task_empleador', probability: 1.0 }, + ], + }], + ['gw_decision', { + id: uuidv4(), processId, bpmnElementId: 'gw_decision', gatewayType: 'exclusive', + branches: [ + { flowId: 'flow_dec_aprobado', targetElementId: 'task_aprobar', probability: decisionProbs[0] }, + { flowId: 'flow_dec_rechazado', targetElementId: 'task_rechazar', probability: decisionProbs[1] }, + ], + }], + ]) + + return { processXml: xml, activities, gateways, resources: new Map(), globalSettings: { currency: 'USD', overheadPercentage: 0 } } + } + + it('rama "completo" 70% → task_score tiene prob ≈ 0.7', () => { + const result = runSimulation(buildSimInput([0.7, 0.3], [0.8, 0.2])) + const score = result.perActivity.find((a) => a.bpmnElementId === 'task_score') + expect(score?.executionProbability).toBeCloseTo(0.7, 2) + }) + + it('rama "incompleto" 30% → task_pedir_docs tiene prob ≈ 0.3', () => { + const result = runSimulation(buildSimInput([0.7, 0.3], [0.8, 0.2])) + const pedir = result.perActivity.find((a) => a.bpmnElementId === 'task_pedir_docs') + expect(pedir?.executionProbability).toBeCloseTo(0.3, 2) + }) + + it('task_aprobar tiene prob = 0.8 (task_analisis converge ambos caminos a 1.0, luego 80% decision)', () => { + // task_analisis recibe: + // - camino completo (70%): AND join correcto → 0.7 + // - camino incompleto (30%): task_pedir_docs → 0.3 + // - suma XOR: 0.7 + 0.3 = 1.0 + // gw_decision → task_aprobar: 1.0 × 0.8 = 0.8 + const result = runSimulation(buildSimInput([0.7, 0.3], [0.8, 0.2])) + const analisis = result.perActivity.find((a) => a.bpmnElementId === 'task_analisis') + const aprobar = result.perActivity.find((a) => a.bpmnElementId === 'task_aprobar') + const rechazar = result.perActivity.find((a) => a.bpmnElementId === 'task_rechazar') + expect(analisis?.executionProbability).toBeCloseTo(1.0, 2) + expect(aprobar?.executionProbability).toBeCloseTo(0.8, 2) + expect(rechazar?.executionProbability).toBeCloseTo(0.2, 2) + }) + + it('gateway AND: bureau y empleador tienen la misma prob que score', () => { + const result = runSimulation(buildSimInput([1.0, 0.0], [0.5, 0.5])) + const bureau = result.perActivity.find((a) => a.bpmnElementId === 'task_bureau') + const empleador = result.perActivity.find((a) => a.bpmnElementId === 'task_empleador') + // Ambos deben tener la misma probabilidad (AND paralelo) + expect(bureau?.executionProbability).toBeCloseTo(empleador?.executionProbability ?? -1, 2) + }) + + it('con probs que no suman 1 el motor no explota, solo produce resultados parciales', () => { + // Esto valida que el motor es robusto aunque la UI deba prevenirlo + expect(() => runSimulation(buildSimInput([0.3, 0.3], [0.5, 0.5]))).not.toThrow() + }) +}) diff --git a/tests/integration/indexeddb-persistence.test.ts b/tests/integration/indexeddb-persistence.test.ts new file mode 100644 index 0000000..1c16e46 --- /dev/null +++ b/tests/integration/indexeddb-persistence.test.ts @@ -0,0 +1,331 @@ +/** + * Tests permanentes de persistencia en IndexedDB vía Dexie. + * Usan fake-indexeddb (parcheado en tests/setup.ts) para correr en Node. + * + * Cada test limpia las tablas en beforeEach para garantizar aislamiento. + * Verifican que los repositorios escriben y recuperan datos correctamente. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { db } from '@/persistence/db' +import { + processRepo, + activityRepo, + gatewayRepo, + resourceRepo, + simulationRepo, +} from '@/persistence/repositories' +import type { Process, Activity, GatewayConfig, Resource, Simulation, SimulationResult } from '@/domain/types' + +// ─── Fixtures ───────────────────────────────────────────────────────────────── + +function makeProcess(id = 'proc-1'): Process { + return { + id, + name: 'Proceso de prueba', + clientName: 'Cliente Test', + bpmnXml: '', + currency: 'USD', + overheadPercentage: 0.2, + createdAt: 1000, + updatedAt: 2000, + } +} + +function makeActivity(id: string, processId: string, bpmnElementId: string): Activity { + return { + id, + processId, + bpmnElementId, + name: `Tarea ${bpmnElementId}`, + type: 'task', + directCostFixed: 100, + executionTimeMinutes: 30, + assignedResources: [{ resourceId: 'res-1', utilizationPercent: 0.5 }], + } +} + +function makeGateway(id: string, processId: string, bpmnElementId: string): GatewayConfig { + return { + id, + processId, + bpmnElementId, + gatewayType: 'exclusive', + branches: [ + { flowId: 'f1', targetElementId: 'taskA', probability: 0.7 }, + { flowId: 'f2', targetElementId: 'taskB', probability: 0.3 }, + ], + } +} + +function makeResource(id: string, processId: string): Resource { + return { + id, + processId, + name: 'Analista', + type: 'role', + costPerHour: 60, + } +} + +function makeSimulation(id: string, processId: string): Simulation { + const result: SimulationResult = { + totalCost: 1200, + totalDirectCost: 1000, + totalIndirectCost: 200, + totalTimeMinutes: 90, + perActivity: [], + perResource: [], + warnings: [], + } + return { id, processId, executedAt: 3000, result } +} + +// ─── Setup / Teardown ───────────────────────────────────────────────────────── + +beforeEach(async () => { + await db.open() + await db.transaction( + 'rw', + [db.processes, db.activities, db.gateways, db.resources, db.simulations], + async () => { + await Promise.all([ + db.processes.clear(), + db.activities.clear(), + db.gateways.clear(), + db.resources.clear(), + db.simulations.clear(), + ]) + } + ) +}) + +afterEach(async () => { + await db.close() +}) + +// ─── Process ───────────────────────────────────────────────────────────────── +describe('IndexedDB / processRepo', () => { + it('guarda un proceso y lo recupera por id', async () => { + const proc = makeProcess() + await processRepo.save(proc) + const retrieved = await processRepo.getById('proc-1') + expect(retrieved).toBeDefined() + expect(retrieved!.name).toBe('Proceso de prueba') + expect(retrieved!.currency).toBe('USD') + expect(retrieved!.overheadPercentage).toBe(0.2) + }) + + it('getById devuelve undefined para id inexistente', async () => { + const result = await processRepo.getById('no-existe') + expect(result).toBeUndefined() + }) + + it('getAll devuelve todos los procesos guardados', async () => { + await processRepo.save(makeProcess('p1')) + await processRepo.save(makeProcess('p2')) + await processRepo.save(makeProcess('p3')) + const all = await processRepo.getAll() + expect(all).toHaveLength(3) + }) + + it('save sobrescribe (put) un proceso existente', async () => { + const proc = makeProcess() + await processRepo.save(proc) + await processRepo.save({ ...proc, name: 'Nombre actualizado', updatedAt: 9999 }) + const updated = await processRepo.getById('proc-1') + expect(updated!.name).toBe('Nombre actualizado') + expect(updated!.updatedAt).toBe(9999) + }) + + it('delete elimina el proceso y sus entidades relacionadas en cascada', async () => { + const proc = makeProcess('proc-del') + await processRepo.save(proc) + await activityRepo.saveMany([makeActivity('a1', 'proc-del', 'task1')]) + await gatewayRepo.saveMany([makeGateway('g1', 'proc-del', 'gw1')]) + await resourceRepo.save(makeResource('r1', 'proc-del')) + + await processRepo.delete('proc-del') + + expect(await processRepo.getById('proc-del')).toBeUndefined() + expect(await activityRepo.getByProcess('proc-del')).toHaveLength(0) + expect(await gatewayRepo.getByProcess('proc-del')).toHaveLength(0) + expect(await resourceRepo.getByProcess('proc-del')).toHaveLength(0) + }) +}) + +// ─── Activity ───────────────────────────────────────────────────────────────── +describe('IndexedDB / activityRepo', () => { + const processId = 'proc-act' + + it('saveMany y getByProcess devuelven las mismas actividades', async () => { + const acts = [ + makeActivity('a1', processId, 'task1'), + makeActivity('a2', processId, 'task2'), + makeActivity('a3', processId, 'task3'), + ] + await activityRepo.saveMany(acts) + const retrieved = await activityRepo.getByProcess(processId) + expect(retrieved).toHaveLength(3) + const ids = retrieved.map((a) => a.id).sort() + expect(ids).toEqual(['a1', 'a2', 'a3']) + }) + + it('persiste assignedResources (array anidado) correctamente', async () => { + const act = makeActivity('a1', processId, 'task1') + await activityRepo.save(act) + const [retrieved] = await activityRepo.getByProcess(processId) + expect(retrieved.assignedResources).toHaveLength(1) + expect(retrieved.assignedResources[0].resourceId).toBe('res-1') + expect(retrieved.assignedResources[0].utilizationPercent).toBe(0.5) + }) + + it('no devuelve actividades de otro proceso', async () => { + await activityRepo.save(makeActivity('a-other', 'otro-proc', 'task1')) + const result = await activityRepo.getByProcess(processId) + expect(result).toHaveLength(0) + }) +}) + +// ─── GatewayConfig ──────────────────────────────────────────────────────────── +describe('IndexedDB / gatewayRepo', () => { + const processId = 'proc-gw' + + it('saveMany y getByProcess devuelven la config con branches correctas', async () => { + const gw = makeGateway('g1', processId, 'gw_decision') + await gatewayRepo.saveMany([gw]) + const [retrieved] = await gatewayRepo.getByProcess(processId) + expect(retrieved.bpmnElementId).toBe('gw_decision') + expect(retrieved.gatewayType).toBe('exclusive') + expect(retrieved.branches).toHaveLength(2) + expect(retrieved.branches[0].probability).toBe(0.7) + expect(retrieved.branches[1].probability).toBe(0.3) + }) + + it('save individual actualiza la config existente', async () => { + const gw = makeGateway('g1', processId, 'gw1') + await gatewayRepo.save(gw) + const updated: GatewayConfig = { + ...gw, + branches: [ + { flowId: 'f1', targetElementId: 'taskA', probability: 0.6 }, + { flowId: 'f2', targetElementId: 'taskB', probability: 0.4 }, + ], + } + await gatewayRepo.save(updated) + const [retrieved] = await gatewayRepo.getByProcess(processId) + expect(retrieved.branches[0].probability).toBe(0.6) + }) +}) + +// ─── Resource ───────────────────────────────────────────────────────────────── +describe('IndexedDB / resourceRepo', () => { + const processId = 'proc-res' + + it('save y getByProcess recupera el recurso correctamente', async () => { + await resourceRepo.save(makeResource('r1', processId)) + const [retrieved] = await resourceRepo.getByProcess(processId) + expect(retrieved.name).toBe('Analista') + expect(retrieved.costPerHour).toBe(60) + expect(retrieved.type).toBe('role') + }) + + it('delete elimina un recurso por id', async () => { + await resourceRepo.save(makeResource('r1', processId)) + await resourceRepo.save(makeResource('r2', processId)) + await resourceRepo.delete('r1') + const remaining = await resourceRepo.getByProcess(processId) + expect(remaining).toHaveLength(1) + expect(remaining[0].id).toBe('r2') + }) + + it('múltiples recursos para el mismo proceso', async () => { + const types = ['role', 'system', 'equipment'] as const + for (const [i, type] of types.entries()) { + await resourceRepo.save({ ...makeResource(`r${i}`, processId), type }) + } + const all = await resourceRepo.getByProcess(processId) + expect(all).toHaveLength(3) + const typeSet = new Set(all.map((r) => r.type)) + expect(typeSet.has('role')).toBe(true) + expect(typeSet.has('system')).toBe(true) + expect(typeSet.has('equipment')).toBe(true) + }) +}) + +// ─── Simulation ─────────────────────────────────────────────────────────────── +describe('IndexedDB / simulationRepo', () => { + const processId = 'proc-sim' + + it('save y getLatestByProcess recuperan el snapshot de resultado', async () => { + const sim = makeSimulation('sim-1', processId) + await simulationRepo.save(sim) + const retrieved = await simulationRepo.getLatestByProcess(processId) + expect(retrieved).toBeDefined() + expect(retrieved!.result.totalCost).toBe(1200) + expect(retrieved!.result.totalDirectCost).toBe(1000) + expect(retrieved!.result.totalIndirectCost).toBe(200) + }) + + it('getLatestByProcess devuelve undefined si no hay simulaciones', async () => { + const result = await simulationRepo.getLatestByProcess('proc-sin-sim') + expect(result).toBeUndefined() + }) + + it('múltiples simulaciones: getByProcess las recupera todas', async () => { + await simulationRepo.save({ ...makeSimulation('s1', processId), executedAt: 1000 }) + await simulationRepo.save({ ...makeSimulation('s2', processId), executedAt: 2000 }) + await simulationRepo.save({ ...makeSimulation('s3', processId), executedAt: 3000 }) + const all = await simulationRepo.getByProcess(processId) + expect(all).toHaveLength(3) + }) + + it('persiste warnings en el resultado (campo libre)', async () => { + const sim: Simulation = { + ...makeSimulation('s-warn', processId), + result: { + ...makeSimulation('s-warn', processId).result, + warnings: ['Loop detectado en taskX'], + }, + } + await simulationRepo.save(sim) + const retrieved = await simulationRepo.getLatestByProcess(processId) + expect(retrieved!.result.warnings).toHaveLength(1) + expect(retrieved!.result.warnings[0]).toContain('Loop detectado') + }) +}) + +// ─── Round-trip completo (simula el flujo de ImportPage) ───────────────────── +describe('IndexedDB / round-trip proceso completo', () => { + it('guarda proceso + actividades + gateways + recursos, luego los recupera íntegros', async () => { + const proc = makeProcess('rt-proc') + const acts = [ + makeActivity('rt-a1', 'rt-proc', 'taskA'), + makeActivity('rt-a2', 'rt-proc', 'taskB'), + ] + const gws = [makeGateway('rt-g1', 'rt-proc', 'gw1')] + const resources = [makeResource('rt-r1', 'rt-proc')] + + await Promise.all([ + processRepo.save(proc), + activityRepo.saveMany(acts), + gatewayRepo.saveMany(gws), + resourceRepo.save(resources[0]), + ]) + + // Simular recarga de página + const [retrievedProc, retrievedActs, retrievedGws, retrievedRes] = await Promise.all([ + processRepo.getById('rt-proc'), + activityRepo.getByProcess('rt-proc'), + gatewayRepo.getByProcess('rt-proc'), + resourceRepo.getByProcess('rt-proc'), + ]) + + expect(retrievedProc!.id).toBe('rt-proc') + expect(retrievedActs).toHaveLength(2) + expect(retrievedGws).toHaveLength(1) + expect(retrievedGws[0].branches).toHaveLength(2) + expect(retrievedRes).toHaveLength(1) + expect(retrievedRes[0].costPerHour).toBe(60) + }) +}) diff --git a/tests/integration/simulation-engine.test.ts b/tests/integration/simulation-engine.test.ts new file mode 100644 index 0000000..1fadc5d --- /dev/null +++ b/tests/integration/simulation-engine.test.ts @@ -0,0 +1,278 @@ +/** + * Tests permanentes del motor de simulación corriendo sobre los 3 sample BPMNs. + * Protegen contra regresiones en runSimulation, la propagación de probabilidades + * y la detección de loops. + * + * Costos dummy predecibles para que las aserciones sean deterministas. + */ + +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'fs' +import { resolve } from 'path' +import { v4 as uuidv4 } from 'uuid' +import { parseBpmnXml, extractActivityElements, extractGatewayElements } from '@/domain/bpmn-parser' +import { runSimulation } from '@/domain/simulation' +import { bpmnTypeToGatewayType } from '@/domain/types' +import type { Activity, GatewayConfig, Resource, SimulationInput } from '@/domain/types' + +function loadSample(name: string): string { + return readFileSync(resolve(__dirname, '../../public/sample-processes', name), 'utf-8') +} + +// Construye un SimulationInput con costos fijos iguales para todas las actividades +function buildInput( + xml: string, + opts: { + directCostFixed?: number + executionTimeMinutes?: number + overheadPercentage?: number + gatewayProbsOverride?: Record + } = {} +): SimulationInput { + const { + directCostFixed = 100, + executionTimeMinutes = 30, + overheadPercentage = 0.2, + gatewayProbsOverride = {}, + } = opts + + const processId = 'sim-test' + const graph = parseBpmnXml(xml) + const actEls = extractActivityElements(graph) + const gwEls = extractGatewayElements(graph) + + const activities = new Map( + actEls.map((el) => [ + el.bpmnElementId, + { + id: uuidv4(), + processId, + bpmnElementId: el.bpmnElementId, + name: el.name, + type: el.type, + directCostFixed, + executionTimeMinutes, + assignedResources: [], + }, + ]) + ) + + const gateways = new Map( + gwEls.map((el) => { + const gwType = bpmnTypeToGatewayType(el.gatewayType as any) + const n = el.outgoing.length + const probsOverride = gatewayProbsOverride[el.bpmnElementId] + return [ + el.bpmnElementId, + { + id: uuidv4(), + processId, + bpmnElementId: el.bpmnElementId, + gatewayType: gwType, + branches: el.outgoing.map((flowId, i) => ({ + flowId, + targetElementId: graph.flows.get(flowId)?.targetRef ?? '', + probability: + probsOverride?.[i] ?? + (gwType === 'parallel' ? 1.0 : parseFloat((1 / n).toFixed(6))), + })), + }, + ] + }) + ) + + return { + processXml: xml, + activities, + gateways, + resources: new Map(), + globalSettings: { currency: 'USD', overheadPercentage }, + } +} + +// ─── simple-linear.bpmn ────────────────────────────────────────────────────── +describe('simulación / simple-linear.bpmn', () => { + const xml = loadSample('simple-linear.bpmn') + + it('corre sin lanzar error con costos dummy', () => { + expect(() => runSimulation(buildInput(xml))).not.toThrow() + }) + + it('no genera warnings (proceso sin loops ni gateways)', () => { + const result = runSimulation(buildInput(xml)) + expect(result.warnings).toHaveLength(0) + }) + + it('costo total = 5 × 100 × 1.2 = 600 (5 tareas, prob=1, overhead=20%)', () => { + const result = runSimulation(buildInput(xml, { directCostFixed: 100, overheadPercentage: 0.2 })) + expect(result.totalDirectCost).toBe(500) + expect(result.totalIndirectCost).toBeCloseTo(100) + expect(result.totalCost).toBeCloseTo(600) + }) + + it('perActivity tiene 5 entradas, todas con executionProbability = 1', () => { + const result = runSimulation(buildInput(xml)) + expect(result.perActivity).toHaveLength(5) + result.perActivity.forEach((a) => { + expect(a.executionProbability).toBeCloseTo(1.0) + }) + }) + + it('perActivity ordenado descendente por expectedTotalCost', () => { + const result = runSimulation(buildInput(xml, { directCostFixed: 100 })) + for (let i = 0; i < result.perActivity.length - 1; i++) { + expect(result.perActivity[i].expectedTotalCost).toBeGreaterThanOrEqual( + result.perActivity[i + 1].expectedTotalCost + ) + } + }) + + it('percentOfTotal suma exactamente 100%', () => { + const result = runSimulation(buildInput(xml)) + const sum = result.perActivity.reduce((acc, a) => acc + a.percentOfTotal, 0) + expect(sum).toBeCloseTo(100, 1) + }) + + it('tiempo total = 5 × 30 min = 150 min', () => { + const result = runSimulation(buildInput(xml, { executionTimeMinutes: 30 })) + expect(result.totalTimeMinutes).toBeCloseTo(150) + }) +}) + +// ─── medium-with-gateways.bpmn ─────────────────────────────────────────────── +describe('simulación / medium-with-gateways.bpmn', () => { + const xml = loadSample('medium-with-gateways.bpmn') + + it('corre sin lanzar error con costos dummy', () => { + expect(() => runSimulation(buildInput(xml))).not.toThrow() + }) + + it('no genera warnings (sin loops)', () => { + const result = runSimulation(buildInput(xml)) + expect(result.warnings).toHaveLength(0) + }) + + it('perActivity tiene exactamente 11 entradas', () => { + const result = runSimulation(buildInput(xml)) + expect(result.perActivity).toHaveLength(11) + }) + + it('actividades antes de cualquier gateway tienen prob = 1.0', () => { + const result = runSimulation(buildInput(xml)) + const recv = result.perActivity.find((a) => a.bpmnElementId === 'task_recv') + const valid = result.perActivity.find((a) => a.bpmnElementId === 'task_valid') + expect(recv?.executionProbability).toBeCloseTo(1.0) + expect(valid?.executionProbability).toBeCloseTo(1.0) + }) + + it('gateway XOR con probs 0.7/0.3 propaga correctamente', () => { + // gw_datos: completo(idx 0)=0.7, incompleto(idx 1)=0.3 + const result = runSimulation( + buildInput(xml, { + gatewayProbsOverride: { gw_datos: [0.7, 0.3] }, + }) + ) + const score = result.perActivity.find((a) => a.bpmnElementId === 'task_score') + const pedirDocs = result.perActivity.find((a) => a.bpmnElementId === 'task_pedir_docs') + expect(score?.executionProbability).toBeCloseTo(0.7, 1) + expect(pedirDocs?.executionProbability).toBeCloseTo(0.3, 1) + }) + + it('gateway paralelo AND: ambas ramas tienen prob igual a la entrada', () => { + // gw_datos completo=1.0 para simplificar, gw_parallel_split AND ambas =1 + const result = runSimulation( + buildInput(xml, { + gatewayProbsOverride: { + gw_datos: [1.0, 0.0], // siempre el camino completo + gw_decision: [0.5, 0.5], + }, + }) + ) + const bureau = result.perActivity.find((a) => a.bpmnElementId === 'task_bureau') + const empleador = result.perActivity.find((a) => a.bpmnElementId === 'task_empleador') + // Ambas ramas del AND deben tener prob similar (≈ prob del score) + expect(bureau?.executionProbability).toBeGreaterThan(0) + expect(empleador?.executionProbability).toBeGreaterThan(0) + expect(Math.abs((bureau?.executionProbability ?? 0) - (empleador?.executionProbability ?? 0))).toBeLessThan(0.01) + }) + + it('perActivity ordenado descendente por costo', () => { + const result = runSimulation(buildInput(xml)) + for (let i = 0; i < result.perActivity.length - 1; i++) { + expect(result.perActivity[i].expectedTotalCost).toBeGreaterThanOrEqual( + result.perActivity[i + 1].expectedTotalCost + ) + } + }) +}) + +// ─── complex-with-loop.bpmn ─────────────────────────────────────────────────── +describe('simulación / complex-with-loop.bpmn', () => { + const xml = loadSample('complex-with-loop.bpmn') + + it('corre sin lanzar error (a pesar del loop)', () => { + expect(() => runSimulation(buildInput(xml))).not.toThrow() + }) + + it('🚨 genera al menos un warning de loop detectado', () => { + const result = runSimulation(buildInput(xml)) + expect(result.warnings.length).toBeGreaterThan(0) + const hasLoopWarning = result.warnings.some( + (w) => w.toLowerCase().includes('loop') || w.toLowerCase().includes('ciclo') + ) + expect(hasLoopWarning).toBe(true) + }) + + it('el warning menciona el nodo "Inspección visual y funcional"', () => { + const result = runSimulation(buildInput(xml)) + const loopWarning = result.warnings.find( + (w) => w.toLowerCase().includes('loop') || w.toLowerCase().includes('ciclo') + ) + expect(loopWarning).toContain('Inspección visual y funcional') + }) + + it('resultado es válido a pesar del loop: costo > 0 y 9 actividades', () => { + const result = runSimulation(buildInput(xml, { directCostFixed: 100 })) + expect(result.totalCost).toBeGreaterThan(0) + expect(result.perActivity).toHaveLength(9) + }) + + it('con probabilidades reales (60% aprobado, 30% correcciones, 10% rechazo)', () => { + const result = runSimulation( + buildInput(xml, { + directCostFixed: 200, + overheadPercentage: 0.15, + gatewayProbsOverride: { + // gw_resultado: 3 salidas — aprobado 0.6, obs_menores 0.3, rechazo 0.1 + gw_resultado: [0.6, 0.3, 0.1], + }, + }) + ) + + expect(result.totalCost).toBeGreaterThan(0) + expect(result.warnings.length).toBeGreaterThan(0) + + // task_inspeccion tiene prob alta (es el nodo más costoso del proceso con loop) + const inspeccion = result.perActivity.find((a) => a.bpmnElementId === 'task_inspeccion') + expect(inspeccion).toBeDefined() + expect(inspeccion!.executionProbability).toBeGreaterThan(0) + + // task_correcciones tiene prob = prob del camino de correcciones ≈ 0.3 + const correcciones = result.perActivity.find((a) => a.bpmnElementId === 'task_correcciones') + expect(correcciones).toBeDefined() + expect(correcciones!.executionProbability).toBeCloseTo(0.3, 1) + + // task_rechazo tiene prob ≈ 0.1 + const rechazo = result.perActivity.find((a) => a.bpmnElementId === 'task_rechazo') + expect(rechazo).toBeDefined() + expect(rechazo!.executionProbability).toBeCloseTo(0.1, 1) + }) + + it('percentOfTotal suma ≈ 100%', () => { + const result = runSimulation(buildInput(xml)) + if (result.perActivity.length > 0) { + const sum = result.perActivity.reduce((acc, a) => acc + a.percentOfTotal, 0) + expect(sum).toBeCloseTo(100, 0) + } + }) +}) diff --git a/tests/lib/colors.test.ts b/tests/lib/colors.test.ts new file mode 100644 index 0000000..abd3ff5 --- /dev/null +++ b/tests/lib/colors.test.ts @@ -0,0 +1,213 @@ +import { describe, it, expect } from 'vitest' +import { + heatmapColorHex, + computeActivityT, + hasSignificantCostVariance, + activityColor, + NEUTRAL_HEATMAP_COLOR, + type ActivityCostData, +} from '@/lib/colors' + +// ─── Fixtures ───────────────────────────────────────────────────────────────── + +function makeActs(costs: number[]): ActivityCostData[] { + const total = costs.reduce((s, c) => s + c, 0) || 1 + return costs.map((c) => ({ + expectedTotalCost: Math.max(0, c), + percentOfTotal: (c / total) * 100, + })) +} + +// ─── heatmapColorHex ───────────────────────────────────────────────────────── +describe('heatmapColorHex', () => { + it('t=0 → verde (#10b981)', () => { + expect(heatmapColorHex(0)).toBe('#10b981') + }) + + it('t=1 → rojo (#ef4444)', () => { + expect(heatmapColorHex(1)).toBe('#ef4444') + }) + + it('t=0.5 → amarillo (#f59e0b)', () => { + expect(heatmapColorHex(0.5)).toBe('#f59e0b') + }) + + it('t<0 → clampea a t=0 (verde)', () => { + expect(heatmapColorHex(-1)).toBe(heatmapColorHex(0)) + }) + + it('t>1 → clampea a t=1 (rojo)', () => { + expect(heatmapColorHex(2)).toBe(heatmapColorHex(1)) + }) + + it('devuelve string hex válido en formato #rrggbb', () => { + for (const t of [0, 0.25, 0.5, 0.75, 1]) { + const hex = heatmapColorHex(t) + expect(hex).toMatch(/^#[0-9a-f]{6}$/) + } + }) + + it('colores intermedios son distintos entre sí', () => { + const colors = [0, 0.25, 0.5, 0.75, 1].map(heatmapColorHex) + const unique = new Set(colors) + expect(unique.size).toBe(5) + }) +}) + +// ─── hasSignificantCostVariance ────────────────────────────────────────────── +describe('hasSignificantCostVariance', () => { + it('varianza cero: todos iguales → false', () => { + expect(hasSignificantCostVariance(makeActs([100, 100, 100, 100, 100]))).toBe(false) + }) + + it('una sola actividad → false (no hay con qué comparar)', () => { + expect(hasSignificantCostVariance(makeActs([500]))).toBe(false) + }) + + it('lista vacía → false', () => { + expect(hasSignificantCostVariance([])).toBe(false) + }) + + it('varianza < 5%: costos casi iguales → false', () => { + // rango = (103-100)/103 ≈ 2.9% < 5% + expect(hasSignificantCostVariance(makeActs([100, 101, 102, 103]))).toBe(false) + }) + + it('varianza > 5%: costos claramente distintos → true', () => { + // rango = (200-100)/200 = 50% > 5% + expect(hasSignificantCostVariance(makeActs([100, 200]))).toBe(true) + }) + + it('varianza ≈ THRESHOLD (5%): está justo en el límite', () => { + // rango = (105-100)/105 ≈ 4.76% < 5% → false + expect(hasSignificantCostVariance(makeActs([100, 105]))).toBe(false) + // rango = (111-100)/111 ≈ 9.9% > 5% → true + expect(hasSignificantCostVariance(makeActs([100, 111]))).toBe(true) + }) + + it('no crashea con costos negativos (defensivo)', () => { + expect(() => hasSignificantCostVariance(makeActs([-10, -5, 0]))).not.toThrow() + }) + + it('todos en cero → false (max=0)', () => { + expect(hasSignificantCostVariance(makeActs([0, 0, 0]))).toBe(false) + }) +}) + +// ─── computeActivityT ──────────────────────────────────────────────────────── +describe('computeActivityT — modo relativo', () => { + it('actividad con mayor % → t = 1.0', () => { + const acts = makeActs([300, 100, 50]) + const t = computeActivityT(acts[0], acts, 'relative') + expect(t).toBeCloseTo(1.0) + }) + + it('actividad con menor costo → t < t de la más cara', () => { + const acts = makeActs([300, 100, 50]) + const tBarata = computeActivityT(acts[2], acts, 'relative') + const tCara = computeActivityT(acts[0], acts, 'relative') + expect(tBarata).toBeLessThan(tCara) + }) + + it('t siempre está en [0, 1]', () => { + const acts = makeActs([1000, 500, 100, 10]) + for (const act of acts) { + const t = computeActivityT(act, acts, 'relative') + expect(t).toBeGreaterThanOrEqual(0) + expect(t).toBeLessThanOrEqual(1) + } + }) + + it('lista vacía → t = 0 sin crash', () => { + const act: ActivityCostData = { expectedTotalCost: 100, percentOfTotal: 100 } + expect(computeActivityT(act, [], 'relative')).toBe(0) + }) +}) + +describe('computeActivityT — modo absoluto', () => { + it('actividad más cara → t = 1.0', () => { + const acts = makeActs([100, 200, 300]) + const t = computeActivityT(acts[2], acts, 'absolute') + expect(t).toBeCloseTo(1.0) + }) + + it('actividad más barata → t = 0.0', () => { + const acts = makeActs([100, 200, 300]) + const t = computeActivityT(acts[0], acts, 'absolute') + expect(t).toBeCloseTo(0.0) + }) + + it('actividad del medio → t ≈ 0.5', () => { + const acts = makeActs([100, 200, 300]) + const t = computeActivityT(acts[1], acts, 'absolute') + expect(t).toBeCloseTo(0.5) + }) + + it('un solo precio (max=min) → t = 0.5 (defensivo)', () => { + const acts = makeActs([100, 100]) + const t = computeActivityT(acts[0], acts, 'absolute') + expect(t).toBe(0.5) + }) +}) + +// ─── activityColor — función unificada ─────────────────────────────────────── +describe('activityColor', () => { + it('SIN varianza significativa → TODOS devuelven NEUTRAL_HEATMAP_COLOR', () => { + const acts = makeActs([100, 100, 100, 100, 100]) + for (const act of acts) { + expect(activityColor(act, acts, 'relative')).toBe(NEUTRAL_HEATMAP_COLOR) + expect(activityColor(act, acts, 'absolute')).toBe(NEUTRAL_HEATMAP_COLOR) + } + }) + + it('CON varianza real modo relativo: la más cara NO es neutra', () => { + const acts = makeActs([500, 100, 10]) + const color = activityColor(acts[0], acts, 'relative') + expect(color).not.toBe(NEUTRAL_HEATMAP_COLOR) + }) + + it('CON varianza real modo relativo: la más cara es roja (#ef4444)', () => { + const acts = makeActs([500, 100, 10]) + const colorCara = activityColor(acts[0], acts, 'relative') + expect(colorCara).toBe('#ef4444') + }) + + it('CON varianza real modo relativo: la más barata es verde (#10b981)', () => { + const acts = makeActs([500, 100, 10]) + const colorBarata = activityColor(acts[2], acts, 'relative') + // No necesariamente exactamente verde, pero sí del espectro inferior + // La más barata tiene percentOfTotal bajo / maxPercent bajo → t bajo → color verdoso + expect(colorBarata).not.toBe('#ef4444') + }) + + it('CON varianza real modo absoluto: la más cara es roja', () => { + const acts = makeActs([1000, 100, 10]) + const colorCara = activityColor(acts[0], acts, 'absolute') + expect(colorCara).toBe('#ef4444') + }) + + it('CON varianza real modo absoluto: la más barata es verde', () => { + const acts = makeActs([1000, 100, 10]) + const colorBarata = activityColor(acts[2], acts, 'absolute') + expect(colorBarata).toBe('#10b981') + }) + + it('edge: costos negativos no crashean', () => { + const acts = makeActs([0, 0, 0]) + expect(() => activityColor(acts[0], acts, 'relative')).not.toThrow() + }) + + it('simple-linear scenario: 5 actividades igual costo → todas neutras', () => { + // Este es el caso de use real de simple-linear.bpmn con costos fijos iguales + const acts = makeActs([100, 100, 100, 100, 100]) + const colors = acts.map((a) => activityColor(a, acts, 'relative')) + expect(colors.every((c) => c === NEUTRAL_HEATMAP_COLOR)).toBe(true) + }) + + it('NEUTRAL_HEATMAP_COLOR es un hex válido y claramente distinto del gradiente', () => { + expect(NEUTRAL_HEATMAP_COLOR).toMatch(/^#[0-9a-f]{6}$/) + expect(NEUTRAL_HEATMAP_COLOR).not.toBe('#10b981') // no es verde + expect(NEUTRAL_HEATMAP_COLOR).not.toBe('#f59e0b') // no es amarillo + expect(NEUTRAL_HEATMAP_COLOR).not.toBe('#ef4444') // no es rojo + }) +}) diff --git a/tests/lib/export/csv-export.test.ts b/tests/lib/export/csv-export.test.ts new file mode 100644 index 0000000..86a19a4 --- /dev/null +++ b/tests/lib/export/csv-export.test.ts @@ -0,0 +1,220 @@ +import { describe, it, expect } from 'vitest' +import { generateCsvContent } from '@/lib/export/csv-export' +import type { Process, Simulation, Resource, SimulationResult } from '@/domain/types' + +// ─── Fixtures ───────────────────────────────────────────────────────────────── + +function makeProcess(currency: string, extras: Partial = {}): Process { + return { + id: 'p1', name: 'Proceso de Ventas', clientName: 'Empresa ABC', + bpmnXml: '', currency, overheadPercentage: 0.2, + createdAt: 0, updatedAt: 0, ...extras, + } +} + +function makeSimulation(perActivity: SimulationResult['perActivity'] = []): Simulation { + const totalDirect = perActivity.reduce((s, a) => s + a.expectedDirectCost, 0) + const totalIndirect = perActivity.reduce((s, a) => s + a.expectedIndirectCost, 0) + return { + id: 'sim1', processId: 'p1', + executedAt: new Date('2026-05-13T10:00:00Z').getTime(), + result: { + totalCost: totalDirect + totalIndirect, + totalDirectCost: totalDirect, + totalIndirectCost: totalIndirect, + totalTimeMinutes: 90, + perActivity, + perResource: [], + warnings: [], + }, + } +} + +const mockActivities: SimulationResult['perActivity'] = [ + { + activityId: 'a1', bpmnElementId: 'task1', activityName: 'Recibir solicitud', + expectedDirectCost: 500, expectedIndirectCost: 100, expectedTotalCost: 600, + percentOfTotal: 60, executionProbability: 1.0, expectedExecutions: 1.0, + executionTimeMinutes: 30, resourceCostBreakdown: [], + }, + { + activityId: 'a2', bpmnElementId: 'task2', activityName: 'Análisis de crédito', + expectedDirectCost: 250, expectedIndirectCost: 50, expectedTotalCost: 300, + percentOfTotal: 30, executionProbability: 0.8, expectedExecutions: 0.8, + executionTimeMinutes: 60, resourceCostBreakdown: [], + }, + { + activityId: 'a3', bpmnElementId: 'task3', activityName: 'Notificación al cliente', + expectedDirectCost: 83.33, expectedIndirectCost: 16.67, expectedTotalCost: 100, + percentOfTotal: 10, executionProbability: 0.5, expectedExecutions: 0.5, + executionTimeMinutes: 15, resourceCostBreakdown: [], + }, +] + +const resources: Resource[] = [] + +// ─── BOM UTF-8 ──────────────────────────────────────────────────────────────── +describe('CSV — BOM UTF-8', () => { + it('comienza con BOM UTF-8 (\\uFEFF)', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources }) + expect(csv.charCodeAt(0)).toBe(0xFEFF) + }) + + it('BOM está presente para PYG también', () => { + const csv = generateCsvContent({ process: makeProcess('PYG'), simulation: makeSimulation(mockActivities), resources }) + expect(csv.charCodeAt(0)).toBe(0xFEFF) + }) +}) + +// ─── Separadores ────────────────────────────────────────────────────────────── +describe('CSV — separadores', () => { + it('USD: separador de columnas es coma ","', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources }) + // Los headers deben estar separados por coma + const headerLine = csv.split('\r\n').find((l) => l.includes('Actividad'))! + expect(headerLine).toContain(',') + expect(headerLine).not.toContain(';') + }) + + it('PYG: separador de columnas es punto y coma ";"', () => { + const csv = generateCsvContent({ process: makeProcess('PYG'), simulation: makeSimulation(mockActivities), resources }) + const headerLine = csv.split('\r\n').find((l) => l.includes('Actividad'))! + expect(headerLine).toContain(';') + expect(headerLine).not.toContain(',"Actividad"') + }) + + it('BRL: separador es punto y coma', () => { + const csv = generateCsvContent({ process: makeProcess('BRL'), simulation: makeSimulation(mockActivities), resources }) + const headerLine = csv.split('\r\n').find((l) => l.includes('Actividad'))! + expect(headerLine).toContain(';') + }) + + it('EUR: separador es punto y coma', () => { + const csv = generateCsvContent({ process: makeProcess('EUR'), simulation: makeSimulation(mockActivities), resources }) + const headerLine = csv.split('\r\n').find((l) => l.includes('Actividad'))! + expect(headerLine).toContain(';') + }) +}) + +// ─── Separadores decimales ───────────────────────────────────────────────────── +describe('CSV — separadores decimales', () => { + it('USD: decimal es punto "." → "500.00"', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources }) + expect(csv).toContain('500.00') + }) + + it('PYG: decimal es coma "," → "500,00"', () => { + const csv = generateCsvContent({ process: makeProcess('PYG'), simulation: makeSimulation(mockActivities), resources }) + expect(csv).toContain('500,00') + }) + + it('BRL: decimal es coma', () => { + const csv = generateCsvContent({ process: makeProcess('BRL'), simulation: makeSimulation(mockActivities), resources }) + expect(csv).toContain('500,00') + }) +}) + +// ─── Headers ────────────────────────────────────────────────────────────────── +describe('CSV — headers', () => { + it('contiene "Actividad" en los headers', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources }) + expect(csv).toContain('Actividad') + }) + + it('header de costo incluye la moneda: "Costo total (USD)"', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources }) + expect(csv).toContain('Costo total (USD)') + }) + + it('header de costo incluye la moneda: "Costo total (PYG)"', () => { + const csv = generateCsvContent({ process: makeProcess('PYG'), simulation: makeSimulation(mockActivities), resources }) + expect(csv).toContain('Costo total (PYG)') + }) + + it('contiene todos los headers requeridos', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources }) + expect(csv).toContain('ID BPMN') + expect(csv).toContain('Tiempo (min)') + expect(csv).toContain('% del total') + expect(csv).toContain('Ejecuciones esperadas') + expect(csv).toContain('Recursos asignados') + }) +}) + +// ─── Cantidad de filas ──────────────────────────────────────────────────────── +describe('CSV — cantidad de filas', () => { + it('tiene exactamente N filas de datos + header + metadata', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources }) + // Quitar BOM antes de procesar para no confundir el filtro de comentarios + const csvNoBom = csv.startsWith('') ? csv.slice(1) : csv + const lines = csvNoBom.split('\r\n').filter((l) => l.trim() && !l.startsWith('#')) + // 1 header + N actividades + expect(lines).toHaveLength(mockActivities.length + 1) + }) + + it('0 actividades → solo header', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation([]), resources }) + const csvNoBom = csv.startsWith('') ? csv.slice(1) : csv + const lines = csvNoBom.split('\r\n').filter((l) => l.trim() && !l.startsWith('#')) + expect(lines).toHaveLength(1) // solo el header + }) +}) + +// ─── Metadata de cabecera ───────────────────────────────────────────────────── +describe('CSV — metadata', () => { + it('la metadata comienza con "# Proceso:"', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources }) + expect(csv).toContain('# Proceso: Proceso de Ventas') + }) + + it('la metadata contiene el nombre del cliente', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources }) + expect(csv).toContain('# Cliente: Empresa ABC') + }) + + it('la metadata contiene el costo total', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources }) + expect(csv).toContain('# Costo total (USD)') + }) +}) + +// ─── Caracteres especiales / UTF-8 ─────────────────────────────────────────── +describe('CSV — UTF-8 y caracteres especiales', () => { + it('nombres con acentos se preservan correctamente', () => { + const activitiesWithAccents: SimulationResult['perActivity'] = [{ + activityId: 'a1', bpmnElementId: 't1', + activityName: 'Revisión y Análisis Crítico', + expectedDirectCost: 100, expectedIndirectCost: 20, expectedTotalCost: 120, + percentOfTotal: 100, executionProbability: 1, expectedExecutions: 1, + executionTimeMinutes: 30, resourceCostBreakdown: [], + }] + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(activitiesWithAccents), resources }) + expect(csv).toContain('Revisión y Análisis Crítico') + }) + + it('nombre del proceso con ñ se preserva', () => { + const process = makeProcess('USD', { name: 'Gestión de la Compañía' }) + const csv = generateCsvContent({ process, simulation: makeSimulation([]), resources }) + expect(csv).toContain('Gestión de la Compañía') + }) + + it('actividad con nombre en blanco no rompe el CSV', () => { + const activitiesBlank: SimulationResult['perActivity'] = [{ + activityId: 'a1', bpmnElementId: 't1', activityName: '', + expectedDirectCost: 0, expectedIndirectCost: 0, expectedTotalCost: 0, + percentOfTotal: 0, executionProbability: 1, expectedExecutions: 1, + executionTimeMinutes: 0, resourceCostBreakdown: [], + }] + expect(() => generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(activitiesBlank), resources })).not.toThrow() + }) +}) + +// ─── Recursos vacíos ────────────────────────────────────────────────────────── +describe('CSV — recursos vacíos', () => { + it('actividad sin recursos → celda de recursos vacía (no rompe)', () => { + const csv = generateCsvContent({ process: makeProcess('USD'), simulation: makeSimulation(mockActivities), resources: [] }) + // No debe crashear y el contenido es válido + expect(csv.length).toBeGreaterThan(0) + expect(csv).toContain('Recibir solicitud') + }) +}) diff --git a/tests/lib/export/measure-sizes.test.ts b/tests/lib/export/measure-sizes.test.ts new file mode 100644 index 0000000..3876db8 --- /dev/null +++ b/tests/lib/export/measure-sizes.test.ts @@ -0,0 +1,88 @@ +/** + * Medición de tamaños de CSV para los 3 sample BPMNs. + * Este test calcula el tamaño en bytes del CSV generado para cada BPMN. + */ +import { describe, it } from 'vitest' +import { readFileSync } from 'fs' +import { resolve } from 'path' +import { v4 as uuidv4 } from 'uuid' +import { parseBpmnXml, extractActivityElements, extractGatewayElements } from '@/domain/bpmn-parser' +import { runSimulation } from '@/domain/simulation' +import { generateCsvContent } from '@/lib/export/csv-export' +import { bpmnTypeToGatewayType } from '@/domain/types' +import type { Activity, GatewayConfig, SimulationInput, Process, Simulation } from '@/domain/types' + +function loadBpmn(name: string): string { + return readFileSync(resolve(__dirname, '../../../public/sample-processes', name), 'utf-8') +} + +function buildInput(xml: string, directCost: number): SimulationInput { + const graph = parseBpmnXml(xml) + const actEls = extractActivityElements(graph) + const gwEls = extractGatewayElements(graph) + const processId = 'measure-proc' + + const activities = new Map( + actEls.map((el) => [el.bpmnElementId, { + id: uuidv4(), processId, bpmnElementId: el.bpmnElementId, name: el.name, + type: el.type, directCostFixed: directCost, executionTimeMinutes: 30, assignedResources: [], + }]) + ) + + const gateways = new Map( + gwEls.map((el) => { + const gwType = bpmnTypeToGatewayType(el.gatewayType as any) + return [el.bpmnElementId, { + id: uuidv4(), processId, bpmnElementId: el.bpmnElementId, gatewayType: gwType, + branches: el.outgoing.map((flowId) => ({ + flowId, targetElementId: graph.flows.get(flowId)?.targetRef ?? '', + probability: gwType === 'parallel' ? 1.0 : 1 / el.outgoing.length, + })), + }] + }) + ) + + return { + processXml: xml, activities, gateways, resources: new Map(), + globalSettings: { currency: 'USD', overheadPercentage: 0.2 }, + } +} + +describe('Medición de tamaños de CSV — 3 sample BPMNs', () => { + const BPMNS = [ + { file: 'simple-linear.bpmn', name: 'Proceso Lineal Simple', client: 'Demo' }, + { file: 'medium-with-gateways.bpmn', name: 'Aprobación de Crédito', client: 'Banco ABC' }, + { file: 'complex-with-loop.bpmn', name: 'Control de Calidad', client: 'Empresa XYZ' }, + ] + + for (const bpmn of BPMNS) { + it(`${bpmn.file}: mide tamaño del CSV`, () => { + const xml = loadBpmn(bpmn.file) + const result = runSimulation(buildInput(xml, 1000)) + + const process: Process = { + id: 'p1', name: bpmn.name, clientName: bpmn.client, + bpmnXml: xml, currency: 'USD', overheadPercentage: 0.2, + createdAt: 0, updatedAt: 0, + } + + const simulation: Simulation = { + id: 's1', processId: 'p1', + executedAt: new Date('2026-05-13T12:00:00Z').getTime(), + result, + } + + const csv = generateCsvContent({ process, simulation, resources: [] }) + const bytes = new TextEncoder().encode(csv).length + + console.log(`\n📊 ${bpmn.file}`) + console.log(` Actividades: ${result.perActivity.length}`) + console.log(` CSV size: ${bytes} bytes (${(bytes / 1024).toFixed(1)} KB)`) + console.log(` CSV líneas de datos: ${result.perActivity.length} + 1 header + 7 meta`) + console.log(` Costo total: $${result.totalCost.toFixed(2)}`) + + // El CSV no debe ser vacío + expect(bytes).toBeGreaterThan(100) + }) + } +}) diff --git a/tests/lib/export/pdf-export.test.ts b/tests/lib/export/pdf-export.test.ts new file mode 100644 index 0000000..70bd9cf --- /dev/null +++ b/tests/lib/export/pdf-export.test.ts @@ -0,0 +1,166 @@ +/** + * Smoke tests del export PDF. + * jsPDF + autotable se mockean porque requieren canvas/DOM completo. + * Se verifica: llamadas al builder, metadata, nombre de archivo, no-throw. + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +// ─── Mock de jsPDF ──────────────────────────────────────────────────────────── +const mockDoc = { + setProperties: vi.fn(), + setFont: vi.fn(), + setFontSize: vi.fn(), + setTextColor: vi.fn(), + setFillColor: vi.fn(), + setDrawColor: vi.fn(), + setLineWidth: vi.fn(), + text: vi.fn(), + rect: vi.fn(), + roundedRect: vi.fn(), + line: vi.fn(), + addImage: vi.fn(), + addPage: vi.fn(), + setPage: vi.fn(), + save: vi.fn(), + splitTextToSize: vi.fn().mockImplementation((text: string) => [text]), + internal: { getNumberOfPages: vi.fn().mockReturnValue(3) }, +} + +// Arrow functions no pueden ser constructoras — usar function() regular +vi.mock('jspdf', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function MockJsPDF(this: any) { return mockDoc } + return { jsPDF: MockJsPDF } +}) + +vi.mock('jspdf-autotable', () => ({ + default: vi.fn(), +})) + +// ─── Fixtures ───────────────────────────────────────────────────────────────── + +import type { Process, Simulation } from '@/domain/types' + +const mockProcess: Process = { + id: 'p1', name: 'Proceso de Aprobación de Crédito', + clientName: 'Banco XYZ', + bpmnXml: '', currency: 'USD', + overheadPercentage: 0.2, + createdAt: 0, updatedAt: 0, +} + +const mockSimulation: Simulation = { + id: 'sim1', processId: 'p1', + executedAt: new Date('2026-05-13T10:00:00Z').getTime(), + result: { + totalCost: 1200, + totalDirectCost: 1000, + totalIndirectCost: 200, + totalTimeMinutes: 90, + perActivity: [ + { + activityId: 'a1', bpmnElementId: 'task1', activityName: 'Recibir solicitud', + expectedDirectCost: 600, expectedIndirectCost: 120, expectedTotalCost: 720, + percentOfTotal: 60, executionProbability: 1.0, expectedExecutions: 1.0, + executionTimeMinutes: 30, resourceCostBreakdown: [], + }, + { + activityId: 'a2', bpmnElementId: 'task2', activityName: 'Procesar', + expectedDirectCost: 400, expectedIndirectCost: 80, expectedTotalCost: 480, + percentOfTotal: 40, executionProbability: 0.8, expectedExecutions: 0.8, + executionTimeMinutes: 60, resourceCostBreakdown: [], + }, + ], + perResource: [], + warnings: [], + }, +} + +// ─── Tests ──────────────────────────────────────────────────────────────────── + +describe('exportToPdf — smoke tests', () => { + beforeEach(() => { vi.clearAllMocks() }) + + it('no lanza error con parámetros válidos y heatmap null', async () => { + const { exportToPdf } = await import('@/lib/export/pdf-export') + await expect(exportToPdf({ + process: mockProcess, + simulation: mockSimulation, + resources: [], + heatmapImageData: null, + })).resolves.not.toThrow() + }) + + it('llama a doc.addPage al menos una vez (estructura multi-página)', async () => { + const { exportToPdf } = await import('@/lib/export/pdf-export') + await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) + // El PDF tiene al menos 2 páginas (análisis + tabla) + expect(mockDoc.addPage).toHaveBeenCalled() + }) + + it('llama a setProperties con metadata correcta', async () => { + const { exportToPdf } = await import('@/lib/export/pdf-export') + await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) + expect(mockDoc.setProperties).toHaveBeenCalledWith(expect.objectContaining({ + title: expect.stringContaining('Proceso de Aprobación de Crédito'), + author: 'Banco XYZ', + subject: 'Simulación de proceso BPMN', + })) + }) + + it('llama a doc.save con nombre de archivo correcto', async () => { + const { exportToPdf } = await import('@/lib/export/pdf-export') + await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) + const saveCall = mockDoc.save.mock.calls[0][0] as string + expect(saveCall).toMatch(/proceso-de-aprobacion-de-credito/) + expect(saveCall).toMatch(/banco-xyz/) + expect(saveCall).toMatch(/\.pdf$/) + }) + + it('NO llama a addImage cuando heatmapImageData es null', async () => { + const { exportToPdf } = await import('@/lib/export/pdf-export') + await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) + expect(mockDoc.addImage).not.toHaveBeenCalled() + }) + + it('llama a addImage cuando heatmapImageData está disponible', async () => { + const { exportToPdf } = await import('@/lib/export/pdf-export') + const fakeDataUrl = 'data:image/jpeg;base64,/9j/fake' + await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: fakeDataUrl }) + expect(mockDoc.addImage).toHaveBeenCalledWith(fakeDataUrl, 'JPEG', expect.any(Number), expect.any(Number), expect.any(Number), expect.any(Number)) + }) + + it('llama a addPageNumbers (setPage para el footer)', async () => { + const { exportToPdf } = await import('@/lib/export/pdf-export') + await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) + // addPageNumbers itera setPage para cada página + expect(mockDoc.setPage).toHaveBeenCalled() + }) + + it('llama a autoTable con head que contiene la moneda', async () => { + const autoTable = (await import('jspdf-autotable')).default as ReturnType + const { exportToPdf } = await import('@/lib/export/pdf-export') + await exportToPdf({ process: mockProcess, simulation: mockSimulation, resources: [], heatmapImageData: null }) + expect(autoTable).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + head: expect.arrayContaining([ + expect.arrayContaining([ + expect.objectContaining({ content: expect.stringContaining('USD') }), + ]), + ]), + }) + ) + }) + + it('con warnings en el resultado: no lanza error', async () => { + const { exportToPdf } = await import('@/lib/export/pdf-export') + const simWithWarnings = { + ...mockSimulation, + result: { ...mockSimulation.result, warnings: ['Loop detectado en task1'] }, + } + await expect(exportToPdf({ + process: mockProcess, simulation: simWithWarnings, resources: [], heatmapImageData: null, + })).resolves.not.toThrow() + }) +}) diff --git a/tests/lib/export/slug.test.ts b/tests/lib/export/slug.test.ts new file mode 100644 index 0000000..4948311 --- /dev/null +++ b/tests/lib/export/slug.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from 'vitest' +import { slugify, buildFileName } from '@/lib/export/slug' + +describe('slugify', () => { + it('"Proceso de Compras" → "proceso-de-compras"', () => { + expect(slugify('Proceso de Compras')).toBe('proceso-de-compras') + }) + + it('"Análisis Año 2025" → "analisis-ano-2025"', () => { + expect(slugify('Análisis Año 2025')).toBe('analisis-ano-2025') + }) + + it('elimina la ñ → n', () => { + expect(slugify('Gestión de Compañía')).toBe('gestion-de-compania') + }) + + it('elimina acentos (á é í ó ú ü)', () => { + expect(slugify('Ú ltimo Análisis Crítico')).toBe('u-ltimo-analisis-critico') + }) + + it('caracteres especiales → guiones', () => { + expect(slugify('Proceso #1 / Sub-proceso (test)')).toBe('proceso-1-sub-proceso-test') + }) + + it('múltiples espacios → un solo guión', () => { + expect(slugify('Proceso de ventas')).toBe('proceso-de-ventas') + }) + + it('string vacío → string vacío', () => { + expect(slugify('')).toBe('') + }) + + it('solo números → se mantienen', () => { + expect(slugify('2025')).toBe('2025') + }) + + it('ya en minúsculas sin acentos → sin cambios relevantes', () => { + expect(slugify('proceso-lineal')).toBe('proceso-lineal') + }) + + it('limita a 60 caracteres', () => { + const long = 'a'.repeat(100) + expect(slugify(long)).toHaveLength(60) + }) +}) + +describe('buildFileName', () => { + // new Date(año, mes0, día) → fecha local sin ambigüedad de timezone + const date = new Date(2026, 4, 13) // 13 de mayo de 2026, local time + + it('genera filename con proceso + cliente + fecha', () => { + const name = buildFileName('Proceso de Ventas', 'Banco ABC', date, 'pdf') + expect(name).toBe('proceso-de-ventas_banco-abc_20260513.pdf') + }) + + it('omite cliente si está vacío', () => { + const name = buildFileName('Proceso de Ventas', '', date, 'csv') + expect(name).toBe('proceso-de-ventas_20260513.csv') + }) + + it('omite cliente si es undefined', () => { + const name = buildFileName('Proceso', undefined, date, 'pdf') + expect(name).toBe('proceso_20260513.pdf') + }) + + it('extensión csv correcta', () => { + const name = buildFileName('test', 'cliente', date, 'csv') + expect(name).toMatch(/\.csv$/) + }) + + it('extensión pdf correcta', () => { + const name = buildFileName('test', 'cliente', date, 'pdf') + expect(name).toMatch(/\.pdf$/) + }) + + it('nombre con caracteres especiales queda limpio (sin #, paréntesis)', () => { + const name = buildFileName('Análisis #1', 'Empresa S.A.', date, 'csv') + // Verificar solo el slug (sin la extensión que tiene un punto legítimo) + const slug = name.replace(/\.\w+$/, '') + expect(slug).not.toMatch(/[#()]/) + expect(name).toMatch(/analisis/) + }) +}) diff --git a/tests/lib/format.test.ts b/tests/lib/format.test.ts new file mode 100644 index 0000000..f73d203 --- /dev/null +++ b/tests/lib/format.test.ts @@ -0,0 +1,211 @@ +import { describe, it, expect } from 'vitest' +import { formatCurrency, formatPercent, formatMinutes, formatSimulationTimestamp } from '@/lib/format' + +// ─── formatCurrency ─────────────────────────────────────────────────────────── +describe('formatCurrency', () => { + // USD + it('USD: formatea 0 correctamente', () => { + const result = formatCurrency(0, 'USD') + expect(result).toMatch(/0/) + expect(result).toMatch(/\$|USD/) + }) + + it('USD: formatea 1 con 2 decimales', () => { + const result = formatCurrency(1, 'USD') + expect(result).toMatch(/1[.,]00/) + }) + + it('USD: formatea 1000 con separador de miles', () => { + const result = formatCurrency(1000, 'USD') + // Debe tener separador (coma o punto) antes de los tres ceros + expect(result).toMatch(/1[.,]000/) + }) + + it('USD: formatea 1234567 con separadores', () => { + const result = formatCurrency(1234567, 'USD') + expect(result).toMatch(/1/) + expect(result).toMatch(/234/) + expect(result).toMatch(/567/) + }) + + it('USD: formatea decimales correctamente', () => { + const result = formatCurrency(1234.56, 'USD') + expect(result).toMatch(/1[.,]234/) + expect(result).toMatch(/56/) + }) + + // PYG — el guaraní no tiene decimales centavos en la práctica + it('PYG: devuelve una cadena con el valor', () => { + const result = formatCurrency(1000000, 'PYG') + expect(result).toBeTruthy() + expect(typeof result).toBe('string') + expect(result.length).toBeGreaterThan(0) + }) + + it('PYG: 0 formatea sin crash', () => { + expect(() => formatCurrency(0, 'PYG')).not.toThrow() + }) + + it('PYG: número grande formatea sin crash', () => { + expect(() => formatCurrency(1_234_567_890, 'PYG')).not.toThrow() + const result = formatCurrency(1_234_567_890, 'PYG') + expect(result).toMatch(/1/) + }) + + // BRL + it('BRL: formatea con símbolo correcto', () => { + const result = formatCurrency(1000, 'BRL') + expect(result).toBeTruthy() + // El símbolo R$ puede aparecer o la abreviatura BRL + expect(result).toMatch(/1/) + }) + + // EUR + it('EUR: formatea 1234.56 sin crash', () => { + expect(() => formatCurrency(1234.56, 'EUR')).not.toThrow() + const result = formatCurrency(1234.56, 'EUR') + expect(result).toMatch(/1/) + }) + + // Negativos — el reporte no debe tener valores negativos, pero la función no debe crashear + it('No crashea con valores negativos', () => { + expect(() => formatCurrency(-100, 'USD')).not.toThrow() + const result = formatCurrency(-100, 'USD') + expect(result).toMatch(/-|100/) + }) + + // Múltiples monedas producen outputs distintos + it('USD y PYG producen outputs distintos para el mismo número', () => { + const usd = formatCurrency(1000, 'USD') + const pyg = formatCurrency(1000, 'PYG') + expect(usd).not.toBe(pyg) + }) +}) + +// ─── formatMinutes ──────────────────────────────────────────────────────────── +describe('formatMinutes', () => { + it('5 min → "5 min"', () => { + expect(formatMinutes(5)).toBe('5 min') + }) + + it('30 min → "30 min"', () => { + expect(formatMinutes(30)).toBe('30 min') + }) + + it('59 min → "59 min" (aún no llega a 1h)', () => { + expect(formatMinutes(59)).toBe('59 min') + }) + + it('60 min → "1h" (exactamente 1 hora)', () => { + expect(formatMinutes(60)).toBe('1h') + }) + + it('90 min → "1h 30min"', () => { + expect(formatMinutes(90)).toBe('1h 30min') + }) + + it('125 min → "2h 5min"', () => { + expect(formatMinutes(125)).toBe('2h 5min') + }) + + it('120 min → "2h" (horas exactas, sin fracción)', () => { + expect(formatMinutes(120)).toBe('2h') + }) + + it('1440 min → "24h" (un día completo)', () => { + expect(formatMinutes(1440)).toBe('24h') + }) + + it('0 min → "0 min"', () => { + expect(formatMinutes(0)).toBe('0 min') + }) + + it('No crashea con valores decimales', () => { + expect(() => formatMinutes(90.7)).not.toThrow() + }) +}) + +// ─── formatPercent ──────────────────────────────────────────────────────────── +// Nota: el locale es-PY usa coma como separador decimal → "23,4%" no "23.4%". +// Los tests verifican estructura (contiene el valor y el símbolo) sin asumir separador. +describe('formatPercent', () => { + it('23.4 → contiene "23" y "4" y "%" con 1 decimal', () => { + const result = formatPercent(23.4) + expect(result).toContain('23') + expect(result).toContain('4') + expect(result).toContain('%') + }) + + it('100 → contiene "100" y "%" con 1 decimal', () => { + const result = formatPercent(100) + expect(result).toContain('100') + expect(result).toContain('%') + }) + + it('0 → contiene "0" y "%" sin crash', () => { + const result = formatPercent(0) + expect(result).toContain('0') + expect(result).toContain('%') + }) + + it('el locale es-PY usa coma decimal: 23.4 → "23,4%"', () => { + // Documenta el comportamiento real del locale configurado + expect(formatPercent(23.4)).toBe('23,4%') + }) + + it('100% con 0 decimales → "100%"', () => { + expect(formatPercent(100, 0)).toBe('100%') + }) + + it('20 con 0 decimales → "20%"', () => { + expect(formatPercent(20, 0)).toBe('20%') + }) +}) + +// ─── formatSimulationTimestamp ──────────────────────────────────────────────── +describe('formatSimulationTimestamp', () => { + // Timestamp fijo para reproducibilidad: 13 de mayo de 2026, 09:45 UTC-4 (PY hora de verano) + // Usamos una fecha conocida y verificamos la estructura del output sin depender de TZ del CI. + const KNOWN_TS = new Date('2026-05-13T13:45:00Z').getTime() // UTC + + it('devuelve objeto con date y time strings no vacíos', () => { + const { date, time } = formatSimulationTimestamp(KNOWN_TS) + expect(date.length).toBeGreaterThan(0) + expect(time.length).toBeGreaterThan(0) + }) + + it('date contiene el año 2026', () => { + const { date } = formatSimulationTimestamp(KNOWN_TS) + expect(date).toContain('2026') + }) + + it('date contiene el número 13 (día)', () => { + const { date } = formatSimulationTimestamp(KNOWN_TS) + expect(date).toContain('13') + }) + + it('date contiene "mayo" (nombre del mes en es-PY)', () => { + const { date } = formatSimulationTimestamp(KNOWN_TS) + expect(date).toMatch(/mayo/i) + }) + + it('time tiene formato HH:mm (dos pares de dígitos separados por ":")', () => { + const { time } = formatSimulationTimestamp(KNOWN_TS) + expect(time).toMatch(/\d{1,2}:\d{2}/) + }) + + it('no crashea con timestamp 0 (epoch)', () => { + expect(() => formatSimulationTimestamp(0)).not.toThrow() + const { date, time } = formatSimulationTimestamp(0) + expect(date.length).toBeGreaterThan(0) + expect(time.length).toBeGreaterThan(0) + }) + + it('timestamps distintos producen salidas distintas', () => { + const ts1 = new Date('2026-01-01T10:00:00Z').getTime() + const ts2 = new Date('2026-06-15T18:30:00Z').getTime() + const r1 = formatSimulationTimestamp(ts1) + const r2 = formatSimulationTimestamp(ts2) + expect(r1.date).not.toBe(r2.date) + }) +}) diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..46807c6 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,8 @@ +// Parchear IndexedDB antes de que Dexie intente abrirla +import 'fake-indexeddb/auto' +import '@testing-library/jest-dom' + +// jsdom no implementa scrollIntoView — mock para que no rompa tests de tabla +if (typeof HTMLElement !== 'undefined') { + HTMLElement.prototype.scrollIntoView = () => {} +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..8d7c3f1 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "esnext", + "types": ["vite/client", "vitest/globals"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Strict */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Paths */ + "ignoreDeprecations": "6.0", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src", "tests"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..f372281 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,34 @@ +import path from 'path' +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + build: { + rollupOptions: { + output: { + manualChunks: (id: string) => { + if (id.includes('node_modules/bpmn-js') || id.includes('node_modules/diagram-js')) return 'vendor-bpmn' + if (id.includes('node_modules/echarts') || id.includes('node_modules/zrender')) return 'vendor-charts' + if (id.includes('node_modules/react') || id.includes('node_modules/react-dom')) return 'vendor-react' + if (id.includes('node_modules/@tanstack')) return 'vendor-router' + if (id.includes('node_modules/@radix-ui')) return 'vendor-ui' + }, + }, + }, + }, + optimizeDeps: { + include: ['bpmn-js'], + }, + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./tests/setup.ts'], + exclude: ['**/node_modules/**', '**/dist/**', 'tests/e2e/**'], + }, +})