diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 9c303644..4a38216e 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -88,7 +88,11 @@ RUN apt update && apt install -y \ && rm -rf /tmp/* /var/tmp/* \ && rm -rf /var/lib/apt/lists/* - +# Node.js & npm for frontend build +RUN apt-get update && \ + apt-get install -y curl && \ + curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \ + apt-get install -y nodejs WORKDIR /tmp diff --git a/.dockerignore b/.dockerignore index 9b91c7d3..f5fe77dd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,10 @@ resources/ !resources/api !resources/pipelines !resources/cache/aymurai -!resources/cache/tfhub_modules notebooks/ -*.egg-info/ \ No newline at end of file +*.egg-info/ + +frontend/node_modules/ +frontend/out/ +frontend-dist/ diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 319cd7f0..a180c827 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -69,6 +69,7 @@ jobs: - name: Run api tests env: DISKCACHE_ROOT: /tmp + SQLALCHEMY_DATABASE_URI: sqlite:////tmp/aymurai-test.db run: uv run --no-sync pytest -q --tb=short --disable-warnings --color=yes --maxfail=5 tests/api - name: Download pipelines data diff --git a/.gitignore b/.gitignore index 2dad2bac..feb0089a 100755 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,9 @@ aymurai/version.py notebooks/** !notebooks/**/ !notebooks/**/*.ipynb + +# Frontend build output +frontend-dist/ + +# pnpm local package store +.pnpm-store/ diff --git a/Makefile b/Makefile index 816c1c5b..898fc3ae 100644 --- a/Makefile +++ b/Makefile @@ -44,3 +44,19 @@ alembic-regenerate: cd aymurai && \ uv run alembic revision --autogenerate -m "Create database" && \ uv run alembic upgrade head + +# --- Frontend build --- +FRONTEND_DIR=frontend +FRONTEND_DIST_DIR=frontend-dist + +frontend-install: + cd $(FRONTEND_DIR) && pnpm install + +frontend-build: frontend-install + cd $(FRONTEND_DIR) && pnpm run build:web + # Copy build output to frontend-dist + rm -rf $(FRONTEND_DIST_DIR) + cp -r $(FRONTEND_DIR)/out/renderer $(FRONTEND_DIST_DIR) + +frontend-clean: + rm -rf $(FRONTEND_DIST_DIR) diff --git a/README.md b/README.md index fae091de..573196fe 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,13 @@ docker run -d --name aymurai-backend-gpu --gpus all \ Open Swagger UI: ```text -http://localhost:8899/docs +http://localhost:8899/api/docs +``` + +Open the bundled frontend: + +```text +http://localhost:8899/ ``` ## Quick Start (Docker Compose) @@ -80,6 +86,7 @@ make api-logs ## Runtime Overview - Framework: `FastAPI` - Default API port: `8899` +- Bundled frontend path: `GET /` - DB engine: `SQLModel` + Alembic migrations on startup - Default DB URI: `sqlite:////resources/cache/sqlite/database.db` - Production pipeline configs: @@ -87,16 +94,16 @@ make api-logs - `resources/pipelines/production/datapublic/pipeline.json` ## Main Public Endpoints -- `GET /server/healthcheck` -- `GET /server/stats/summary` -- `POST /misc/document-extract` (and deprecated alias `POST /document-extract`) -- `POST /anonymizer/predict` -- `POST /anonymizer/disambiguate` -- `POST /anonymizer/validation` -- `POST /anonymizer/anonymize-document` -- `POST /datapublic/predict/{document_id}` -- `GET /datapublic/validation/document/{document_id}` -- `POST /datapublic/validation/document/{document_id}` +- `GET /api/server/healthcheck` +- `GET /api/server/stats/summary` +- `POST /api/misc/document-extract` (and deprecated alias `POST /api/document-extract`) +- `POST /api/anonymizer/predict` +- `POST /api/anonymizer/disambiguate` +- `POST /api/anonymizer/validation` +- `POST /api/anonymizer/anonymize-document` +- `POST /api/datapublic/predict/{document_id}` +- `GET /api/datapublic/validation/document/{document_id}` +- `POST /api/datapublic/validation/document/{document_id}` For full request/response contracts and examples, see [docs/api/README.md](docs/api/README.md). diff --git a/aymurai/api/core.py b/aymurai/api/core.py index c9223455..185dba4f 100644 --- a/aymurai/api/core.py +++ b/aymurai/api/core.py @@ -1,9 +1,7 @@ from fastapi.routing import APIRouter from .endpoints.routers.anonymizer import anonymizer -from .endpoints.routers.anonymizer import database as anonymizer_database from .endpoints.routers.datapublic import datapublic - from .endpoints.routers.misc import convert, document_extract from .endpoints.routers.server import stats @@ -23,11 +21,6 @@ prefix="/anonymizer", tags=["anonymization/model"], ) -# router.include_router( -# anonymizer_database.router, -# prefix="/anonymizer/database", -# tags=["anonymization/database"], -# ) # Datapublic router.include_router( diff --git a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py index 6fde1db7..1be1a121 100644 --- a/aymurai/api/endpoints/routers/anonymizer/anonymizer.py +++ b/aymurai/api/endpoints/routers/anonymizer/anonymizer.py @@ -5,7 +5,6 @@ from collections.abc import Iterable from threading import Lock -import torch from fastapi import Body, Depends, Form, HTTPException, Query, UploadFile from fastapi.responses import FileResponse from fastapi.routing import APIRouter @@ -31,7 +30,7 @@ RenderPolicy, TextRequest, ) -from aymurai.settings import settings +from aymurai.settings import DEFAULT_RENDER_POLICY, settings from aymurai.text.anonymization import ( InvalidDocumentAnonymizer, get_anonymizer, @@ -48,7 +47,6 @@ RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH -torch.set_num_threads = 100 # FIXME: polemic ? pipeline_lock = Lock() @@ -198,10 +196,7 @@ def _merge_render_policy( Returns: RenderPolicy: Effective render policy. """ - policy = RenderPolicy( - suffix_mode="auto", - suffix_threshold=1, - ) + policy = RenderPolicy.model_validate(DEFAULT_RENDER_POLICY) def apply(incoming: RenderPolicy) -> None: nonlocal policy diff --git a/aymurai/api/endpoints/routers/datapublic/datapublic.py b/aymurai/api/endpoints/routers/datapublic/datapublic.py index 91e1f0d5..4b531390 100644 --- a/aymurai/api/endpoints/routers/datapublic/datapublic.py +++ b/aymurai/api/endpoints/routers/datapublic/datapublic.py @@ -1,7 +1,6 @@ import os from threading import Lock -import torch from fastapi import Body, Depends, HTTPException, Query from fastapi.routing import APIRouter from pydantic import UUID5 @@ -28,7 +27,6 @@ RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH -torch.set_num_threads = 100 # FIXME: polemic ? pipeline_lock = Lock() diff --git a/aymurai/api/endpoints/routers/frontend/__init__.py b/aymurai/api/endpoints/routers/frontend/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/aymurai/api/endpoints/routers/frontend/__init__.py @@ -0,0 +1 @@ + diff --git a/aymurai/api/endpoints/routers/frontend/frontend.py b/aymurai/api/endpoints/routers/frontend/frontend.py new file mode 100644 index 00000000..22ceaf41 --- /dev/null +++ b/aymurai/api/endpoints/routers/frontend/frontend.py @@ -0,0 +1,114 @@ +from pathlib import Path + +from fastapi import APIRouter, HTTPException +from fastapi.responses import FileResponse, RedirectResponse, Response + +from aymurai.settings import settings + +router = APIRouter(include_in_schema=False) + +LEGACY_API_ROUTE_PREFIXES = ( + "anonymizer", + "api", + "convert", + "datapublic", + "database", + "document-extract", + "docs", + "misc", + "openapi.json", + "redoc", + "server", +) +FEATURE_ROUTE_SLUGS = ("anonymizer", "data_set") +FEATURE_ROUTE_STEPS = ("", "finish", "onboarding", "preview", "process", "validation") +LEGACY_FEATURE_ROUTE_SLUGS = { + "ANONYMIZER": "anonymizer", + "DATA_SET": "data_set", +} + + +def _frontend_dist_dir() -> Path: + return Path(settings.FRONTEND_DIST_DIR).expanduser() + + +def _split_asset_path(asset_path: str) -> list[str]: + return [segment for segment in asset_path.lstrip("/").split("/") if segment] + + +def _is_frontend_feature_route(asset_path: str) -> bool: + segments = _split_asset_path(asset_path) + if not segments or segments[0] not in FEATURE_ROUTE_SLUGS: + return False + + return len(segments) == 1 or segments[1] in FEATURE_ROUTE_STEPS + + +def _is_legacy_api_route(asset_path: str) -> bool: + first_segment = asset_path.lstrip("/").split("/", maxsplit=1)[0] + if _is_frontend_feature_route(asset_path): + return False + return first_segment in LEGACY_API_ROUTE_PREFIXES + + +def _canonical_frontend_path(asset_path: str) -> str | None: + segments = _split_asset_path(asset_path) + if not segments: + return None + + if segments[0] == "app": + canonical_segments = segments[1:] + if canonical_segments: + canonical_feature = LEGACY_FEATURE_ROUTE_SLUGS.get(canonical_segments[0]) + if canonical_feature: + canonical_segments = [canonical_feature, *canonical_segments[1:]] + return "/" + "/".join(canonical_segments) + + canonical_feature = LEGACY_FEATURE_ROUTE_SLUGS.get(segments[0]) + if canonical_feature: + return "/" + "/".join([canonical_feature, *segments[1:]]) + + return None + + +def _resolve_frontend_path(asset_path: str) -> Path: + dist_dir = _frontend_dist_dir().resolve() + requested_path = (dist_dir / asset_path.lstrip("/")).resolve() + requested_path.relative_to(dist_dir) + return requested_path + + +def _serve_frontend(asset_path: str = "") -> Response: + canonical_path = _canonical_frontend_path(asset_path) + if canonical_path is not None: + return RedirectResponse(canonical_path or "/") + + if asset_path and _is_legacy_api_route(asset_path): + raise HTTPException(status_code=404, detail="API route not found.") + + dist_dir = _frontend_dist_dir() + index_file = dist_dir / "index.html" + if not dist_dir.is_dir() or not index_file.is_file(): + raise HTTPException(status_code=404, detail="Frontend build not found.") + + if asset_path: + try: + requested_path = _resolve_frontend_path(asset_path) + except ValueError as error: + raise HTTPException( + status_code=404, detail="Frontend asset not found." + ) from error + + if requested_path.is_file(): + return FileResponse(requested_path) + + if Path(asset_path).suffix: + raise HTTPException(status_code=404, detail="Frontend asset not found.") + + return FileResponse(index_file) + + +@router.get("/") +@router.get("/{asset_path:path}") +async def frontend_app(asset_path: str = "") -> Response: + return _serve_frontend(asset_path) diff --git a/aymurai/api/main.py b/aymurai/api/main.py index 4bfd9370..27ab0c70 100644 --- a/aymurai/api/main.py +++ b/aymurai/api/main.py @@ -2,19 +2,19 @@ import time from contextlib import asynccontextmanager -import torch from alembic import command from alembic.config import Config from fastapi import FastAPI, Request -from fastapi.responses import RedirectResponse from fastapi.middleware.cors import CORSMiddleware from starlette.formparsers import MultiPartParser from aymurai.api import core +from aymurai.api.endpoints.routers.frontend import frontend +from aymurai.api.startup.database import check_db_connection +from aymurai.api.utils import configure_torch_threads from aymurai.logger import get_logger -from aymurai.settings import settings from aymurai.pipeline import AymurAIPipeline -from aymurai.api.startup.database import check_db_connection +from aymurai.settings import settings try: from aymurai.version import __version__ @@ -24,7 +24,7 @@ logger = get_logger(__name__) -torch.set_num_threads = 100 # FIXME: polemic ? +configure_torch_threads() RESOURCES_BASEPATH = settings.RESOURCES_BASEPATH @@ -54,8 +54,9 @@ async def lifespan(app: FastAPI): logger.info(">> Running Alembic migrations") alembic_cfg = Config(str(settings.ALEMBIC_INI_PATH)) command.upgrade(alembic_cfg, "head") - except Exception as error: - logger.error("Error while starting up:", error) + except Exception: + logger.exception("Error while starting up") + raise yield @@ -64,6 +65,9 @@ async def lifespan(app: FastAPI): title="AymurAI API", version=__version__, lifespan=lifespan, + docs_url="/api/docs", + redoc_url="/api/redoc", + openapi_url="/api/openapi.json", ) @@ -90,33 +94,20 @@ async def add_process_time_header(request: Request, call_next): return response -@api.get("/", response_class=RedirectResponse, include_in_schema=False) -async def index(): - return "/docs" - - -# @api.get("/docs", include_in_schema=False) -# async def custom_swagger_ui_html(): -# return get_swagger_ui_html( -# openapi_url=api.openapi_url, -# title=f"{api.title} - Swagger UI", -# swagger_css_url="https://cdn.jsdelivr.net/gh/danielperezrubio/swagger-dark-theme@main/assets/swagger-ui.min.css", -# ) - - ################################################################################ # MARK: API ENDPOINTS ################################################################################ # Healthcheck -@api.get("/server/healthcheck", status_code=200, tags=["server"]) +@api.get("/api/server/healthcheck", status_code=200, tags=["server"]) def healthcheck(): return {"status": "ok"} # Api endpoints -api.include_router(core.router) +api.include_router(core.router, prefix="/api") +api.include_router(frontend.router) if __name__ == "__main__": diff --git a/aymurai/api/utils.py b/aymurai/api/utils.py index 0132eaac..1944c9d7 100644 --- a/aymurai/api/utils.py +++ b/aymurai/api/utils.py @@ -1,4 +1,5 @@ import cachetools +import torch from aymurai.logger import get_logger from aymurai.settings import settings @@ -9,7 +10,7 @@ mem_cache = cachetools.TTLCache( maxsize=settings.MEMORY_CACHE_MAXSIZE, - ttl=settings.MEMORY_CACHE_MAXSIZE, + ttl=settings.MEMORY_CACHE_TTL, getsizeof=lambda _: 0, ) @@ -19,3 +20,7 @@ @cachetools.cached(cache=mem_cache) def load_pipeline(path: str): return AymurAIPipeline.load(path) + + +def configure_torch_threads() -> None: + torch.set_num_threads(settings.TORCH_NUM_THREADS) diff --git a/aymurai/config/__init__.py b/aymurai/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aymurai/config/default_disambiguation_label_policies.yml b/aymurai/config/default_disambiguation_label_policies.yml new file mode 100644 index 00000000..4de95df8 --- /dev/null +++ b/aymurai/config/default_disambiguation_label_policies.yml @@ -0,0 +1,124 @@ +PER: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: true + +NUM_EXPEDIENTE: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +CUIJ: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +NUM_ACTUACION: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +DNI: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +LINK: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +TELEFONO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +FECHA: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +DIRECCION: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +TEXTO_ANONIMIZAR: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +LOC: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +CORREO_ELECTRONICO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +CUIT_CUIL: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +NACIONALIDAD: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +EDAD: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +NOMBRE_ARCHIVO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +BANCO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +NUM_MATRICULA: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +ESTUDIOS: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +IP: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +USUARIX: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +CBU: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +MARCA_AUTOMOVIL: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +PATENTE_DOMINIO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false + +NUM_CAJA_AHORRO: + disambiguation: fuzzy + anonymize: true + use_subclass_when_available: false diff --git a/aymurai/settings.py b/aymurai/settings.py index 2079f561..259d03ed 100644 --- a/aymurai/settings.py +++ b/aymurai/settings.py @@ -1,5 +1,6 @@ import json import os +from copy import deepcopy from pathlib import Path from dotenv import load_dotenv @@ -7,10 +8,18 @@ from pydantic_settings import BaseSettings import aymurai +from aymurai.utils.yaml_data import load_yaml PARENT = Path(aymurai.__file__).parent +DEFAULT_DISAMBIGUATION_LABEL_POLICIES = load_yaml( + str(PARENT / "config" / "default_disambiguation_label_policies.yml") +) + +DEFAULT_RENDER_POLICY = {"suffix_mode": "auto", "suffix_threshold": 1} + + def load_env(): load_dotenv(".env") @@ -63,6 +72,7 @@ def assemble_cors_origins(cls, v) -> list[str]: # Cachetools settings MEMORY_CACHE_MAXSIZE: int = 1 MEMORY_CACHE_TTL: int = 60 + TORCH_NUM_THREADS: int = 4 LIBREOFFICE_BIN: str = "libreoffice" PDF_WATERMARK_FONT_REGULAR: str | None = None @@ -76,29 +86,29 @@ def assemble_cors_origins(cls, v) -> list[str]: THRESHOLD: int = 70 # Label policies (JSON dict: label -> {disambiguation, anonymize}) - DISAMBIGUATION_LABEL_POLICIES: dict | None = None - - @field_validator("DISAMBIGUATION_LABEL_POLICIES", mode="before") - @classmethod - def parse_label_policies(cls, v): - if v is None or v == "": - return None - if isinstance(v, str): - return json.loads(v) - return v + DISAMBIGUATION_LABEL_POLICIES: dict | None = Field( + default_factory=lambda: deepcopy(DEFAULT_DISAMBIGUATION_LABEL_POLICIES) + ) # Render policy (JSON dict) - RENDER_POLICY: dict | None = None + RENDER_POLICY: dict | None = Field( + default_factory=lambda: deepcopy(DEFAULT_RENDER_POLICY) + ) - @field_validator("RENDER_POLICY", mode="before") + @field_validator("DISAMBIGUATION_LABEL_POLICIES", "RENDER_POLICY", mode="before") @classmethod - def parse_render_policy(cls, v): + def parse_policies(cls, v): if v is None or v == "": return None if isinstance(v, str): return json.loads(v) return v + FRONTEND_DIST_DIR: str = Field( + default="frontend-dist", + validation_alias=AliasChoices("AYMURAI_FRONTEND_DIST_DIR", "FRONTEND_DIST_DIR"), + ) + load_env() settings = Settings() diff --git a/aymurai/utils/json_encoding.py b/aymurai/utils/json_encoding.py index 5ff367a9..b5554812 100644 --- a/aymurai/utils/json_encoding.py +++ b/aymurai/utils/json_encoding.py @@ -5,6 +5,7 @@ import datetime from typing import Any +import numpy as np import pandas as pd from aymurai.logger import get_logger @@ -60,6 +61,10 @@ def default(self, obj: Any) -> Any: str(obj), ], } + elif isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) elif pd.isna(obj): return "null" else: diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index bd492f09..9d1b4a0d 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -1,3 +1,24 @@ +# syntax=docker/dockerfile:1.7 + +# ------------------- +# Frontend Build Stage +# ------------------- +FROM node:lts-bookworm AS frontend-builder + +WORKDIR /frontend + +RUN npm install --global corepack@latest && \ + corepack enable pnpm && \ + corepack prepare pnpm@latest --activate + +COPY frontend/ ./ + +RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile + +RUN pnpm run build:web + + # ------------------- # Build Stage # ------------------- @@ -74,10 +95,13 @@ COPY --from=builder --chown=app:app /app/.venv /app/.venv # Copy static resources COPY resources/pipelines /resources/pipelines +COPY --from=frontend-builder /frontend/out/renderer /app/frontend-dist # Set working directory WORKDIR /app +ENV FRONTEND_DIST_DIR=/app/frontend-dist + # Define start command CMD uv run fastapi run --port 8899 .venv/lib/python3.10/site-packages/aymurai/api/main.py diff --git a/docs/api/README.md b/docs/api/README.md index 212e215e..4b3957c5 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -5,30 +5,30 @@ This document describes the currently mounted public API in `aymurai/api/main.py ## Base URL and OpenAPI - Local base URL: `http://localhost:8899` -- Swagger UI: `http://localhost:8899/docs` -- OpenAPI JSON: `http://localhost:8899/openapi.json` +- Swagger UI: `http://localhost:8899/api/docs` +- OpenAPI JSON: `http://localhost:8899/api/openapi.json` ## Public Endpoints (Mounted) | Method | Path | Purpose | |---|---|---| -| `GET` | `/server/healthcheck` | Service liveness check | -| `GET` | `/server/stats/summary` | Runtime CPU/memory stats | -| `POST` | `/document-extract` | Deprecated alias of `/misc/document-extract` | -| `POST` | `/misc/document-extract` | Extract normalized paragraphs from uploaded document | -| `POST` | `/anonymizer/predict` | NER prediction for a paragraph | -| `POST` | `/anonymizer/disambiguate` | Canonical entity disambiguation + policy merge | -| `POST` | `/anonymizer/validation` | Fetch paragraph-level manual validation | -| `POST` | `/anonymizer/anonymize-document` | Compile and export anonymized document | -| `POST` | `/datapublic/predict/{document_id}` | Predict entities for data-public flow | -| `GET` | `/datapublic/validation/document/{document_id}` | Read document-level validation | -| `POST` | `/datapublic/validation/document/{document_id}` | Save document-level validation | -| `POST` | `/convert/pdf/odt` | Convert PDF to ODT | -| `POST` | `/convert/pdf/docx` | Convert PDF to DOCX | -| `POST` | `/convert/docx/odt` | Convert DOCX to ODT | -| `POST` | `/convert/docx/pdf` | Convert DOCX to PDF | -| `POST` | `/convert/odt/pdf` | Convert ODT to PDF | -| `POST` | `/convert/odt/docx` | Convert ODT to DOCX | +| `GET` | `/api/server/healthcheck` | Service liveness check | +| `GET` | `/api/server/stats/summary` | Runtime CPU/memory stats | +| `POST` | `/api/document-extract` | Deprecated alias of `/api/misc/document-extract` | +| `POST` | `/api/misc/document-extract` | Extract normalized paragraphs from uploaded document | +| `POST` | `/api/anonymizer/predict` | NER prediction for a paragraph | +| `POST` | `/api/anonymizer/disambiguate` | Canonical entity disambiguation + policy merge | +| `POST` | `/api/anonymizer/validation` | Fetch paragraph-level manual validation | +| `POST` | `/api/anonymizer/anonymize-document` | Compile and export anonymized document | +| `POST` | `/api/datapublic/predict/{document_id}` | Predict entities for data-public flow | +| `GET` | `/api/datapublic/validation/document/{document_id}` | Read document-level validation | +| `POST` | `/api/datapublic/validation/document/{document_id}` | Save document-level validation | +| `POST` | `/api/convert/pdf/odt` | Convert PDF to ODT | +| `POST` | `/api/convert/pdf/docx` | Convert PDF to DOCX | +| `POST` | `/api/convert/docx/odt` | Convert DOCX to ODT | +| `POST` | `/api/convert/docx/pdf` | Convert DOCX to PDF | +| `POST` | `/api/convert/odt/pdf` | Convert ODT to PDF | +| `POST` | `/api/convert/odt/docx` | Convert ODT to DOCX | ## Core Data Contracts @@ -136,7 +136,7 @@ Note: the JSON snippets below are minimal valid examples. Real payloads may incl ``` ```bash -curl -s http://localhost:8899/server/healthcheck +curl -s http://localhost:8899/api/server/healthcheck ``` #### `GET /server/stats/summary` @@ -153,7 +153,7 @@ curl -s http://localhost:8899/server/healthcheck ``` ```bash -curl -s http://localhost:8899/server/stats/summary +curl -s http://localhost:8899/api/server/stats/summary ``` ### Document Extraction @@ -174,7 +174,7 @@ curl -s http://localhost:8899/server/stats/summary ```bash curl -s -X POST \ -F "file=@/resources/data/sample/document-01.docx" \ - http://localhost:8899/misc/document-extract + http://localhost:8899/api/misc/document-extract ``` Common errors: @@ -189,7 +189,7 @@ Common errors: - Response `200`: `DocumentInformation` ```bash -curl -s -X POST "http://localhost:8899/anonymizer/predict?use_cache=true" \ +curl -s -X POST "http://localhost:8899/api/anonymizer/predict?use_cache=true" \ -H "Content-Type: application/json" \ -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' ``` @@ -218,7 +218,7 @@ curl -s -X POST "http://localhost:8899/anonymizer/predict?use_cache=true" \ - Response `200`: `DocumentAnnotations` (with `data` and effective `label_policies`) ```bash -curl -s -X POST http://localhost:8899/anonymizer/disambiguate \ +curl -s -X POST http://localhost:8899/api/anonymizer/disambiguate \ -H "Content-Type: application/json" \ -d '{"paragraphs":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}}}' ``` @@ -228,7 +228,7 @@ curl -s -X POST http://localhost:8899/anonymizer/disambiguate \ - Response `200`: `list[DocLabel] | null` ```bash -curl -s -X POST http://localhost:8899/anonymizer/validation \ +curl -s -X POST http://localhost:8899/api/anonymizer/validation \ -H "Content-Type: application/json" \ -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' ``` @@ -240,7 +240,7 @@ curl -s -X POST http://localhost:8899/anonymizer/validation \ - Response `200`: binary anonymized `.odt` file ```bash -curl -X POST http://localhost:8899/anonymizer/anonymize-document \ +curl -X POST http://localhost:8899/api/anonymizer/anonymize-document \ -F "file=@/resources/data/sample/document-01.docx" \ -F 'annotations={"data":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}},"render_policy":{"suffix_mode":"auto","suffix_threshold":1}}' ``` @@ -258,7 +258,7 @@ Common errors: - Response `200`: `DocumentInformation` ```bash -curl -s -X POST "http://localhost:8899/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \ +curl -s -X POST "http://localhost:8899/api/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \ -H "Content-Type: application/json" \ -d '{"text":"Buenos Aires, 17 de noviembre de 2024"}' ``` @@ -268,7 +268,7 @@ curl -s -X POST "http://localhost:8899/datapublic/predict/7e6b6f35-2f29-58f7-9f8 - Response `404`: document not found ```bash -curl -s http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc +curl -s http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc ``` #### `POST /datapublic/validation/document/{document_id}` @@ -276,7 +276,7 @@ curl -s http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7- - Response `200`: empty body ```bash -curl -s -X POST http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \ +curl -s -X POST http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \ -H "Content-Type: application/json" \ -d '{"materia":"penal","violencia_de_genero":"si"}' ``` @@ -287,12 +287,12 @@ All conversion endpoints use `multipart/form-data` with a `file` field. | Method | Path | Input | Output | |---|---|---|---| -| `POST` | `/convert/pdf/odt` | `.pdf` | `.odt` | -| `POST` | `/convert/pdf/docx` | `.pdf` | `.docx` | -| `POST` | `/convert/docx/odt` | `.docx` | `.odt` | -| `POST` | `/convert/docx/pdf` | `.docx` | `.pdf` | -| `POST` | `/convert/odt/pdf` | `.odt` | `.pdf` | -| `POST` | `/convert/odt/docx` | `.odt` | `.docx` | +| `POST` | `/api/convert/pdf/odt` | `.pdf` | `.odt` | +| `POST` | `/api/convert/pdf/docx` | `.pdf` | `.docx` | +| `POST` | `/api/convert/docx/odt` | `.docx` | `.odt` | +| `POST` | `/api/convert/docx/pdf` | `.docx` | `.pdf` | +| `POST` | `/api/convert/odt/pdf` | `.odt` | `.pdf` | +| `POST` | `/api/convert/odt/docx` | `.odt` | `.docx` | For PDF input endpoints, optional query param: - `backend=libreoffice|pandoc` (default: `libreoffice`) @@ -312,9 +312,9 @@ Common errors: The following route modules exist in code but are not included in `core.router` at runtime: - `aymurai/api/endpoints/routers/datapublic/dataset.py` - - includes `/datapublic/dataset/*` CRUD/batch routes, but router is not mounted. + - includes `/api/datapublic/dataset/*` CRUD/batch routes, but router is not mounted. - `aymurai/api/endpoints/routers/anonymizer/database.py` - - `/anonymizer/database/*` routes exist, include is commented out. + - `/api/anonymizer/database/*` routes exist, include is commented out. - `aymurai/api/endpoints/routers/database/*` - additional DB admin routes exist, but no mounting in `core.router`. diff --git a/docs/es/api/README.md b/docs/es/api/README.md index adb3db4e..d51ec974 100644 --- a/docs/es/api/README.md +++ b/docs/es/api/README.md @@ -5,30 +5,30 @@ Este documento describe la API pública actualmente montada en `aymurai/api/main ## Base URL y OpenAPI - Base local: `http://localhost:8899` -- Swagger UI: `http://localhost:8899/docs` -- OpenAPI JSON: `http://localhost:8899/openapi.json` +- Swagger UI: `http://localhost:8899/api/docs` +- OpenAPI JSON: `http://localhost:8899/api/openapi.json` ## Endpoints públicos (montados) | Método | Path | Propósito | |---|---|---| -| `GET` | `/server/healthcheck` | Liveness del servicio | -| `GET` | `/server/stats/summary` | Métricas de CPU/memoria | -| `POST` | `/document-extract` | Alias deprecado de `/misc/document-extract` | -| `POST` | `/misc/document-extract` | Extrae párrafos normalizados de un documento | -| `POST` | `/anonymizer/predict` | Predicción NER por párrafo | -| `POST` | `/anonymizer/disambiguate` | Desambiguación canónica + merge de políticas | -| `POST` | `/anonymizer/validation` | Obtiene validación manual por párrafo | -| `POST` | `/anonymizer/anonymize-document` | Compila y exporta documento anonimizado | -| `POST` | `/datapublic/predict/{document_id}` | Predicción para flujo data-public | -| `GET` | `/datapublic/validation/document/{document_id}` | Lee validación a nivel documento | -| `POST` | `/datapublic/validation/document/{document_id}` | Guarda validación a nivel documento | -| `POST` | `/convert/pdf/odt` | Convierte PDF a ODT | -| `POST` | `/convert/pdf/docx` | Convierte PDF a DOCX | -| `POST` | `/convert/docx/odt` | Convierte DOCX a ODT | -| `POST` | `/convert/docx/pdf` | Convierte DOCX a PDF | -| `POST` | `/convert/odt/pdf` | Convierte ODT a PDF | -| `POST` | `/convert/odt/docx` | Convierte ODT a DOCX | +| `GET` | `/api/server/healthcheck` | Liveness del servicio | +| `GET` | `/api/server/stats/summary` | Métricas de CPU/memoria | +| `POST` | `/api/document-extract` | Alias deprecado de `/api/misc/document-extract` | +| `POST` | `/api/misc/document-extract` | Extrae párrafos normalizados de un documento | +| `POST` | `/api/anonymizer/predict` | Predicción NER por párrafo | +| `POST` | `/api/anonymizer/disambiguate` | Desambiguación canónica + merge de políticas | +| `POST` | `/api/anonymizer/validation` | Obtiene validación manual por párrafo | +| `POST` | `/api/anonymizer/anonymize-document` | Compila y exporta documento anonimizado | +| `POST` | `/api/datapublic/predict/{document_id}` | Predicción para flujo data-public | +| `GET` | `/api/datapublic/validation/document/{document_id}` | Lee validación a nivel documento | +| `POST` | `/api/datapublic/validation/document/{document_id}` | Guarda validación a nivel documento | +| `POST` | `/api/convert/pdf/odt` | Convierte PDF a ODT | +| `POST` | `/api/convert/pdf/docx` | Convierte PDF a DOCX | +| `POST` | `/api/convert/docx/odt` | Convierte DOCX a ODT | +| `POST` | `/api/convert/docx/pdf` | Convierte DOCX a PDF | +| `POST` | `/api/convert/odt/pdf` | Convierte ODT a PDF | +| `POST` | `/api/convert/odt/docx` | Convierte ODT a DOCX | ## Contratos de datos principales @@ -136,7 +136,7 @@ Nota: los snippets JSON de abajo son ejemplos mínimos válidos. Los payloads re ``` ```bash -curl -s http://localhost:8899/server/healthcheck +curl -s http://localhost:8899/api/server/healthcheck ``` #### `GET /server/stats/summary` @@ -153,7 +153,7 @@ curl -s http://localhost:8899/server/healthcheck ``` ```bash -curl -s http://localhost:8899/server/stats/summary +curl -s http://localhost:8899/api/server/stats/summary ``` ### Extracción de documentos @@ -174,7 +174,7 @@ curl -s http://localhost:8899/server/stats/summary ```bash curl -s -X POST \ -F "file=@/resources/data/sample/document-01.docx" \ - http://localhost:8899/misc/document-extract + http://localhost:8899/api/misc/document-extract ``` Errores comunes: @@ -189,7 +189,7 @@ Errores comunes: - Respuesta `200`: `DocumentInformation` ```bash -curl -s -X POST "http://localhost:8899/anonymizer/predict?use_cache=true" \ +curl -s -X POST "http://localhost:8899/api/anonymizer/predict?use_cache=true" \ -H "Content-Type: application/json" \ -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' ``` @@ -218,7 +218,7 @@ curl -s -X POST "http://localhost:8899/anonymizer/predict?use_cache=true" \ - Respuesta `200`: `DocumentAnnotations` (incluye `data` y `label_policies` efectivas) ```bash -curl -s -X POST http://localhost:8899/anonymizer/disambiguate \ +curl -s -X POST http://localhost:8899/api/anonymizer/disambiguate \ -H "Content-Type: application/json" \ -d '{"paragraphs":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}}}' ``` @@ -228,7 +228,7 @@ curl -s -X POST http://localhost:8899/anonymizer/disambiguate \ - Respuesta `200`: `list[DocLabel] | null` ```bash -curl -s -X POST http://localhost:8899/anonymizer/validation \ +curl -s -X POST http://localhost:8899/api/anonymizer/validation \ -H "Content-Type: application/json" \ -d '{"text":"Acusado: Ramiro Marrón DNI 34.555.666."}' ``` @@ -240,7 +240,7 @@ curl -s -X POST http://localhost:8899/anonymizer/validation \ - Respuesta `200`: archivo `.odt` anonimizado (binario) ```bash -curl -X POST http://localhost:8899/anonymizer/anonymize-document \ +curl -X POST http://localhost:8899/api/anonymizer/anonymize-document \ -F "file=@/resources/data/sample/document-01.docx" \ -F 'annotations={"data":[{"document":"Acusado: Ramiro Marrón DNI 34.555.666.","labels":[]}],"label_policies":{"PER":{"anonymize":true,"disambiguation":"fuzzy"}},"render_policy":{"suffix_mode":"auto","suffix_threshold":1}}' ``` @@ -258,7 +258,7 @@ Errores comunes: - Respuesta `200`: `DocumentInformation` ```bash -curl -s -X POST "http://localhost:8899/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \ +curl -s -X POST "http://localhost:8899/api/datapublic/predict/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc?use_cache=true" \ -H "Content-Type: application/json" \ -d '{"text":"Buenos Aires, 17 de noviembre de 2024"}' ``` @@ -268,7 +268,7 @@ curl -s -X POST "http://localhost:8899/datapublic/predict/7e6b6f35-2f29-58f7-9f8 - Respuesta `404`: documento inexistente ```bash -curl -s http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc +curl -s http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc ``` #### `POST /datapublic/validation/document/{document_id}` @@ -276,7 +276,7 @@ curl -s http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7- - Respuesta `200`: body vacío ```bash -curl -s -X POST http://localhost:8899/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \ +curl -s -X POST http://localhost:8899/api/datapublic/validation/document/7e6b6f35-2f29-58f7-9f8e-fd1d9026a6bc \ -H "Content-Type: application/json" \ -d '{"materia":"penal","violencia_de_genero":"si"}' ``` @@ -287,12 +287,12 @@ Todos los endpoints de conversión usan `multipart/form-data` con campo `file`. | Método | Path | Input | Output | |---|---|---|---| -| `POST` | `/convert/pdf/odt` | `.pdf` | `.odt` | -| `POST` | `/convert/pdf/docx` | `.pdf` | `.docx` | -| `POST` | `/convert/docx/odt` | `.docx` | `.odt` | -| `POST` | `/convert/docx/pdf` | `.docx` | `.pdf` | -| `POST` | `/convert/odt/pdf` | `.odt` | `.pdf` | -| `POST` | `/convert/odt/docx` | `.odt` | `.docx` | +| `POST` | `/api/convert/pdf/odt` | `.pdf` | `.odt` | +| `POST` | `/api/convert/pdf/docx` | `.pdf` | `.docx` | +| `POST` | `/api/convert/docx/odt` | `.docx` | `.odt` | +| `POST` | `/api/convert/docx/pdf` | `.docx` | `.pdf` | +| `POST` | `/api/convert/odt/pdf` | `.odt` | `.pdf` | +| `POST` | `/api/convert/odt/docx` | `.odt` | `.docx` | Para endpoints con input PDF, query param opcional: - `backend=libreoffice|pandoc` (default: `libreoffice`) @@ -312,9 +312,9 @@ Errores comunes: Las siguientes rutas existen en código pero no están incluidas en `core.router` en runtime: - `aymurai/api/endpoints/routers/datapublic/dataset.py` - - define `/datapublic/dataset/*`, pero ese router no está montado. + - define `/api/datapublic/dataset/*`, pero ese router no está montado. - `aymurai/api/endpoints/routers/anonymizer/database.py` - - define `/anonymizer/database/*`, pero su include está comentado. + - define `/api/anonymizer/database/*`, pero su include está comentado. - `aymurai/api/endpoints/routers/database/*` - rutas administrativas de DB, sin montaje en `core.router`. diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..ff549f0e --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,13 @@ +node_modules +build +tmp +.nvmrc +.gitignore +.editorconfig +.tmp +.next +.DS_Store +*.log +README.md +CONTRIBUTING.md +out diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..e31ba0eb --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +node_modules/* +/.pnp +.pnp.js + +# testing +/coverage +.husky + +# production +/build +/dist +/out + +# misc +.DS_Store +.env +.env.development +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Panda CSS generated files +src/renderer/src/styled + +/me/ \ No newline at end of file diff --git a/frontend/.gitlab-ci.yml b/frontend/.gitlab-ci.yml new file mode 100644 index 00000000..942f2495 --- /dev/null +++ b/frontend/.gitlab-ci.yml @@ -0,0 +1,18 @@ +--- +image: alpine:latest + +stages: + - analysis + +# This creates a temporary cache folder that prevents from +# the node_modules, and .npm (also apply for .yarn) +# to be recreated each CI run +cache: + key: ${CI_COMMIT_REF_SLUG} + paths: + - node_modules + - .npm + +include: + - local: /.gitlab/analysis/lint.yml + - local: /.gitlab/analysis/audit.yml diff --git a/frontend/.gitlab/analysis/audit.yml b/frontend/.gitlab/analysis/audit.yml new file mode 100644 index 00000000..a66d3a44 --- /dev/null +++ b/frontend/.gitlab/analysis/audit.yml @@ -0,0 +1,13 @@ +--- +audit: + stage: analysis + image: node:14 + cache: + paths: + - node_modules + - .npm + before_script: + - npm ci + script: + - npm audit + allow_failure: true diff --git a/frontend/.gitlab/analysis/lint.yml b/frontend/.gitlab/analysis/lint.yml new file mode 100644 index 00000000..bcb728c6 --- /dev/null +++ b/frontend/.gitlab/analysis/lint.yml @@ -0,0 +1,12 @@ +--- +lint: + stage: analysis + image: node:14 + cache: + paths: + - node_modules + - .npm + before_script: + - npm ci + script: + - npm run lint diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 00000000..dd854dc4 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +public-hoist-pattern[]=@types/* diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 00000000..544138be --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 00000000..14cfdc59 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,27 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "biomejs.biome", + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "editor.rulers": [80, 120], + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + } +} diff --git a/frontend/CONTRIBUTING.md b/frontend/CONTRIBUTING.md new file mode 100644 index 00000000..7f08ddba --- /dev/null +++ b/frontend/CONTRIBUTING.md @@ -0,0 +1,86 @@ +# Best Practices + +## Linter + +This project uses _Create React App_ ESLint configuration as a base. You won't +be able to commit files with linting errors, and if you do a `--force` +you'll break CI. + +We also use [Markdownlint](https://github.com/DavidAnson/markdownlint) for +Markdown files. + +If you are using **VS Code**, you should install ESLint and Markdownlint +extensions. Not only it will show you linting errors but also you can set it to +fix some of them on save. To do so, edit your `settings.json` config file and +add the following: + +```json + "eslint.autoFixOnSave": true, + "eslint.alwaysShowStatus": true, + "eslint.validate": [ + "javascript", + "javascriptreact" + ] +``` + +## Branches + +- `main` is our main branch for nightly builds +- Try to not develop directly on `main` branch +- merges to `main` should be through `pull request` practice. + +### Branch naming + +- `poc/branch-name` for proof of concepts +- `feat/branch-name` for new features like, components, services, +validations, flows, etc. +- `test/branch-name` for unit/integration tests +- `fix/branch-name` for bugfixing code +- `hotfix/branch-name` for bugfixing in prod +- `docs/branch-name` for documentation + +### Pull Requests + +- Define a reviewer from the team. +- Close the branch after PR merge (except when it has the `WIP` prefix). +Otherwise this'll keep a ton unnecessary of UAT environments. +- Focus on one task at time +- Make atomic/little commits +- Push before leaving the office +- Be sure all of your tests passed + +## Commits + +We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) +specification. This will allow us to create automatized changelogs and semantic +versioning. Because of this, _you won't be able to commit messages that do not +follow this specification_. + +```text +feat: added hat wobble +^--^ ^---------------^ +| | +| +-> Summary in past simple tense. +| ++-------> Type: chore, docs, feat, fix, refactor, style, or test. +``` + +### Commit Types + +- `feat`: (new feature for the user, not a new feature for build script) +- `fix`: (bug fix for the user, not a fix to a build script) +- `docs`: (changes to the documentation) +- `style`: (formatting, missing semicolons, etc; no production code change) +- `refactor`: (refactoring production code, eg. renaming a variable) +- `test`: (adding missing tests, refactoring tests; no production code change) +- `chore`: (updating grunt tasks etc; no production code change) + +## Testing + +We use **[Jest](https://jestjs.io/)** for testing and. Tests are automatically +run during the Validate stage of the CI Pipeline. + +The pipeline enforces a 40% minimum coverage as an incentive to write tests. +It's strongly recommended that you raised this threshold over time. + +Jest can be configured via [jest.config.js](jest.config.js). diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..559f844f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,95 @@ +# AymurAI Desktop App + +## Technologies + +### Main technologies + +- ⚛️ **React** + 🆎 **TypeScript** as the _main framework_ with _type checking_ +tools +- ⚡ **Electron** as the _deployment and packaging tool_. It also serves the +purpose of communicating the webapp with the _NodeJS_ process. +- 🪡 **Stitches** as the _styling library_ +- 🛣️ **Tanstack React Router** as the _routing library_ to navigate across the webapp +- 📄 **Mammoth** + **ExcelJS** as the _libraries_ to read and write `.docx` and +datasheet files + +### Other technologies + +- **pnpm** as _package manager_ +- **biome** as code _linter and formatter_ + +##  Getting started + +1. Clone the repository + +1. Navigate to the repository folder and install dependencies with + + ```bash + pnpm install + ``` + +1. Start the app in development mode with + + ```bash + pnpm run dev + ``` + +## Run Aymurai API + +1. First download the image + + ```sh + docker pull registry.gitlab.com/collective.ai/datagenero-public/aymurai-api-prod + ``` + +2. And then create the corresponding container + + ```sh + docker run -p 8899:8899 -h -d registry.gitlab.com/collective.ai/datagenero-public/aymurai-api-prod:latest + ``` + +## Scripts + +### Development + +- `dev:web`: starts the frontend locally to be viewed in a conventional browser +- `dev`: starts the frontend locally and creates an _Electron_ instance to view it +- `start`: previews the production build with Electron +- `start:web`: previews the production web build in a browser + +### Build + +- `build:web`: builds the React Vite project +- `build`: builds both React and Electron projects (without packaging the application) + +### Validation + +- `lint`: runs the linter (_Biome_) across the entire project +- `lint:fix`: fixes linter errors automatically +- `format`: checks code formatting with _Biome_ +- `format:fix`: fixes code formatting automatically +- `typecheck`: runs type checking on both _Node_ and _Web_ applications +- `typecheck:node`: runs type checking on the _Electron_ application +- `typecheck:web`: runs type checking on the _React_ application +- `validate`: runs linting and type checking on both React and renderer +- `pre-commit`: runs _LintStaged_ + +### Deployment + +- `build:unpack`: builds and creates an unpacked distribution directory +- `build:[win|mac|linux]`: builds and packages the application for the desired target +- `package`: cleans build directory, builds, and packages the app with Electron Forge +- `make`: cleans build directory, builds, and creates distributables with Electron Forge +- `postinstall`: installs app dependencies for Electron Builder + +##  Colaborators + +- Melina Gatto (_Project Manager_) +- Luciana Vega (_Project Manager_) +- Andy Orlandi (_Product Designer_) +- Luciano Lapenna (_Developer_) +- Ender Puentes (_Developer_) + +## Special thanks + +- Lucia Wainfeld diff --git a/frontend/a.tml b/frontend/a.tml new file mode 100644 index 00000000..ffba6f07 --- /dev/null +++ b/frontend/a.tml @@ -0,0 +1,18 @@ +// import path from "node:path"; +// import react from "@vitejs/plugin-react"; +// import { defineConfig } from "vite"; + +// export default defineConfig({ +// plugins: [react()], +// build: { +// outDir: "out", // CRA's default build output +// }, +// resolve: { +// alias: [ +// { +// find: "@", +// replacement: path.resolve(__dirname, "src"), +// }, +// ], +// }, +// }); diff --git a/frontend/biome.json b/frontend/biome.json new file mode 100644 index 00000000..676bcd39 --- /dev/null +++ b/frontend/biome.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": ["node_modules", "build", ".husky", "out", "routeTree.gen.ts"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "performance": { + "noAccumulatingSpread": "warn", + "noDelete": "warn" + }, + "correctness": { + "useExhaustiveDependencies": "warn" + }, + "complexity": { + "noUselessSwitchCase": "off", + "noForEach": "off", + "noUselessFragments": "off", + "noBannedTypes": "warn" + }, + "style": { + "noNonNullAssertion": "warn", + "useSelfClosingElements": { + "fix": "safe", + "level": "error" + } + }, + "a11y": { + "noSvgWithoutTitle": "off", + "useKeyWithClickEvents": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/frontend/dev-app-update.yml b/frontend/dev-app-update.yml new file mode 100644 index 00000000..199fb557 --- /dev/null +++ b/frontend/dev-app-update.yml @@ -0,0 +1,3 @@ +provider: generic +url: https://example.com/auto-updates +updaterCacheDirName: vite-electron-test-updater diff --git a/frontend/docs/ANONIMIZADO.docx b/frontend/docs/ANONIMIZADO.docx new file mode 100644 index 00000000..a96d78e1 Binary files /dev/null and b/frontend/docs/ANONIMIZADO.docx differ diff --git a/frontend/docs/SIN_ANONIMIZAR.docx b/frontend/docs/SIN_ANONIMIZAR.docx new file mode 100644 index 00000000..c205bc9b Binary files /dev/null and b/frontend/docs/SIN_ANONIMIZAR.docx differ diff --git a/frontend/docs/ai.md b/frontend/docs/ai.md new file mode 100644 index 00000000..cd3f6d84 --- /dev/null +++ b/frontend/docs/ai.md @@ -0,0 +1,14 @@ +# AymurAI + +La AI está documentada en su swagger accediendo directamente a `{{AYMURAI_URL}}/api/docs` una vez que el servidor está corriendo. + +> 💡 Si la AI está corriendo en la red LAN o bajo un ngrok, se debe cambiar la ruta. + +## Uso + +Para usar la AI en la aplicación se ponen a disposición cuatro funciones en la carpeta `/src/services/aymurai`: + +- `document-extract.ts`: obtiene cada párrafo del documento en texto plano para poder renderizarlo en la aplicación. +- `predict.ts`: genera predicciones sobre los párrafos previamente extraídos. +- `anonymize.ts`: permite generar un archivo `.odt` con las anotaciones hechas por el usuario y la AI. +- `health-check.ts`: se utiliza para verificar si la api está funcionando de manera correcta. Se utiliza en la pantalla de inicio. diff --git a/frontend/docs/authentication.md b/frontend/docs/authentication.md new file mode 100644 index 00000000..b1932b0c --- /dev/null +++ b/frontend/docs/authentication.md @@ -0,0 +1,74 @@ +# Authentication + +La autenticación en la app _AymurAI_ se utiliza únicamente para el guardado de la información de la spreadsheet en el set de datos. Hay dos opciones: + +- [_Google OAuth2_](#google-oauth2) + (cargando la información en _Google Drive_) +- [_offline local_](#local) + (escribiendo en el _filesystem_). + +Actualmente la autenticación con Google se puede realizar en el paso de guardar documento. Anteriormente, esta opción se seleccionaba en la pantalla de inicio, pero fue removida ya que generaba confusión a los usuarios por dar a entender que la aplicación operaba online cuando en realidad la única instancia en la cual lo puede hacer es a la hora de guardar la spreadsheet de Drive. + +Para facilitar el proceso se creó el hook [`useLogin`](../src/hooks/useLogin.ts). + +```ts +const { logout, login } = useLogin({ + onLogout: () => ..., +}); +``` + +## Google OAuth2 + +Utiliza el _social login_ de _Google_ para poder interactuar con la +[_Spreadsheet Google API_](https://developers.google.com/sheets/api/reference/rest). +Esto permite leer/escribir en una hoja de cálculo de _Google Drive_. + +El funcionamiento es el siguiente: + +1. Una vez se presiona el botón Google la app de React se comunica con el + proceso de Node para abrir una nueva ventana que cargaría la _consent screen_. + El proceso sería: + + 1. Se genera un _challenge code_. + 2. Junto al código mencionado se añaden otros datos como el + `client_id`, `redirect_uri`, etc. + 3. Se genera una URL con los datos anteriores para acceder a + `'https://accounts.google.com/o/oauth2/...`. + 4. Se abre una nueva ventana con la URL previamente generada. + +2. Una vez abierta la _consent screen_ al comunicarse con el proceso de + Electron, se añade un _event listener_ para poder tener los datos del usuario y + guardarlos en la sesión. + +3. Esta comunicación genera un código de autorización que se utiliza para + obtener el _access token_ y el _refresh token_. Para actualizar el _access token_ + se utiliza el _refresh token_ y un timer cada 45min. + +> 💡 Para pasar la información devuelta a la app de _React_ se utiliza un +> [deep link](https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app). +> Suele suceder que en modo _development_ no quede definido el _protocol_ para el +> _deep link_, por lo que se debe usar la versión productiva. + +![Electron PKCE para OAuth2](./images/electron_pkce.png) + +> 💡 [Más información](./excel/spreadsheet-api.md) en cómo se utiliza la +> _Google Spreadsheet API_ para generar los archivos. + +## Local + +La app simplemente funciona de manera _local_ y _offline_, leyendo/escribiendo +sobre un archivo de datasheet en el filesystem. No requiere ninguna acción extra +por parte del usuario. + +Esta opción simplemente inicializa el state de la app con la siguiente +información: + +```ts +const loginOffline = (funcType: FunctionType) => { + setUser({ online: false, function: funcType, token: "" }); + // ... +}; +``` + +> 💡 [Más información](./excel/filesystem.md) en cómo y dónde se genera el +> archivo `.xlsx`. diff --git a/frontend/docs/aymurai.md b/frontend/docs/aymurai.md new file mode 100644 index 00000000..183ed62c --- /dev/null +++ b/frontend/docs/aymurai.md @@ -0,0 +1,70 @@ +# AymurAI + +## Tecnologías principales + +AymurAI es una aplicación de escritorio basada en +[NodeJS](https://nodejs.org/en/) que utiliza tres principales tecnologías: + +- **[Electron.JS](https://www.electronjs.org/)**: elegido por su capacidad de + poder crear aplicaciones multiplataforma, Electron nos permite crear + aplicaciones web que pueden ser distribuidas sin la necesidad de tener un + browser preinstalado +- **[React](https://reactjs.org/)**: elegido por su versatilidad, ReactJS es un + web framework que nos permite crear todo tipo de web apps +- **[TypeScript](https://www.typescriptlang.org/)**: un _superset_ (es + decir, que añade features al original, pero sin modificar sus principios) de + _JavaScript_ que permite crear aplicaciones con la seguridad de que el flujo de + datos respeta un orden particular y definido + +Esta combinación de frameworks y lenguajes resulta muy eficiente ya que se puede +crear una web app desde cero, distribuible facilmente y multiplataforma, y con +herramientas incluidas para poder realizar un desarrollo confiable y +con la menor cantidad de _bugs_ posibles. + +Además, el uso de _Electron_ nos permite combinar las ventajas de usar _NodeJS_ +y todos sus paquetes como proceso principal y luego usar un browser levantado +por el mismo _Electron_ basado en +[Chromium](https://www.chromium.org/chromium-projects/) para utilizar las +[Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) que podemos +encontrar en cualquier browser. + +## Otras tecnologías + +Además de las tecnologías principales anteriormente mencionadas se usaron: + +- **[Stitches](https://stitches.dev/)**: elegida como librería de estilos, nos + permite configurar toda una lista de _tokens_ que pueden ser utilizados a lo + largo de toda la aplicación manteniendo una alta consistencia entre los estilos. + Esto nos permite configurar y estandarizar colores, espaciados, sombras, etc además + de darnos la posibilidad de trabajar con _styled components_ para un + ágil desarrollo +- **[Google OAuth2](https://developers.google.com/identity/protocols/oauth2)**: + sistema de autenticación de usuarios basado en [OAuth](https://oauth.net/) que + nos permite tener un acceso total a las APIs de _Google_, permitiéndonos editar + en tiempo real documentos _Spreadsheet_. Sin este sistema de autenticación no se + podría obtener el token que nos permite interaccionar con las APIs + +## Estructura + +Se dividió la aplicación en 6 páginas, que se presentan en el siguiente orden: + +1. **Login**: página en la que se selecciona cómo conectarse a la API de AyMurai y cuál funcionalidad se desea utilizar (anonimizador o set de datos) +1. **Onboarding**: se nos muestra una guía sobre el funcionamiento de la + aplicación y los pasos a seguir para poder realizar la validación de nuestros + archivos. Es en este paso donde podremos cargar nuestros documentos +1. **Preview**: obtendremos una vista previa de los escritos cargados en la + página anterior con la oportunidad de cargar nuevos documentos si es necesario. + Es aquí donde podemos decidir cuáles son los archivos finales que serán + procesados por la inteligencia artificial +1. **Process**: página de procesamiento. En este paso se realiza la consulta + a la inteligencia artificial para obtener las predicciones correspondientes + a cada archivo. Esto puede tardar un poco. Podemos detener el procesamiento + de los documentos o reemplazarlos en cualquier momento si es necesario +1. **Validation**: es en esta instancia donde validaremos lo que nos ha devuelto + la inteligencia artificial. Tendremos un form donde podremos ir seleccionando + las sugerencias hechas por la IA o completar con lo que corresponda en ese momento. + Cada vez que se valida un documento, se crea una conexión con la + _Spreadsheet API_ de _Google_ para poder enviar los datos finales +1. **Finish**: podremos ver una vista preview de los documentos que validamos y + enviamos, con la posibilidad de revisar el set de datos o de realizar una nueva + carga, reiniciando el flujo de trabajo diff --git a/frontend/docs/excel/filesystem.md b/frontend/docs/excel/filesystem.md new file mode 100644 index 00000000..28ae5b55 --- /dev/null +++ b/frontend/docs/excel/filesystem.md @@ -0,0 +1,69 @@ +# Filesystem + +Las funcionalidades de _NodeJS_ sobre el _filesystem_ se aprovechan para poder +leer y escribir sobre el set de datos de forma local y offline en la +computadora. Además, también se generan reportes que ayudan a reentrenar el +modelo de inteligencia artificial. + +## Ubicación + +Las ubicaciones para los archivos de exportación se encuentran en la carpeta: + +```ts +// Ejemplo windows: C:\Users\[Usuario]\Documents\AymurAI +const EXPORTS_FOLDER = path.resolve(homedir(), 'Documents/AymurAI'); +``` + +## API + +Para poder formar un canal de comunicación entre la aplicación de _React_ +y _Electron_, se utilizan los [_preload scripts_](https://www.electronjs.org/docs/latest/tutorial/tutorial-preload) +para exponer una _API_ que permita la lectura y escritura de archivos. + +De esta forma el _frontend_ podría acceder a las funcionalidades de _NodeJS_ +utilizando `window.filesystem.excel.read` y `window.filesystem.excel.write` por ejemplo. +Estas funciones fueron definidas dentro de los archivos de _Electron_. + +Para el caso de los reportes se utiliza `window.filesystem.feedback.export`. + +> 📚 Se pueden encontrar estos scripts y más yendo al +[script preloader del proyecto.](../../renderer/preload.ts) + +## Tipo de archivo + +### Set de datos + +El set de datos se exporta en formato _Excel_ con extensión `.xlsx`. + +```ts +const FILENAME = 'set_de_datos.xlsx'; +// Ejemplo windows: C:/Users/[Usuario]/Documents/AymurAI/set_de_datos.xlsx +const PATH = `${EXPORTS_FOLDER}/${FILENAME}`; +``` + +Desde el frontend se utiliza la librería [_ExcelJS_](https://www.npmjs.com/package/exceljs) +que permite leer un archivo `.xlsx`, modificarlo en memoria y luego escribir los +cambios en el filesystem, apoyándose en las funcionalidades de _NodeJS_. + +```ts +import { Workbook } from 'exceljs'; + +// Ejemplo de escritura de archivo +async function writeXLSX(workbook: Workbook) { + const buffer = (await workbook.xlsx.writeBuffer()) as Buffer; + + return window.filesystem.excel.write(buffer); +} +``` + +### Reportes / Feedback + +El reporte se exporta en formato _JSON_ con extensión `.json`. Utiliza el nombre +del archivo procesado y la fecha para generar el nombre del archivo. + +```ts +const FEEDBACK_FOLDER = `${EXPORTS_FOLDER}/feedback`; +const FILENAME = `${FEEDBACK_FOLDER}/{name}-{date}.json` +``` + +Se escribe en el filesystem de forma tradicional. diff --git a/frontend/docs/excel/spreadsheet-api.md b/frontend/docs/excel/spreadsheet-api.md new file mode 100644 index 00000000..a2087b40 --- /dev/null +++ b/frontend/docs/excel/spreadsheet-api.md @@ -0,0 +1,30 @@ +# Google Spreadsheet API + +Para leer y escribir de forma remota en una hoja de cálculo de _Google Sheets_ se +utiliza la [REST API](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets) +provista por _Google_. + +## Ubicación + +La ubicación del dataset está definida de forma __estática__ dentro del +[código](../../src/utils/config.ts). Esto significa que para poder acceder a +este archivo se necesita tener accesos de lectura/escritura sobre el archivo. + +## Implementación + +Dentro del proyecto se crearon una serie de funciones que ayudan a interactuar +con la API de _Google Sheets_. En general lo que hacen es _envolver_ las +llamadas a la _REST API_ utilizando el _access token_ provisto por el +[proceso de autenticación](../authentication.md). + +> Estas funciones se pueden encontrar dentro de la carpeta de +[servicios de google](../../src/services/google/). + +Por ejemplo, para obtener el contenido de una hoja de cálculo se utiliza la +función: + +```ts +import google from 'services/google'; + +await google(authToken).spreadsheet(id).range('A1:B2'); +``` diff --git a/frontend/docs/images/auth_flows.png b/frontend/docs/images/auth_flows.png new file mode 100644 index 00000000..8a98cee5 Binary files /dev/null and b/frontend/docs/images/auth_flows.png differ diff --git a/frontend/docs/images/electron_pkce.png b/frontend/docs/images/electron_pkce.png new file mode 100644 index 00000000..37c50972 Binary files /dev/null and b/frontend/docs/images/electron_pkce.png differ diff --git a/frontend/docs/images/initial-screen.png b/frontend/docs/images/initial-screen.png new file mode 100644 index 00000000..c339bdc9 Binary files /dev/null and b/frontend/docs/images/initial-screen.png differ diff --git a/frontend/docs/images/submit-form.png b/frontend/docs/images/submit-form.png new file mode 100644 index 00000000..f2505dc5 Binary files /dev/null and b/frontend/docs/images/submit-form.png differ diff --git a/frontend/docs/images/url-selector.png b/frontend/docs/images/url-selector.png new file mode 100644 index 00000000..611d7b1c Binary files /dev/null and b/frontend/docs/images/url-selector.png differ diff --git a/frontend/docs/images/validation-form.png b/frontend/docs/images/validation-form.png new file mode 100644 index 00000000..0680dd98 Binary files /dev/null and b/frontend/docs/images/validation-form.png differ diff --git a/frontend/docs/routing.md b/frontend/docs/routing.md new file mode 100644 index 00000000..d84c7e30 --- /dev/null +++ b/frontend/docs/routing.md @@ -0,0 +1,30 @@ +# Routing + +El proyecto usa `react-router-dom` para manejar las rutas de la aplicación. Las rutas se definen en el archivo `src/index.tsx` y se deben agregar en el objeto `router` de la siguiente manera: + +```jsx +const router = createRouter([ + { + // Main as a layout element + path: '/', + element: , + children: [ + { path: 'onboarding', element: }, + { path: 'preview', element: }, + { path: 'process', element: }, + ], + }, + { + path: '/login', + element: , + }, +]); +``` + +Al ser un proyecto basado en _Electron_, el router que utilizamos no es el mismo que se usa en un proyecto web normal. Para este caso, usamos un [_memory router_](https://reactrouter.com/en/main/routers/create-memory-router#creatememoryrouter) que mantiene en memoria el history stack de las rutas. + +## Protección de rutas + +Existen dos high order components que se encargan de proteger las rutas de la aplicación dependiendo qué se requiere: + +- `withAuthProtection`: verifica si el usuario está autenticado y redirige a `/login` si no lo está. diff --git a/frontend/docs/url_selector.md b/frontend/docs/url_selector.md new file mode 100644 index 00000000..2eecec44 --- /dev/null +++ b/frontend/docs/url_selector.md @@ -0,0 +1,17 @@ +# URL Selector + +En la pantalla de inicio el usuario puede elegir cómo desea conectarse a la API de AyMurAI. Hay dos opciones: + +- De manera local: esto significa, levantando Docker localmente ya sea de forma manual o utilizando el ejecutable desarrollado por el equipo de Collective AI para levantar la API utilizando **[Miniconda](https://docs.anaconda.com/miniconda/)**. + +La aplicación incluye el archivo run_server.bat que, en conjunto con el instalador desarrollado por Collective, procura levantar el server local de modo automático. El funcionamiento de esta feature está condicionada a que el usuario efectivamente tenga el directorio de Miniconda en su PC y esté utilizando Windows. En caso de que dicha ejecución falle, se muestra una alerta al usuario que le indica que deberá hacerlo de forma manual. + +- A través de un servidor: los distintos juzgados pueden tener servidores propios a las que se accede a través de su URL. + + Por razones de seguridad, no es conveniente que estas URLs estén en el código de la aplicación, por lo cual el usuario deberá ingrearlas manualmente. + + A fin de mejorar la experiencia de uso, la URL se preserva y autocompleta evitando así que el usuario tenga que reingresarla cada vez que inicia la aplicación. + +| Pantalla inicial | Selector | +| ------------------------------------------------ | ------------------------------------------ | +| ![Pantalla inicial](./images/initial-screen.png) | ![Selector URL](./images/url-selector.png) | diff --git a/frontend/docs/validator/README.md b/frontend/docs/validator/README.md new file mode 100644 index 00000000..d4d13239 --- /dev/null +++ b/frontend/docs/validator/README.md @@ -0,0 +1,55 @@ +# Validación + +El proceso de guardar la información analizada por la AI tiene que ser validado +por un usuario para asegurar que la información es correcta. La validación se +realiza medianete un formulario en el cual se carga la información que devuelve +la API y el usuario puede modificarla si es necesario. + +![Validation form view](../images/validation-form.png) + +Se desarrollaron varias herramientas para hacer más fácil la generación de las +distintas partes de la pantalla de validación. Entre ellas está el hook +`useForm` y el componente de formulario ``. + +## Hook `useForm` + +Este hook ofrece dos funciones útiles para generar formularios: + +- `submit`: una función que se encarga de recolectar toda la información de los +campos del formulario para poder utilizarla a conveniencia del desarrollador. + + ```tsx + // Ejemplo de uso + const { submit } = useForm(); + const handler = submit(data => ...) + + return
+ ``` + +- `register`: función que permite agregar campos al "state" (más bien una +lista de referencias a los ``) para luego ser recolectados por el +`submit`. Acepta un `string` que representa el "id" del campo. + + ```tsx + // Ejemplo de uso + const { register } = useForm(); + + return + ``` + +> 💡 Más información sobre cómo están compuestas y se utilizan estas funciones en +el archivo del [hook useForm](../../src/hooks/useForm/index.ts) + +## Componente `` + +La pantalla de validación se constituye por varios `
` encapsulados por el +componente `` para "estandarizar" su comportamiento. +Simplemente se encarga de envolver los campos (a los cuales se les pasa un +handler `onChange` para detectar cuando se ingresa algún valor) y de agregar un +botón de submit. + +> ⚠️ El botón de submit NO envía toda la pantalla, si no que valida el grupo de +campos individual. Para enviar TODA la información (o pasar a la siguiente +resolución) se debe usar el botón del footer. + +![Submit button](../images/submit-form.png) diff --git a/frontend/electron-builder.yml b/frontend/electron-builder.yml new file mode 100644 index 00000000..46eea126 --- /dev/null +++ b/frontend/electron-builder.yml @@ -0,0 +1,44 @@ +appId: aymurai.app +productName: AymurAI +directories: + buildResources: build +files: + - '!**/.vscode/*' + - '!src/*' + - '!electron.vite.config.{js,ts,mjs,cjs}' + - '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' + - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' + - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' +asarUnpack: + - resources/** +win: + executableName: AymurAI + icon: src/renderer/public/brand/logo.jpg +nsis: + artifactName: ${name}-${version}-setup.${ext} + shortcutName: ${productName} + uninstallDisplayName: ${productName} + createDesktopShortcut: always +mac: + entitlementsInherit: build/entitlements.mac.plist + icon: src/renderer/public/brand/logo.jpg + extendInfo: + - NSCameraUsageDescription: Application requests access to the device's camera. + - NSMicrophoneUsageDescription: Application requests access to the device's microphone. + - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. + - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. +dmg: + artifactName: ${name}-${version}.${ext} +linux: + target: + - AppImage + - snap + - deb + maintainer: datagenero.org + category: Utility +appImage: + artifactName: ${name}-${version}.${ext} +npmRebuild: false +publish: + provider: generic + url: https://example.com/auto-updates diff --git a/frontend/electron.vite.config.ts b/frontend/electron.vite.config.ts new file mode 100644 index 00000000..a0453cb7 --- /dev/null +++ b/frontend/electron.vite.config.ts @@ -0,0 +1,42 @@ +/** + * Electron-vite config for building the desktop app. + * Used by the `dev`, `build`, and `build:*` scripts. + * + * Configures all three Electron processes: main, preload, and renderer. + * `envDir` is set explicitly so `.env` files are resolved from the project root. + */ +import { tanstackRouter } from "@tanstack/router-plugin/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig, externalizeDepsPlugin } from "electron-vite"; +import { resolve } from "node:path"; + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + }, + preload: { + plugins: [externalizeDepsPlugin()], + }, + renderer: { + envDir: resolve("."), + // Use relative asset URLs so the renderer works under the file:// protocol + // when the app is packaged. The standalone web build (vite.config.ts) + // keeps the default base of "/". + base: "./", + server: { + port: 3000, + }, + resolve: { + alias: { + "@": resolve("src/renderer/src"), + }, + }, + plugins: [ + tanstackRouter({ + target: "react", + autoCodeSplitting: true, + }), + react(), + ], + }, +}); diff --git a/frontend/forge.config.js b/frontend/forge.config.js new file mode 100644 index 00000000..7c0bc436 --- /dev/null +++ b/frontend/forge.config.js @@ -0,0 +1,58 @@ +const path = require("node:path"); +const fs = require("node:fs"); + +module.exports = { + packagerConfig: { + // This forces forge to only package already builded files + ignore: ["^\\/public$", "^\\/src$", "^\\/node_modules$", "^\\/[.].+"], + icon: "public/brand/logo256-text", + name: "AymurAI", + protocols: [ + { + name: "AymurAI", + schemes: ["aymurai.app"], + }, + ], + }, + rebuildConfig: {}, + makers: [ + { + name: "@electron-forge/maker-squirrel", + config: {}, + }, + { + name: "@electron-forge/maker-zip", + platforms: ["darwin"], + }, + { + name: "@electron-forge/maker-deb", + config: { + mimeType: ["x-scheme-handler/aymurai.app"], + }, + }, + { + name: "@electron-forge/maker-rpm", + config: {}, + }, + ], + hooks: { + packageAfterCopy: async ( + _config, + buildPath, + _electronVersion, + _platform, + _arch, + ) => { + // Create logs in the root directory + const twoAbove = path.join(buildPath, "../../"); + // const logMessage = `Build Path: ${twoAbove}\nDirname: ${__dirname}\n---\n`; + // fs.appendFileSync(path.join(__dirname, "logs.txt"), logMessage); + + // biome-ignore lint/style/noVar: Don't know why this is needed + var src = path.join(__dirname, "build/app/run_server.bat"); + // biome-ignore lint/style/noVar: Don't know why this is needed + var dst = `${twoAbove}/run_server.bat`; + fs.cpSync(src, dst, { recursive: true }); + }, + }, +}; diff --git a/frontend/knip.json b/frontend/knip.json new file mode 100644 index 00000000..4651583e --- /dev/null +++ b/frontend/knip.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "tags": [ + "-lintignore" + ] +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..1830abf8 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,100 @@ +{ + "name": "aymurai", + "version": "1.5.0", + "private": true, + "main": "./out/main/index.js", + "scripts": { + "dev:web": "cross-env VITE_APP_MODE=web vite --config vite.config.ts", + "dev": "cross-env VITE_APP_MODE=electron electron-vite dev", + "start": "cross-env VITE_APP_MODE=electron electron-vite preview", + "start:web": "cross-env VITE_APP_MODE=web vite preview --config vite.config.ts", + "lint": "biome check", + "lint:fix": "biome check --write", + "format": "biome format", + "format:fix": "biome format --write", + "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", + "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", + "typecheck": "npm run typecheck:node; npm run typecheck:web", + "validate": "npm run lint && npm run check-types:react && npm run check-types:renderer", + "pre-commit": "lint-staged", + "package": "rm -rf build && npm run build && electron-forge package", + "make": "rm -rf build && npm run build && electron-forge make", + "postinstall": "electron-builder install-app-deps || true", + "panda:codegen": "panda codegen", + "prepare": "panda codegen", + "build:web": "cross-env VITE_APP_MODE=web vite build --config vite.config.ts", + "build": "npm run typecheck && cross-env VITE_APP_MODE=electron electron-vite build", + "build:unpack": "npm run build && electron-builder --dir", + "build:win": "npm run build && electron-builder --win", + "build:mac": "cross-env VITE_APP_MODE=electron electron-vite build && electron-builder --mac", + "build:linux": "cross-env VITE_APP_MODE=electron electron-vite build && electron-builder --linux", + "knip": "knip", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@electron-toolkit/preload": "^3.0.2", + "@electron-toolkit/utils": "^4.0.0", + "electron-squirrel-startup": "^1.0.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@electron-forge/cli": "^6.0.4", + "@electron-forge/maker-deb": "^6.0.4", + "@electron-forge/maker-rpm": "^6.0.4", + "@electron-forge/maker-squirrel": "^6.0.4", + "@electron-forge/maker-zip": "^6.0.4", + "@electron-toolkit/tsconfig": "^1.0.1", + "@pandacss/dev": "^1.8.1", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tooltip": "^1.2.8", + "@stitches/react": "^1.2.8", + "@tanstack/react-form": "1.11.0", + "@tanstack/react-query": "^5.100.5", + "@tanstack/react-router": "^1.139.12", + "@tanstack/react-router-devtools": "^1.139.12", + "@tanstack/router-plugin": "^1.139.12", + "@types/node": "20", + "@types/react": "19", + "@types/react-dom": "19", + "@types/regex-escape": "^3.4.1", + "@vitejs/plugin-react": "^4.5.0", + "axios": "^1.12.0", + "concurrently": "^7.5.0", + "cross-env": "^7.0.3", + "electron": "^22.3.25", + "electron-builder": "^26.0.12", + "electron-debug": "^3.2.0", + "electron-devtools-installer": "^3.2.1", + "electron-vite": "^3.1.0", + "exceljs": "^4.4.0", + "husky": "^8.0.0", + "i18next": "^25.8.13", + "knip": "^6.7.0", + "lint-staged": "^13.0.3", + "markdownlint-cli": "^0.32.2", + "nodemon": "^2.0.20", + "phosphor-react": "^1.4.1", + "prettier": "^2.7.1", + "react": "19", + "react-dom": "19", + "react-hot-toast": "^2.6.0", + "react-i18next": "^16.5.4", + "regex-escape": "^3.4.10", + "typescript": "^5.9.2", + "vite": "^6.3.5", + "vitest": "^4.1.6", + "wait-on": "^6.0.1", + "zod": "^4.0.14", + "zustand": "^5.0.7" + } +} \ No newline at end of file diff --git a/frontend/panda.config.ts b/frontend/panda.config.ts new file mode 100644 index 00000000..72f545b2 --- /dev/null +++ b/frontend/panda.config.ts @@ -0,0 +1,233 @@ +import { defineConfig, defineGlobalStyles } from "@pandacss/dev"; + +const globalCss = defineGlobalStyles({ + "*": { + fontFamily: + '"Archivo", -apple-system, Helvetica Neue, Helvetica, Roboto, sans-serif', // TODO: Replace token here (was $primary) + }, + + html: { + color: "#110041", + }, + + "mark.predicted-word": { + backgroundColor: "#E6E8FF", // TODO: Replace token here (was $primaryAlt) + fontFamily: '"Times New Roman", Times, serif', // TODO: Replace token here (was $file) + padding: "0px 0px 0px 2px", + borderRadius: "8px", + + "& strong": { + fontSize: "12px", + padding: "0px", + margin: "0px", + }, + + "& button.remove-tag": { + visibility: "hidden", + position: "relative", + backgroundColor: "#DC582E", // TODO: Replace token here (was $errorPrimary) + color: "#FFFFFF", // TODO: Replace token here (was $white) + padding: "3px 5px", + borderRadius: "8px", + cursor: "pointer", + fontSize: "10px", + fontWeight: 800, // TODO: Replace token here (was $heavy) + textAlign: "center", + top: "-10px", + right: "-5px", + border: "none", + }, + + "&:hover": { + cursor: "pointer", + "& button.remove-tag": { + visibility: "visible", + }, + }, + }, + + "mark.searched-word": { + backgroundColor: "#E0DDE2", // TODO: Replace token here (was $bgSecondaryAlt) + fontFamily: '"Times New Roman", Times, serif', // TODO: Replace token here (was $file) + padding: "0px 2px", + borderRadius: "8px", + + "&:hover": { + cursor: "pointer", + }, + + "& button.add-tag": { + position: "relative", + backgroundColor: "#1B834E", // TODO: Replace token here (was $successPrimary) + color: "#FFFFFF", // TODO: Replace token here (was $white) + padding: "2px 5px", + borderRadius: "8px", + cursor: "pointer", + fontSize: "12px", + fontWeight: 800, // TODO: Replace token here (was $heavy) + textAlign: "center", + top: "-10px", + right: "-5px", + border: "none", + }, + }, +}); + +const text = (size: number, weight: number, lineHeight: string) => ({ + value: { + fontSize: `${size}px`, + fontWeight: weight, + lineHeight, + }, +}); + +const color = (hex: string) => ({ value: hex }); + +export default defineConfig({ + // Use CSS reset + preflight: true, + + // Where to look for CSS declarations + include: ["./src/renderer/src/**/*.{ts,tsx}"], + + // Files to exclude + exclude: [], + + // JSX framework + jsxFramework: "react", + + // Output directory for generated styled-system + outdir: "src/renderer/src/styled", + + // Global styles (migrated from Stitches globalStyles.ts) + globalCss, + + // Other configuration + strictTokens: true, + + theme: { + extend: { + keyframes: { + fadeIn: { + from: { opacity: "0" }, + to: { opacity: "1" }, + }, + fadeOut: { + from: { opacity: "1" }, + to: { opacity: "0" }, + }, + }, + tokens: { + animations: { + fadeIn: { value: "fadeIn 0.15s ease-out" }, + fadeOut: { value: "fadeOut 0.1s ease-in" }, + }, + }, + }, + textStyles: { + title: { + md: { + strong: text(32, 700, "120%"), + default: text(32, 400, "120%"), + }, + }, + subtitle: { + md: { + strong: text(20, 600, "120%"), + default: text(20, 400, "120%"), + }, + sm: { + strong: text(14, 600, "120%"), + default: text(14, 400, "120%"), + }, + }, + paragraph: { + md: { + strong: text(18, 600, "150%"), + default: text(18, 400, "150%"), + }, + sm: { + strong: text(16, 600, "140%"), + default: text(16, 400, "140%"), + }, + xsm: { + strong: text(10, 600, "140%"), + default: text(10, 400, "140%"), + }, + }, + cta: { + md: { + strong: text(16, 600, "100%"), + default: text(16, 400, "100%"), + }, + sm: { + strong: text(16, 600, "115%"), + default: text(16, 400, "115%"), + }, + }, + label: { + md: { + strong: text(16, 600, "120%"), + default: text(16, 400, "120%"), + }, + sm: { + strong: text(12, 600, "120%"), + default: text(12, 400, "120%"), + }, + }, + }, + semanticTokens: { + colors: { + brand: { + primary: color("#3F479D"), + secondary: color("#C3CCD7"), + tertiary: color("#4A5568"), + }, + text: { + default: color("#110041"), + lighter: color("#625C68"), + "onbutton-default": color("#110041"), + "onbutton-alternative": color("#FFFFFF"), + "onbutton-disabled": color("#2D3748"), + }, + action: { + default: color("#C5CAFF"), + disabled: color("#E0DDE2"), + "alt-default": color("#3F479D"), + hover: color("#110041"), + pressed: color("#3F479D"), + focus: color("#C5CAFF"), + }, + bg: { + primary: color("#F6F5F7"), + secondary: color("#FFFFFF"), + "primary-alternative": color("#E5E8FF"), + "primary-highlight": color("#C5CAFF"), + "secondary-highlight": color("#E0DDE2"), + }, + system: { + success: color("#1B834E"), + "success-secondary": color("#E0FAED"), + error: color("#DC582E"), + "error-secondary": color("#FFECE5"), + warning: color("#F2BA2C"), + "warning-secondary": color("#FFF7DB"), + info: color("#3F479D"), + "info-secondary": color("#F6F5F7"), + }, + }, + borders: { + primary: { value: "1px solid #BCBAB8" }, + secondary: { value: "1px solid #EDF2F7" }, + "primary-alt": { value: "1px solid #110041" }, + error: { value: "1px solid {colors.system.error}" }, + }, + gradients: { + primary: { + value: + "linear-gradient(249.5deg, #C5CAFF -33.26%, #8591E8 28.49%, #3F479D 82.99%)", + }, + }, + }, + }, +}); diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 00000000..e5bbfd38 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,11035 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.2.4) + '@electron-toolkit/preload': + specifier: ^3.0.2 + version: 3.0.2(electron@22.3.27) + '@electron-toolkit/utils': + specifier: ^4.0.0 + version: 4.0.0(electron@22.3.27) + electron-squirrel-startup: + specifier: ^1.0.0 + version: 1.0.1 + devDependencies: + '@biomejs/biome': + specifier: ^1.9.4 + version: 1.9.4 + '@electron-forge/cli': + specifier: ^6.0.4 + version: 6.4.2(encoding@0.1.13) + '@electron-forge/maker-deb': + specifier: ^6.0.4 + version: 6.4.2 + '@electron-forge/maker-rpm': + specifier: ^6.0.4 + version: 6.4.2 + '@electron-forge/maker-squirrel': + specifier: ^6.0.4 + version: 6.4.2 + '@electron-forge/maker-zip': + specifier: ^6.0.4 + version: 6.4.2 + '@electron-toolkit/tsconfig': + specifier: ^1.0.1 + version: 1.0.1(@types/node@20.19.1) + '@pandacss/dev': + specifier: ^1.8.1 + version: 1.8.1(hono@4.11.7)(typescript@5.9.2) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@stitches/react': + specifier: ^1.2.8 + version: 1.2.8(react@19.2.4) + '@tanstack/react-form': + specifier: 1.11.0 + version: 1.11.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-query': + specifier: ^5.100.5 + version: 5.100.5(react@19.2.4) + '@tanstack/react-router': + specifier: ^1.139.12 + version: 1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-router-devtools': + specifier: ^1.139.12 + version: 1.139.12(@tanstack/react-router@1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.139.12)(@types/node@20.19.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.3) + '@tanstack/router-plugin': + specifier: ^1.139.12 + version: 1.139.12(@tanstack/react-router@1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)) + '@types/node': + specifier: '20' + version: 20.19.1 + '@types/react': + specifier: '19' + version: 19.2.13 + '@types/react-dom': + specifier: '19' + version: 19.2.3(@types/react@19.2.13) + '@types/regex-escape': + specifier: ^3.4.1 + version: 3.4.1 + '@vitejs/plugin-react': + specifier: ^4.5.0 + version: 4.5.0(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)) + axios: + specifier: ^1.12.0 + version: 1.13.4 + concurrently: + specifier: ^7.5.0 + version: 7.6.0 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + electron: + specifier: ^22.3.25 + version: 22.3.27 + electron-builder: + specifier: ^26.0.12 + version: 26.0.12(electron-builder-squirrel-windows@26.0.12) + electron-debug: + specifier: ^3.2.0 + version: 3.2.0 + electron-devtools-installer: + specifier: ^3.2.1 + version: 3.2.1 + electron-vite: + specifier: ^3.1.0 + version: 3.1.0(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)) + exceljs: + specifier: ^4.4.0 + version: 4.4.0 + husky: + specifier: ^8.0.0 + version: 8.0.3 + i18next: + specifier: ^25.8.13 + version: 25.8.13(typescript@5.9.2) + knip: + specifier: ^6.7.0 + version: 6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + lint-staged: + specifier: ^13.0.3 + version: 13.3.0 + markdownlint-cli: + specifier: ^0.32.2 + version: 0.32.2 + nodemon: + specifier: ^2.0.20 + version: 2.0.22 + phosphor-react: + specifier: ^1.4.1 + version: 1.4.1(react@19.2.4) + prettier: + specifier: ^2.7.1 + version: 2.8.8 + react: + specifier: '19' + version: 19.2.4 + react-dom: + specifier: '19' + version: 19.2.4(react@19.2.4) + react-hot-toast: + specifier: ^2.6.0 + version: 2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-i18next: + specifier: ^16.5.4 + version: 16.5.4(i18next@25.8.13(typescript@5.9.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.2) + regex-escape: + specifier: ^3.4.10 + version: 3.4.11 + typescript: + specifier: ^5.9.2 + version: 5.9.2 + vite: + specifier: ^6.3.5 + version: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + vitest: + specifier: ^4.1.6 + version: 4.1.6(@types/node@20.19.1)(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)) + wait-on: + specifier: ^6.0.1 + version: 6.0.1 + zod: + specifier: ^4.0.14 + version: 4.0.14 + zustand: + specifier: ^5.0.7 + version: 5.0.7(@types/react@19.2.13)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + +packages: + + 7zip-bin@5.2.0: + resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.27.3': + resolution: {integrity: sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.27.4': + resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.27.3': + resolution: {integrity: sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.5': + resolution: {integrity: sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.4': + resolution: {integrity: sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.4': + resolution: {integrity: sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.27.1': + resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.5': + resolution: {integrity: sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.27.1': + resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.4': + resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.3': + resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@clack/core@0.5.0': + resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} + + '@clack/prompts@0.11.0': + resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + + '@csstools/postcss-cascade-layers@5.0.2': + resolution: {integrity: sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@develar/schema-utils@2.6.5': + resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} + engines: {node: '>= 8.9.0'} + + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + + '@electron-forge/cli@6.4.2': + resolution: {integrity: sha512-bM6YVTV0uUEpIL1jkpARlSm4Li26XZn+avC/lyTdpPqnd65T/oXZNkrAD+2Jb0RlgplOaM21qWm7ybtvKDGDyA==} + engines: {node: '>= 14.17.5'} + hasBin: true + + '@electron-forge/core-utils@6.4.2': + resolution: {integrity: sha512-CjB3aakmRsXAMMDYc8PxNTMf4FdI29y4PErfv7eCXlL5oo3JW0VSKZIV7R8/Po0S0got85q2kmhZgCKuxL1BNA==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/core@6.4.2': + resolution: {integrity: sha512-VtrFZ1Q7NG1ov0jJO/tUvUiYdWZ0Y31xw762is/jfpRPD6V/soOpwJJAoWoPK9TZVkTm2pkS8S5LikCMbNCLxw==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/maker-base@6.4.2': + resolution: {integrity: sha512-zW3GH+LqDK9nxQmQEFkJPR8RqiX0lVk6a4mXll3ngujN1fPevO4ivUAWmaEVeC1dH/hXbN7s9m0S6a37MigftQ==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/maker-deb@6.4.2': + resolution: {integrity: sha512-tlV8ffivgBP94vtYXgAeXgzeKCaRyLuWH9LT8PQW1QrYbAFpCMmuwk/zFaJkyMklImCWmDFTPYMEqdEJGd7Npg==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/maker-rpm@6.4.2': + resolution: {integrity: sha512-+hfbY5pYbAer0y07OtOzVgVBHoTRmemqqZ//T0mKJpyK2ThHKGTvyW8FFlr5jlQs5LoDCM2WHKE8oGtRhivsMg==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/maker-squirrel@6.4.2': + resolution: {integrity: sha512-ukK3RcFaBrQXUzR52PsHxfwDq5XKSnj6A1kkXiyHWqgj+HIU97prBScBb5JRtasPvYN+nDdQO2vlInsLaqcx9Q==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/maker-zip@6.4.2': + resolution: {integrity: sha512-k2nfhhnxcYbUS7rCKCisuqEalxtH9l73+lrtfL0aQZiE/BLbDXyNckDIDOPvX0tBEg62nVzUdJonZwOhZVvAMw==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/plugin-base@6.4.2': + resolution: {integrity: sha512-g6AAtQ7fZ94djBmwcnWasQ8xgaNVNjgaQ00GLK0NkmQ7n0PNbsnlMDuw9vdfTiL6WaLg5nxNSYc9bFJP/rtyeA==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/publisher-base@6.4.2': + resolution: {integrity: sha512-Tnf9O8MFzdT1gsb5EDDaQUoslt7gUuUywtsr+lT/fpBlBQbei2fvioTwvZ1Q1cmsKnld7XhRh6unfgdWLTZzgw==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/shared-types@6.4.2': + resolution: {integrity: sha512-DKOUMsdTXZIq8XiqY0Hi3C+dam/JKUnvfBjwcUeyZqPdgEE1qry8xZmmjorXuLrRf1Jq8rhxYGQInSK4af0QYw==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/template-base@6.4.2': + resolution: {integrity: sha512-vsQh+64Fr2Vxg6k8DAahWq4MAdB2F2qTig+LgIJENv8ksbzC1YIq05SBAS/g2674cdr7WdwyukMy2rgxe3rhnQ==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/template-vite-typescript@6.4.2': + resolution: {integrity: sha512-h3pn6onvC/nLglmJuelYU82Qzrh0l6MqvbBGoT39bbDoRLIqmlhWTWppHgDJVXAGrSoH+9BEpptipeBQWirFwg==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@electron-forge/template-vite@6.4.2': + resolution: {integrity: sha512-NX7jHRblBmIqufMbqWgpI/VnpgF/qMSTq9ZPmDSXamBhid336MC6+DoWzDpXceQZEp0m/jpMLR04ynr8O4jGlg==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@electron-forge/template-webpack-typescript@6.4.2': + resolution: {integrity: sha512-MPAZQ4v6piCED7NT1LTVQf61o6Eg/laNoKbhbrFBSH1i20OUwbtV2MLj6Op292ynI9+1qdHKmFgctr6qPTCAQw==} + engines: {node: '>= 14.17.5'} + + '@electron-forge/template-webpack@6.4.2': + resolution: {integrity: sha512-9QYr/td4cmnGOj8UF25W6An/eI+JXj9T/b+KFybL3cQ87H1yrQOn2T84Bm5/JaB4SPdIu4FdKRjqwR7C7R0g2w==} + engines: {node: '>= 14.17.5'} + + '@electron-toolkit/preload@3.0.2': + resolution: {integrity: sha512-TWWPToXd8qPRfSXwzf5KVhpXMfONaUuRAZJHsKthKgZR/+LqX1dZVSSClQ8OTAEduvLGdecljCsoT2jSshfoUg==} + peerDependencies: + electron: '>=13.0.0' + + '@electron-toolkit/tsconfig@1.0.1': + resolution: {integrity: sha512-M0Mol3odspvtCuheyujLNAW7bXq7KFNYVMRtpjFa4ZfES4MuklXBC7Nli/omvc+PRKlrklgAGx3l4VakjNo8jg==} + peerDependencies: + '@types/node': '*' + + '@electron-toolkit/utils@4.0.0': + resolution: {integrity: sha512-qXSntwEzluSzKl4z5yFNBknmPGjPa3zFhE4mp9+h0cgokY5ornAeP+CJQDBhKsL1S58aOQfcwkD3NwLZCl+64g==} + peerDependencies: + electron: '>=13.0.0' + + '@electron/asar@3.2.18': + resolution: {integrity: sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/fuses@1.8.0': + resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==} + hasBin: true + + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + + '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2': + resolution: {gitHosted: true, integrity: sha512-MXgzlTDEEndJB3TBbvd5uFQO/8gaINo1Hfen8vef5rq/VHVPeB63uuv/uO5+8GFsAJ/rauu6XB79S6K4+aXc+w==, tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2} + version: 10.2.0-electron.1 + engines: {node: '>=12.13.0'} + hasBin: true + + '@electron/notarize@1.2.4': + resolution: {integrity: sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==} + engines: {node: '>= 10.0.0'} + + '@electron/notarize@2.5.0': + resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.3.1': + resolution: {integrity: sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/osx-sign@1.3.3': + resolution: {integrity: sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/rebuild@3.7.0': + resolution: {integrity: sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==} + engines: {node: '>=12.13.0'} + hasBin: true + + '@electron/rebuild@3.7.2': + resolution: {integrity: sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==} + engines: {node: '>=12.13.0'} + hasBin: true + + '@electron/universal@1.5.1': + resolution: {integrity: sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==} + engines: {node: '>=8.6'} + + '@electron/universal@2.0.1': + resolution: {integrity: sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==} + engines: {node: '>=16.4'} + + '@electron/windows-sign@1.2.2': + resolution: {integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==} + engines: {node: '>=14.14'} + hasBin: true + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fast-csv/format@4.3.5': + resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} + + '@fast-csv/parse@4.3.6': + resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/react-dom@2.1.7': + resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@malept/cross-spawn-promise@1.1.1': + resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==} + engines: {node: '>= 10'} + + '@malept/cross-spawn-promise@2.0.0': + resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} + engines: {node: '>= 12.13.0'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + + '@modelcontextprotocol/sdk@1.25.3': + resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/fs@2.1.2': + resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + '@npmcli/move-file@2.0.1': + resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This functionality has been moved to @npmcli/fs + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.19.1': + resolution: {integrity: sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + resolution: {integrity: sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.19.1': + resolution: {integrity: sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + resolution: {integrity: sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + resolution: {integrity: sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + resolution: {integrity: sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.19.1': + resolution: {integrity: sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + resolution: {integrity: sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + resolution: {integrity: sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + resolution: {integrity: sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==} + cpu: [x64] + os: [win32] + + '@pandacss/config@1.8.1': + resolution: {integrity: sha512-SvFvfDav//c2lbk9+IPRGCLGhUd1G8z/Nxs0CwZNLXpI0xTNy0ISGMV1ZKkqBFbmH95e+/5oZXnnM4rAS1LO0g==} + + '@pandacss/core@1.8.1': + resolution: {integrity: sha512-8axy9BApvc/oX5/K2/qX23HX1cHvp3MI/fm+CDx31v9zH3MIy4X49MMDeEU9o6L2Q6p6yinPgQZi7gtOqw+mfQ==} + + '@pandacss/dev@1.8.1': + resolution: {integrity: sha512-rHE7JEDNbqLVpybgT1Ff5R1loYxX3sF/a729EVvsBUtcd/xSHZyjhg8vnB5hXajaWt9Ro+u7chAB6+pdqd1Jog==} + hasBin: true + + '@pandacss/extractor@1.8.1': + resolution: {integrity: sha512-fgztRMZq1f/M8zdCku9ads+Wslje9/stypjN/qLjV436NI2LWktDmWYGTiEyHZTR5yEfI2LtJCa1b1BvZcU07g==} + + '@pandacss/generator@1.8.1': + resolution: {integrity: sha512-+20L+KeSbd2tqalH51M8pAMcJ6HM9rgnKXOyNAN/kx78WXec4jCZN9z9CSCFbrRxH/qpcIx20oAXZBbNghHSyQ==} + + '@pandacss/is-valid-prop@1.8.1': + resolution: {integrity: sha512-gf2HTBCOboc65Jlb9swAjbffXSIv+A4vzSQ9iHyTCDLMcXTHYjPOQNliI36WkuQgR0pNXggBbQXGNaT9wKcrAw==} + + '@pandacss/logger@1.8.1': + resolution: {integrity: sha512-u++vUM+roJPPle0PQ1BGl6GkxwusfzEbKtX35L/5GtF6+CQ1ZfbD07HlMjEzmyEQ+KeQ8joR1Ubh/YGrZdEWrQ==} + + '@pandacss/mcp@1.8.1': + resolution: {integrity: sha512-z3VOz9XVvpdQ0bf6a1REGZToFFcwU/0jyK699fRD3jzMg+OJ24j/T8RZvlFoDPCWcifjXI7eUCSnUJ1TVqXfcQ==} + + '@pandacss/node@1.8.1': + resolution: {integrity: sha512-59Cb55sFL+TO2Sd/s2SxSvh0nY/ouBsJQYqRvUkgLL56FDH28euCYvj8Tt6H9rISKl6xhCpixF7svA8YjTOuXg==} + + '@pandacss/parser@1.8.1': + resolution: {integrity: sha512-dyWX+P4zouBuvqbWqySKDWnrIeq/C2jdeS/GjswQny3pGQHwm1np9yqHfNEtKlaIouXYACbjP1cXl/tEgiKXHA==} + + '@pandacss/postcss@1.8.1': + resolution: {integrity: sha512-xopXqr/Bh/KTewxgsYe2muHIsuLqrZ64RH1cW4KRY5TOI+JaL7M4qAPTqa/fVMh+LV9k0woTd423g7Q3d2oivA==} + + '@pandacss/preset-base@1.8.1': + resolution: {integrity: sha512-NJtVQz4Dn3FuFLci8GvbXVGegKvAkYh7xB5TwJA5mqwRIOqWZ6+U/WoVXu9aR9wj3/EYAhqA5qUJQXrC6dUFmA==} + + '@pandacss/preset-panda@1.8.1': + resolution: {integrity: sha512-89QSWcSdhr1ViW6o0YMAQrSCxmY+xsrr0eA6GnNT6FWkjwL1QAtwjezWwODHN26BjlNTLmW8aK39Tepj6Wh2Aw==} + + '@pandacss/reporter@1.8.1': + resolution: {integrity: sha512-aTAObZ8ALxhGIXtHMEJ0KzDx3au1qlwHe60EA4BGGEPfBg25ntM8SaSbxOMhwUcbD5VffzywRoJeqWMaPmg3zw==} + + '@pandacss/shared@1.8.1': + resolution: {integrity: sha512-8U+iqqvb1hmTRHOCkO4q29OMv//szv/wJAQG6SPEuYth7QH+fEvPMl2qsZyDTe1b+pkB1bt7tpvTF4+lR1K8EA==} + + '@pandacss/token-dictionary@1.8.1': + resolution: {integrity: sha512-6uDH/QbxE/sS/QzfaSswxa+hMdDR1U3PUGVD/uCrt3tJ4zTHgR2icZ3vH9GN9k17WFutzcVQaKTKscY3PYn9TA==} + + '@pandacss/types@1.8.1': + resolution: {integrity: sha512-5hNn6PsbmSWdHmfETGTGW+gpFmzf0xdKlzbFHIIKHZK+zicCtHIg6iMahZsf9wvYiBu0LAKwgdKR+kDMya5N8w==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + 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 + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + 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 + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + 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 + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + 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 + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + 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 + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + 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 + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + 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 + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + 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 + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + 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 + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + 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 + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + 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 + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + 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 + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + 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 + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + 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 + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rolldown/pluginutils@1.0.0-beta.9': + resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} + + '@rollup/rollup-android-arm-eabi@4.41.1': + resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.41.1': + resolution: {integrity: sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.41.1': + resolution: {integrity: sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.41.1': + resolution: {integrity: sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.41.1': + resolution: {integrity: sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.41.1': + resolution: {integrity: sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.41.1': + resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.41.1': + resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.41.1': + resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.41.1': + resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.41.1': + resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': + resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.41.1': + resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.41.1': + resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.41.1': + resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.41.1': + resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.41.1': + resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.41.1': + resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.41.1': + resolution: {integrity: sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.41.1': + resolution: {integrity: sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + cpu: [x64] + os: [win32] + + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@stitches/react@1.2.8': + resolution: {integrity: sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==} + peerDependencies: + react: '>= 16.3.0' + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@tanstack/form-core@1.11.0': + resolution: {integrity: sha512-BaEnQ73h4S72AXdjgQowmDBJPHbAvPnWTY2xeY74O7/qfesZKbgPA7DQi5BW1qMv15BkthloJMi9Qljbf5mi7Q==} + + '@tanstack/history@1.139.0': + resolution: {integrity: sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg==} + engines: {node: '>=12'} + + '@tanstack/query-core@5.100.5': + resolution: {integrity: sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==} + + '@tanstack/react-form@1.11.0': + resolution: {integrity: sha512-jTsTVvRaTLWwdhCAKG0mfrH+/43wudGujwDiRRT09VYxWJeJNFoXqepXW26lY0iuq11a0+A/0lmr2TfVqPanfg==} + peerDependencies: + '@tanstack/react-start': ^1.112.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + vinxi: ^0.5.0 + peerDependenciesMeta: + '@tanstack/react-start': + optional: true + vinxi: + optional: true + + '@tanstack/react-query@5.100.5': + resolution: {integrity: sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-router-devtools@1.139.12': + resolution: {integrity: sha512-deMQGaojEJGFio95o0rDT4OhgtwfgrQIBZAGnXhfyC395n94IuE43uvvv7tkfBzWHQwYK0IvZIeyKMavbvAj7Q==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/react-router': ^1.139.12 + '@tanstack/router-core': ^1.139.12 + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + peerDependenciesMeta: + '@tanstack/router-core': + optional: true + + '@tanstack/react-router@1.139.12': + resolution: {integrity: sha512-qrIxb8c6XXih6MERZKKwdnYg0OannsQLJ/s+4/wRqKqGCG+QmvAMvnmNP7bfYLgFKi+KsE27HqUkHaSpZSenwQ==} + engines: {node: '>=12'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.7.0': + resolution: {integrity: sha512-S/Rq17HaGOk+tQHV/yrePMnG1xbsKZIl/VsNWnNXt4XW+tTY8dTlvpJH2ZQ3GRALsusG5K6Q3unAGJ2pd9W/Ng==} + 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 + + '@tanstack/react-store@0.8.0': + resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==} + 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 + + '@tanstack/router-core@1.139.12': + resolution: {integrity: sha512-HCDi4fpnAFeDDogT0C61yd2nJn0FrIyFDhyHG3xJji8emdn8Ni4rfyrN4Av46xKkXTPUGdbsqih45+uuNtunew==} + engines: {node: '>=12'} + + '@tanstack/router-devtools-core@1.139.12': + resolution: {integrity: sha512-VARlT9alLnROnPsZtHrSZsqYksIdBBQ24yGzEper5K1+1e0fzpcKLnMYLK9cwr//uWA2xmQayznvBnwcTmnUlg==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/router-core': ^1.139.12 + csstype: ^3.0.10 + solid-js: '>=1.9.5' + peerDependenciesMeta: + csstype: + optional: true + + '@tanstack/router-generator@1.139.12': + resolution: {integrity: sha512-HGs35aBml+2TVwoynsEc00/9Duw19GeT1fX+JzrY0TKNfMzq/nbjR+xxU8M1w3+gHqfKiITmW70XSZoWkXu9tw==} + engines: {node: '>=12'} + + '@tanstack/router-plugin@1.139.12': + resolution: {integrity: sha512-xX39CcU6GLMaahr6YGNQYRZOQsd1WefgCH99PtY0cxZr9VNAIpJMYPsQY8h/g8A4JI10rHI1tdKxZAvodWjZxw==} + engines: {node: '>=12'} + peerDependencies: + '@rsbuild/core': '>=1.0.2' + '@tanstack/react-router': ^1.139.12 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' + vite-plugin-solid: ^2.11.10 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.139.0': + resolution: {integrity: sha512-jT7D6NimWqoFSkid4vCno8gvTyfL1+NHpgm3es0B2UNhKKRV3LngOGilm1m6v8Qvk/gy6Fh/tvB+s+hBl6GhOg==} + engines: {node: '>=12'} + + '@tanstack/store@0.7.0': + resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==} + + '@tanstack/store@0.8.0': + resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==} + + '@tanstack/virtual-file-routes@1.139.0': + resolution: {integrity: sha512-9PImF1d1tovTUIpjFVa0W7Fwj/MHif7BaaczgJJfbv3sDt1Gh+oW9W9uCw9M3ndEJynnp5ZD/TTs0RGubH5ssg==} + engines: {node: '>=12'} + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@ts-morph/common@0.28.1': + resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@14.18.63': + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + + '@types/node@16.18.126': + resolution: {integrity: sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==} + + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + + '@types/node@20.19.1': + resolution: {integrity: sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==} + + '@types/plist@3.0.5': + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.13': + resolution: {integrity: sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==} + + '@types/regex-escape@3.4.1': + resolution: {integrity: sha512-aQihdLwAzyST0/LTidv76ZKtltWbea61FprvpMHbqUToUpdZrSYUqPb3ToojMW2a29JfoJXBxtO6K5py4o8uwA==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/verror@1.10.11': + resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@vitejs/plugin-react@4.5.0': + resolution: {integrity: sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + + '@vitest/expect@4.1.6': + resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} + + '@vitest/mocker@4.1.6': + resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.6': + resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==} + + '@vitest/runner@4.1.6': + resolution: {integrity: sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==} + + '@vitest/snapshot@4.1.6': + resolution: {integrity: sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==} + + '@vitest/spy@4.1.6': + resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==} + + '@vitest/utils@4.1.6': + resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==} + + '@vue/compiler-core@3.5.25': + resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==} + + '@vue/compiler-dom@3.5.25': + resolution: {integrity: sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==} + + '@vue/compiler-sfc@3.5.25': + resolution: {integrity: sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==} + + '@vue/compiler-ssr@3.5.25': + resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==} + + '@vue/shared@3.5.25': + resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} + + '@xmldom/xmldom@0.8.10': + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@5.0.0: + resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} + engines: {node: '>=12'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + app-builder-bin@5.0.0-alpha.12: + resolution: {integrity: sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==} + + app-builder-lib@26.0.12: + resolution: {integrity: sha512-+/CEPH1fVKf6HowBUs6LcAIoRcjeqgvAeoSE+cl7Y7LndyQ9ViGPYibNk7wmhMHzNgHIuIbw4nWADPO+4mjgWw==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 26.0.12 + electron-builder-squirrel-windows: 26.0.12 + + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + author-regex@1.0.0: + resolution: {integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==} + engines: {node: '>=0.8'} + + axios@0.25.0: + resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==} + + axios@1.13.4: + resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} + + babel-dead-code-elimination@1.0.10: + resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + binary@0.3.0: + resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.25.0: + resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal@1.0.1: + resolution: {integrity: sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==} + engines: {node: '>=0.4'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer-indexof-polyfill@1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffers@0.1.1: + resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} + engines: {node: '>=0.2.0'} + + builder-util-runtime@9.3.1: + resolution: {integrity: sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==} + engines: {node: '>=12.0.0'} + + builder-util@26.0.11: + resolution: {integrity: sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==} + + bundle-n-require@1.1.2: + resolution: {integrity: sha512-bEk2jakVK1ytnZ9R2AAiZEeK/GxPUM8jvcRxHZXifZDMcjkI4EG/GlsJ2YGSVYT9y/p/gA9/0yDY8rCGsSU6Tg==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacache@16.1.3: + resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + + caniuse-lite@1.0.30001720: + resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} + + caniuse-lite@1.0.30001766: + resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chainsaw@0.1.0: + resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cli-truncate@3.1.0: + resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@11.0.0: + resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} + engines: {node: '>=16'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + commander@9.4.1: + resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==} + engines: {node: ^12.20.0 || >=14} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concurrently@7.6.0: + resolution: {integrity: sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==} + engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} + hasBin: true + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + config-file-ts@0.2.8-rc1: + resolution: {integrity: sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@2.0.0: + resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + + crc@3.8.0: + resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + + cross-dirname@0.1.0: + resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn-windows-exe@1.2.0: + resolution: {integrity: sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==} + engines: {node: '>= 10'} + + cross-spawn@6.0.6: + resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} + engines: {node: '>=4.8'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cross-zip@4.0.1: + resolution: {integrity: sha512-n63i0lZ0rvQ6FXiGQ+/JFCKAUyPFhLQYJIqKaa+tSJtfKeULF/IDNDAbdnSIxgS4NTuw2b0+lj8LzfITuq+ZxQ==} + engines: {node: '>=12.10'} + + crosspath@2.0.0: + resolution: {integrity: sha512-ju88BYCQ2uvjO2bR+SsgLSTwTSctU+6Vp2ePbKPgSCZyy4MWZxYsT738DlKVRE5utUjobjPRm1MkTYKJxCmpTA==} + engines: {node: '>=14.9.0'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssnano-utils@5.0.1: + resolution: {integrity: sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-formdata@0.9.0: + resolution: {integrity: sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + + diff@8.0.2: + resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} + engines: {node: '>=0.3.1'} + + dir-compare@3.3.0: + resolution: {integrity: sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==} + + dir-compare@4.2.0: + resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + + dmg-builder@26.0.12: + resolution: {integrity: sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==} + + dmg-license@1.0.11: + resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} + engines: {node: '>=8'} + os: [darwin] + hasBin: true + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder-squirrel-windows@26.0.12: + resolution: {integrity: sha512-kpwXM7c/ayRUbYVErQbsZ0nQZX4aLHQrPEG9C4h9vuJCXylwFH8a7Jgi2VpKIObzCXO7LKHiCw4KdioFLFOgqA==} + + electron-builder@26.0.12: + resolution: {integrity: sha512-cD1kz5g2sgPTMFHjLxfMjUK5JABq3//J4jPswi93tOPFz6btzXYtK5NrDt717NRbukCUDOrrvmYVOWERlqoiXA==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-debug@3.2.0: + resolution: {integrity: sha512-7xZh+LfUvJ52M9rn6N+tPuDw6oRAjxUj9SoxAZfJ0hVCXhZCsdkrSt7TgXOiWiEOBgEV8qwUIO/ScxllsPS7ow==} + + electron-devtools-installer@3.2.1: + resolution: {integrity: sha512-FaCi+oDCOBTw0gJUsuw5dXW32b2Ekh5jO8lI1NRCQigo3azh2VogsIi0eelMVrP1+LkN/bewyH3Xoo1USjO0eQ==} + + electron-installer-common@0.10.4: + resolution: {integrity: sha512-8gMNPXfAqUE5CfXg8RL0vXpLE9HAaPkgLXVoHE3BMUzogMWenf4LmwQ27BdCUrEhkjrKl+igs2IHJibclR3z3Q==} + engines: {node: '>= 10.0.0'} + + electron-installer-debian@3.2.0: + resolution: {integrity: sha512-58ZrlJ1HQY80VucsEIG9tQ//HrTlG6sfofA3nRGr6TmkX661uJyu4cMPPh6kXW+aHdq/7+q25KyQhDrXvRL7jw==} + engines: {node: '>= 10.0.0'} + os: [darwin, linux] + hasBin: true + + electron-installer-redhat@3.4.0: + resolution: {integrity: sha512-gEISr3U32Sgtj+fjxUAlSDo3wyGGq6OBx7rF5UdpIgbnpUvMN4W5uYb0ThpnAZ42VEJh/3aODQXHbFS4f5J3Iw==} + engines: {node: '>= 10.0.0'} + os: [darwin, linux] + hasBin: true + + electron-is-accelerator@0.1.2: + resolution: {integrity: sha512-fLGSAjXZtdn1sbtZxx52+krefmtNuVwnJCV2gNiVt735/ARUboMl8jnNC9fZEqQdlAv2ZrETfmBUsoQci5evJA==} + + electron-is-dev@1.2.0: + resolution: {integrity: sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==} + + electron-localshortcut@3.2.1: + resolution: {integrity: sha512-DWvhKv36GsdXKnaFFhEiK8kZZA+24/yFLgtTwJJHc7AFgDjNRIBJZ/jq62Y/dWv9E4ypYwrVWN2bVrCYw1uv7Q==} + + electron-packager@17.1.2: + resolution: {integrity: sha512-XofXdikjYI7MVBcnXeoOvRR+yFFFHOLs3J7PF5KYQweigtgLshcH4W660PsvHr4lYZ03JBpLyEcUB8DzHZ+BNw==} + engines: {node: '>= 14.17.5'} + deprecated: Please use @electron/packager moving forward. There is no API change, just a package name change + hasBin: true + + electron-publish@26.0.11: + resolution: {integrity: sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A==} + + electron-squirrel-startup@1.0.1: + resolution: {integrity: sha512-sTfFIHGku+7PsHLJ7v0dRcZNkALrV+YEozINTW8X1nM//e5O3L+rfYuvSW00lmGHnYmUjARZulD8F2V8ISI9RA==} + + electron-to-chromium@1.5.161: + resolution: {integrity: sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==} + + electron-to-chromium@1.5.283: + resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==} + + electron-vite@3.1.0: + resolution: {integrity: sha512-M7aAzaRvSl5VO+6KN4neJCYLHLpF/iWo5ztchI/+wMxIieDZQqpbCYfaEHHHPH6eupEzfvZdLYdPdmvGqoVe0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@swc/core': ^1.0.0 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + '@swc/core': + optional: true + + electron-winstaller@5.4.0: + resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} + engines: {node: '>=8.0.0'} + + electron@22.3.27: + resolution: {integrity: sha512-7Rht21vHqj4ZFRnKuZdFqZFsvMBCmDqmjetiMqPtF+TmTBiGne1mnstVXOA/SRGhN2Qy5gY5bznJKpiqogjM8A==} + engines: {node: '>= 12.20.55'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + entities@3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + exceljs@4.4.0: + resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} + engines: {node: '>=8.3.0'} + + execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + + execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.4.1: + resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + engines: {'0': node >=0.6.0} + + fast-csv@4.3.6: + resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==} + engines: {node: '>=10.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.4.5: + resolution: {integrity: sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + filename-reserved-regex@2.0.0: + resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} + engines: {node: '>=4'} + + filenamify@4.3.0: + resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==} + engines: {node: '>=8'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flora-colossus@2.0.0: + resolution: {integrity: sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==} + engines: {node: '>= 12'} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + deprecated: This package is no longer supported. + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + galactus@1.0.0: + resolution: {integrity: sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==} + engines: {node: '>= 12'} + + gar@1.0.4: + resolution: {integrity: sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-folder-size@2.0.1: + resolution: {integrity: sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA==} + hasBin: true + + get-installed-path@2.1.1: + resolution: {integrity: sha512-Qkn9eq6tW5/q9BDVdMpB8tOHljX9OSP0jRC5TRNVA4qRc839t4g8KQaR8t0Uv0EFVL0MlyG7m/ofjEgAROtYsA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-package-info@1.0.0: + resolution: {integrity: sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==} + engines: {node: '>= 4.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@8.0.3: + resolution: {integrity: sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + goober@2.1.18: + resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} + peerDependencies: + csstype: ^3.0.10 + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + + hono@4.11.7: + resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} + engines: {node: '>=16.9.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + husky@8.0.3: + resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} + engines: {node: '>=14'} + hasBin: true + + i18next@25.8.13: + resolution: {integrity: sha512-E0vzjBY1yM+nsFrtgkjLhST2NBkirkvOVoQa0MSldhsuZ3jUge7ZNpuwG0Cfc74zwo5ZwRzg3uOgT+McBn32iA==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + + iconv-corefoundation@1.1.7: + resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} + engines: {node: ^8.11.2 || >=10} + os: [darwin] + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@3.0.1: + resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.4: + resolution: {integrity: sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==} + engines: {node: '>= 18.0.0'} + + isbot@5.1.32: + resolution: {integrity: sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + javascript-stringify@2.1.0: + resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.1.0: + resolution: {integrity: sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + junk@3.1.0: + resolution: {integrity: sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==} + engines: {node: '>=8'} + + keyboardevent-from-electron-accelerator@2.0.0: + resolution: {integrity: sha512-iQcmNA0M4ETMNi0kG/q0h/43wZk7rMeKYrXP7sqKIJbHkTU8Koowgzv+ieR/vWJbOwxx5nDC3UnudZ0aLSu4VA==} + + keyboardevents-areequal@0.2.2: + resolution: {integrity: sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + knip@6.7.0: + resolution: {integrity: sha512-ckL51NDH1YJxnv1kNB0iUdDngB4f/e9Igz8uIqYfmNDoyOFmmk1V0WFv3LQ7/hzC63b2Z9X41gGUE9eOWrZpaA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + linkify-it@4.0.1: + resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} + + lint-staged@13.3.0: + resolution: {integrity: sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ==} + engines: {node: ^16.14.0 || >=18.0.0} + hasBin: true + + listenercount@1.0.1: + resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} + + listr2@5.0.8: + resolution: {integrity: sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==} + engines: {node: ^14.13.1 || >=16.0.0} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + + listr2@6.6.1: + resolution: {integrity: sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg==} + engines: {node: '>=16.0.0'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + + load-json-file@2.0.0: + resolution: {integrity: sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==} + engines: {node: '>=4'} + + locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + + lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + + lodash.isnil@4.0.0: + resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isundefined@3.0.1: + resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + + log-update@5.0.1: + resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + look-it-up@2.1.0: + resolution: {integrity: sha512-nMoGWW2HurtuJf6XAL56FWTDCWLOTSsanrgwOyaR5Y4e3zfG5N/0cU5xWZSEU3tBxhQugRbV1xL9jb+ug7yZww==} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-fetch-happen@10.2.1: + resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + map-age-cleaner@0.1.3: + resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} + engines: {node: '>=6'} + + markdown-it@13.0.1: + resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==} + hasBin: true + + markdownlint-cli@0.32.2: + resolution: {integrity: sha512-xmJT1rGueUgT4yGNwk6D0oqQr90UJ7nMyakXtqjgswAkEhYYqjHew9RY8wDbOmh2R270IWjuKSeZzHDEGPAUkQ==} + engines: {node: '>=14'} + hasBin: true + + markdownlint-rule-helpers@0.17.2: + resolution: {integrity: sha512-XaeoW2NYSlWxMCZM2B3H7YTG6nlaLfkEZWMBhr4hSPlq9MuY2sy83+Xr89jXOqZMZYjvi5nBCGoFh7hHoPKZmA==} + engines: {node: '>=12'} + + markdownlint@0.26.2: + resolution: {integrity: sha512-2Am42YX2Ex5SQhRq35HxYWDfz1NLEOZWWN25nqd2h3AHRKsGRE+Qg1gt1++exW792eXTrR4jCNHfShfWk9Nz8w==} + engines: {node: '>=14'} + + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + mem@4.3.0: + resolution: {integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==} + engines: {node: '>=6'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + microdiff@1.5.0: + resolution: {integrity: sha512-Drq+/THMvDdzRYrK0oxJmOKiC24ayUV8ahrt8l3oRK51PWt6gdtrIGrlIH3pT/lFh1z93FbAcidtsHcWbnRz8Q==} + + micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@2.1.2: + resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + node-abi@3.75.0: + resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} + engines: {node: '>=10'} + + node-addon-api@1.7.2: + resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + + node-api-version@0.2.1: + resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + + node-eval@2.0.0: + resolution: {integrity: sha512-Ap+L9HznXAVeJj3TJ1op6M6bg5xtTq8L5CU/PJxtkhea/DrIxdTknGKIECKd/v/Lgql95iuMAYvIzBNd0pmcMg==} + engines: {node: '>= 4'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + nodemon@2.0.22: + resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==} + engines: {node: '>=8.10.0'} + hasBin: true + + nopt@6.0.0: + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object-path@0.11.8: + resolution: {integrity: sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==} + engines: {node: '>= 10.12.0'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + outdent@0.8.0: + resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} + + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.19.1: + resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-defer@1.0.0: + resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} + engines: {node: '>=4'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-is-promise@2.1.0: + resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} + engines: {node: '>=6'} + + p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + + p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parse-author@2.0.0: + resolution: {integrity: sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==} + engines: {node: '>=0.10.0'} + + parse-json@2.2.0: + resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==} + engines: {node: '>=0.10.0'} + + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + path-type@2.0.0: + resolution: {integrity: sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==} + engines: {node: '>=4'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pe-library@0.4.1: + resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} + engines: {node: '>=12', npm: '>=6'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + phosphor-react@1.4.1: + resolution: {integrity: sha512-gO5j7U0xZrdglTAYDYPACU4xDOFBTJmptrrB/GeR+tHhCZF3nUMyGmV/0hnloKjuTrOmpSFlbfOY78H39rgjUQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss-discard-duplicates@7.0.2: + resolution: {integrity: sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-discard-empty@7.0.1: + resolution: {integrity: sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-merge-rules@7.0.7: + resolution: {integrity: sha512-njWJrd/Ms6XViwowaaCc+/vqhPG3SmXn725AGrnl+BgTuRPEacjiLEaGq16J6XirMJbtKkTwnt67SS+e2WGoew==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-minify-selectors@7.0.5: + resolution: {integrity: sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-nested@7.0.2: + resolution: {integrity: sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-normalize-whitespace@7.0.1: + resolution: {integrity: sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.4: + resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postject@1.0.0-alpha.6: + resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} + engines: {node: '>=14.0.0'} + hasBin: true + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + + prettier@3.7.3: + resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==} + engines: {node: '>=14'} + hasBin: true + + proc-log@2.0.1: + resolution: {integrity: sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rcedit@3.1.0: + resolution: {integrity: sha512-WRlRdY1qZbu1L11DklT07KuHfRk42l0NFFJdaExELEu4fEQ982bP5Z6OWGPj/wLLIuKRQDCxZJGAwoFsxhZhNA==} + engines: {node: '>= 10.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-hot-toast@2.6.0: + resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + react-i18next@16.5.4: + resolution: {integrity: sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==} + peerDependencies: + i18next: '>= 25.6.2' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + 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 + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + 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 + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + read-binary-file-arch@1.0.6: + resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} + hasBin: true + + read-pkg-up@2.0.0: + resolution: {integrity: sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==} + engines: {node: '>=4'} + + read-pkg@2.0.0: + resolution: {integrity: sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==} + engines: {node: '>=4'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + regex-escape@3.4.11: + resolution: {integrity: sha512-051l4Hl/0HoJwTvNztrWVjoxLiseSfCrDgWqwR1cnGM/nyQSeIjmvti5zZ7HzOmsXDPaJ2k0iFxQ6/WNpJD5wQ==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resedit@1.7.2: + resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} + engines: {node: '>=12', npm: '>=6'} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + + resolve-package@1.0.1: + resolution: {integrity: sha512-rzB7NnQpOkPHBWFPP3prUMqOP6yg3HkRGgcvR+lDyvyHoY3fZLFLYDkPXh78SPVBAE6VTCk/V+j8we4djg6o4g==} + engines: {node: '>=4', npm: '>=2'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + + rollup@4.41.1: + resolution: {integrity: sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-con@1.2.12: + resolution: {integrity: sha512-5257ILMYIF4RztL9uoZ7V9Q97zHtNHn5bN3NobeAnzB1P3ASLgg8qocM2u+R18ttp+VEM78N2LK8XcNVtnSRrg==} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-filename@1.6.3: + resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.0.0: + resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + seroval-plugins@1.3.3: + resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval-plugins@1.4.0: + resolution: {integrity: sha512-zir1aWzoiax6pbBVjoYVd0O1QQXgIL3eVGBMsBsNmM8Ukq90yGaWlfx0AB9dTS8GPqrOrbXn79vmItCUP9U3BQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.3.2: + resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} + engines: {node: '>=10'} + + seroval@1.4.0: + resolution: {integrity: sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==} + engines: {node: '>=10'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.2: + resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-update-notifier@1.1.0: + resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} + engines: {node: '>=8.10.0'} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + + socks-proxy-agent@7.0.0: + resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} + engines: {node: '>= 10'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + solid-js@1.9.10: + resolution: {integrity: sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.21: + resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + ssri@9.0.1: + resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + strip-outer@1.0.1: + resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} + engines: {node: '>=0.10.0'} + + sudo-prompt@9.2.1: + resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + + temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tiny-async-pool@1.3.0: + resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} + + tiny-each-async@2.0.3: + resolution: {integrity: sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + traverse@0.3.9: + resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trim-repeated@1.0.0: + resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} + engines: {node: '>=0.10.0'} + + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + + ts-evaluator@1.2.0: + resolution: {integrity: sha512-ncSGek1p92bj2ifB7s9UBgryHCkU9vwC5d+Lplt12gT9DH+e41X8dMoHRQjIMeAvyG7j9dEnuHmwgOtuRIQL+Q==} + engines: {node: '>=14.19.0'} + peerDependencies: + jsdom: '>=14.x || >=15.x || >=16.x || >=17.x || >=18.x || >=19.x || >=20.x || >=21.x || >=22.x' + typescript: '>=3.2.x || >= 4.x || >= 5.x' + peerDependenciesMeta: + jsdom: + optional: true + + ts-morph@27.0.2: + resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} + + ts-pattern@5.9.0: + resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + + unbash@3.0.0: + resolution: {integrity: sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA==} + engines: {node: '>=14'} + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unique-filename@2.0.1: + resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + unique-slug@3.0.0: + resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + unzip-crx-3@0.2.0: + resolution: {integrity: sha512-0+JiUq/z7faJ6oifVB5nSwt589v1KCduqIJupNVDoWSXZtWDmjDGO3RAEOvwJ07w90aoXoP4enKsR7ecMrJtWQ==} + + unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + 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 + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + 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 + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + username@5.1.0: + resolution: {integrity: sha512-PCKbdWw85JsYMvmCv5GH3kXmM66rCd9m1hBEDutPNv94b/pqCMT4NtcKyeWYvLFiE8b+ha1Jdl8XAaUdPn5QTg==} + engines: {node: '>=8'} + + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + verror@1.10.1: + resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + engines: {node: '>=0.6.0'} + + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vite@7.2.4: + resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.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 + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.6: + resolution: {integrity: sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + 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 + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + wait-on@6.0.1: + resolution: {integrity: sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==} + engines: {node: '>=10.0.0'} + hasBin: true + + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrapjs@5.1.1: + resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} + engines: {node: '>=12.17'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaku@0.16.7: + resolution: {integrity: sha512-Syu3IB3rZvKvYk7yTiyl1bo/jiEFaaStrgv1V2TIJTqYPStSMQVO8EQjg/z+DRzLq/4LIIharNT3iH1hylEIRw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@2.3.1: + resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} + engines: {node: '>= 14'} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yarn-or-npm@3.0.1: + resolution: {integrity: sha512-fTiQP6WbDAh5QZAVdbMQkecZoahnbOjClTQhzv74WX5h2Uaidj1isf9FDes11TKtsZ0/ZVfZsqZ+O3x6aLERHQ==} + engines: {node: '>=8.6.0'} + hasBin: true + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.0.14: + resolution: {integrity: sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zustand@5.0.7: + resolution: {integrity: sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==} + 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 + +snapshots: + + 7zip-bin@5.2.0: {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.27.3': {} + + '@babel/core@7.27.4': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helpers': 7.27.4 + '@babel/parser': 7.27.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.3 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.3': + dependencies: + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.5 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.27.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.5 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.27.4 + '@babel/types': 7.27.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.27.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.5 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.3 + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.27.4': + dependencies: + '@babel/types': 7.27.3 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.27.1': {} + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + + '@babel/traverse@7.27.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.27.3 + '@babel/parser': 7.27.4 + '@babel/template': 7.27.2 + '@babel/types': 7.27.3 + debug: 4.4.1 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.27.3': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@clack/core@0.5.0': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.11.0': + dependencies: + '@clack/core': 0.5.0 + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.6)': + dependencies: + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1) + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@develar/schema-utils@2.6.5': + dependencies: + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + '@dnd-kit/accessibility@3.1.1(react@19.2.4)': + dependencies: + react: 19.2.4 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.2.4) + '@dnd-kit/utilities': 3.2.2(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@dnd-kit/utilities': 3.2.2(react@19.2.4) + react: 19.2.4 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.2.4)': + dependencies: + react: 19.2.4 + tslib: 2.8.1 + + '@electron-forge/cli@6.4.2(encoding@0.1.13)': + dependencies: + '@electron-forge/core': 6.4.2(encoding@0.1.13) + '@electron-forge/shared-types': 6.4.2 + '@electron/get': 2.0.3 + chalk: 4.1.2 + commander: 4.1.1 + debug: 4.4.1 + fs-extra: 10.1.0 + listr2: 5.0.8 + semver: 7.7.2 + transitivePeerDependencies: + - bluebird + - encoding + - enquirer + - supports-color + + '@electron-forge/core-utils@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@electron/rebuild': 3.7.2 + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.1 + find-up: 5.0.0 + fs-extra: 10.1.0 + log-symbols: 4.1.0 + semver: 7.7.2 + yarn-or-npm: 3.0.1 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/core@6.4.2(encoding@0.1.13)': + dependencies: + '@electron-forge/core-utils': 6.4.2 + '@electron-forge/maker-base': 6.4.2 + '@electron-forge/plugin-base': 6.4.2 + '@electron-forge/publisher-base': 6.4.2 + '@electron-forge/shared-types': 6.4.2 + '@electron-forge/template-base': 6.4.2 + '@electron-forge/template-vite': 6.4.2 + '@electron-forge/template-vite-typescript': 6.4.2 + '@electron-forge/template-webpack': 6.4.2 + '@electron-forge/template-webpack-typescript': 6.4.2 + '@electron/get': 2.0.3 + '@electron/rebuild': 3.7.2 + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.1 + electron-packager: 17.1.2 + fast-glob: 3.3.3 + filenamify: 4.3.0 + find-up: 5.0.0 + fs-extra: 10.1.0 + got: 11.8.6 + interpret: 3.1.1 + listr2: 5.0.8 + lodash: 4.17.21 + log-symbols: 4.1.0 + node-fetch: 2.7.0(encoding@0.1.13) + progress: 2.0.3 + rechoir: 0.8.0 + resolve-package: 1.0.1 + semver: 7.7.2 + source-map-support: 0.5.21 + sudo-prompt: 9.2.1 + username: 5.1.0 + yarn-or-npm: 3.0.1 + transitivePeerDependencies: + - bluebird + - encoding + - enquirer + - supports-color + + '@electron-forge/maker-base@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + fs-extra: 10.1.0 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/maker-deb@6.4.2': + dependencies: + '@electron-forge/maker-base': 6.4.2 + '@electron-forge/shared-types': 6.4.2 + optionalDependencies: + electron-installer-debian: 3.2.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/maker-rpm@6.4.2': + dependencies: + '@electron-forge/maker-base': 6.4.2 + '@electron-forge/shared-types': 6.4.2 + optionalDependencies: + electron-installer-redhat: 3.4.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/maker-squirrel@6.4.2': + dependencies: + '@electron-forge/maker-base': 6.4.2 + '@electron-forge/shared-types': 6.4.2 + fs-extra: 10.1.0 + optionalDependencies: + electron-winstaller: 5.4.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/maker-zip@6.4.2': + dependencies: + '@electron-forge/maker-base': 6.4.2 + '@electron-forge/shared-types': 6.4.2 + cross-zip: 4.0.1 + fs-extra: 10.1.0 + got: 11.8.6 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/plugin-base@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/publisher-base@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/shared-types@6.4.2': + dependencies: + '@electron/rebuild': 3.7.2 + electron-packager: 17.1.2 + listr2: 5.0.8 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/template-base@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.1 + fs-extra: 10.1.0 + username: 5.1.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/template-vite-typescript@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@electron-forge/template-base': 6.4.2 + fs-extra: 10.1.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/template-vite@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@electron-forge/template-base': 6.4.2 + fs-extra: 10.1.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/template-webpack-typescript@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@electron-forge/template-base': 6.4.2 + fs-extra: 10.1.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-forge/template-webpack@6.4.2': + dependencies: + '@electron-forge/shared-types': 6.4.2 + '@electron-forge/template-base': 6.4.2 + fs-extra: 10.1.0 + transitivePeerDependencies: + - bluebird + - enquirer + - supports-color + + '@electron-toolkit/preload@3.0.2(electron@22.3.27)': + dependencies: + electron: 22.3.27 + + '@electron-toolkit/tsconfig@1.0.1(@types/node@20.19.1)': + dependencies: + '@types/node': 20.19.1 + + '@electron-toolkit/utils@4.0.0(electron@22.3.27)': + dependencies: + electron: 22.3.27 + + '@electron/asar@3.2.18': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.2 + + '@electron/asar@3.4.1': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.2 + + '@electron/fuses@1.8.0': + dependencies: + chalk: 4.1.2 + fs-extra: 9.1.0 + minimist: 1.2.8 + + '@electron/get@2.0.3': + dependencies: + debug: 4.4.1 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2': + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + glob: 8.1.0 + graceful-fs: 4.2.11 + make-fetch-happen: 10.2.1 + nopt: 6.0.0 + proc-log: 2.0.1 + semver: 7.7.2 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + + '@electron/notarize@1.2.4': + dependencies: + debug: 4.4.1 + fs-extra: 9.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/notarize@2.5.0': + dependencies: + debug: 4.4.1 + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.1': + dependencies: + compare-version: 0.1.2 + debug: 4.4.1 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.3': + dependencies: + compare-version: 0.1.2 + debug: 4.4.1 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/rebuild@3.7.0': + dependencies: + '@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2 + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.1 + detect-libc: 2.0.4 + fs-extra: 10.1.0 + got: 11.8.6 + node-abi: 3.75.0 + node-api-version: 0.2.1 + ora: 5.4.1 + read-binary-file-arch: 1.0.6 + semver: 7.7.2 + tar: 6.2.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - supports-color + + '@electron/rebuild@3.7.2': + dependencies: + '@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2 + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.1 + detect-libc: 2.0.4 + fs-extra: 10.1.0 + got: 11.8.6 + node-abi: 3.75.0 + node-api-version: 0.2.1 + ora: 5.4.1 + read-binary-file-arch: 1.0.6 + semver: 7.7.2 + tar: 6.2.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - supports-color + + '@electron/universal@1.5.1': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 1.1.1 + debug: 4.4.1 + dir-compare: 3.3.0 + fs-extra: 9.1.0 + minimatch: 3.1.2 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/universal@2.0.1': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.1 + dir-compare: 4.2.0 + fs-extra: 11.3.0 + minimatch: 9.0.5 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/windows-sign@1.2.2': + dependencies: + cross-dirname: 0.1.0 + debug: 4.4.1 + fs-extra: 11.3.0 + minimist: 1.2.8 + postject: 1.0.0-alpha.6 + transitivePeerDependencies: + - supports-color + optional: true + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + + '@fast-csv/format@4.3.5': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.isboolean: 3.0.3 + lodash.isequal: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + + '@fast-csv/parse@4.3.6': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.groupby: 4.6.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + lodash.isundefined: 3.0.1 + lodash.uniq: 4.5.0 + + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.7.5 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@floating-ui/utils@0.2.10': {} + + '@gar/promisify@1.1.3': {} + + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@hono/node-server@1.19.9(hono@4.11.7)': + dependencies: + hono: 4.11.7 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@malept/cross-spawn-promise@1.1.1': + dependencies: + cross-spawn: 7.0.6 + + '@malept/cross-spawn-promise@2.0.0': + dependencies: + cross-spawn: 7.0.6 + + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.4.1 + fs-extra: 9.1.0 + lodash: 4.17.21 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + + '@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.0.14)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.7) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.0.14 + zod-to-json-schema: 3.25.1(zod@4.0.14) + transitivePeerDependencies: + - hono + - supports-color + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@npmcli/fs@2.1.2': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.7.2 + + '@npmcli/move-file@2.0.1': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-project/types@0.127.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + optional: true + + '@oxc-resolver/binding-android-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + optional: true + + '@pandacss/config@1.8.1': + dependencies: + '@pandacss/logger': 1.8.1 + '@pandacss/preset-base': 1.8.1 + '@pandacss/preset-panda': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/types': 1.8.1 + bundle-n-require: 1.1.2 + escalade: 3.2.0 + microdiff: 1.5.0 + typescript: 5.9.3 + + '@pandacss/core@1.8.1': + dependencies: + '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.6) + '@pandacss/is-valid-prop': 1.8.1 + '@pandacss/logger': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/token-dictionary': 1.8.1 + '@pandacss/types': 1.8.1 + browserslist: 4.28.0 + hookable: 5.5.3 + lightningcss: 1.30.2 + lodash.merge: 4.6.2 + outdent: 0.8.0 + postcss: 8.5.6 + postcss-discard-duplicates: 7.0.2(postcss@8.5.6) + postcss-discard-empty: 7.0.1(postcss@8.5.6) + postcss-merge-rules: 7.0.7(postcss@8.5.6) + postcss-minify-selectors: 7.0.5(postcss@8.5.6) + postcss-nested: 7.0.2(postcss@8.5.6) + postcss-normalize-whitespace: 7.0.1(postcss@8.5.6) + postcss-selector-parser: 7.1.1 + ts-pattern: 5.9.0 + + '@pandacss/dev@1.8.1(hono@4.11.7)(typescript@5.9.2)': + dependencies: + '@clack/prompts': 0.11.0 + '@pandacss/config': 1.8.1 + '@pandacss/logger': 1.8.1 + '@pandacss/mcp': 1.8.1(hono@4.11.7)(typescript@5.9.2) + '@pandacss/node': 1.8.1(typescript@5.9.2) + '@pandacss/postcss': 1.8.1(typescript@5.9.2) + '@pandacss/preset-base': 1.8.1 + '@pandacss/preset-panda': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/token-dictionary': 1.8.1 + '@pandacss/types': 1.8.1 + cac: 6.7.14 + transitivePeerDependencies: + - '@cfworker/json-schema' + - hono + - jsdom + - supports-color + - typescript + + '@pandacss/extractor@1.8.1(typescript@5.9.2)': + dependencies: + '@pandacss/shared': 1.8.1 + ts-evaluator: 1.2.0(typescript@5.9.2) + ts-morph: 27.0.2 + transitivePeerDependencies: + - jsdom + - typescript + + '@pandacss/generator@1.8.1': + dependencies: + '@pandacss/core': 1.8.1 + '@pandacss/is-valid-prop': 1.8.1 + '@pandacss/logger': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/token-dictionary': 1.8.1 + '@pandacss/types': 1.8.1 + javascript-stringify: 2.1.0 + outdent: 0.8.0 + pluralize: 8.0.0 + postcss: 8.5.6 + ts-pattern: 5.9.0 + + '@pandacss/is-valid-prop@1.8.1': {} + + '@pandacss/logger@1.8.1': + dependencies: + '@pandacss/types': 1.8.1 + kleur: 4.1.5 + + '@pandacss/mcp@1.8.1(hono@4.11.7)(typescript@5.9.2)': + dependencies: + '@clack/prompts': 0.11.0 + '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.7)(zod@4.0.14) + '@pandacss/logger': 1.8.1 + '@pandacss/node': 1.8.1(typescript@5.9.2) + '@pandacss/token-dictionary': 1.8.1 + '@pandacss/types': 1.8.1 + zod: 4.0.14 + transitivePeerDependencies: + - '@cfworker/json-schema' + - hono + - jsdom + - supports-color + - typescript + + '@pandacss/node@1.8.1(typescript@5.9.2)': + dependencies: + '@pandacss/config': 1.8.1 + '@pandacss/core': 1.8.1 + '@pandacss/generator': 1.8.1 + '@pandacss/logger': 1.8.1 + '@pandacss/parser': 1.8.1(typescript@5.9.2) + '@pandacss/reporter': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/token-dictionary': 1.8.1 + '@pandacss/types': 1.8.1 + browserslist: 4.28.0 + chokidar: 4.0.3 + fast-glob: 3.3.3 + fs-extra: 11.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + lodash.merge: 4.6.2 + look-it-up: 2.1.0 + outdent: 0.8.0 + p-limit: 5.0.0 + package-manager-detector: 1.6.0 + perfect-debounce: 1.0.0 + picomatch: 4.0.3 + pkg-types: 2.3.0 + pluralize: 8.0.0 + postcss: 8.5.6 + prettier: 3.2.5 + ts-morph: 27.0.2 + ts-pattern: 5.9.0 + tsconfck: 3.1.6(typescript@5.9.2) + transitivePeerDependencies: + - jsdom + - typescript + + '@pandacss/parser@1.8.1(typescript@5.9.2)': + dependencies: + '@pandacss/config': 1.8.1 + '@pandacss/core': 1.8.1 + '@pandacss/extractor': 1.8.1(typescript@5.9.2) + '@pandacss/logger': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/types': 1.8.1 + '@vue/compiler-sfc': 3.5.25 + magic-string: 0.30.21 + ts-morph: 27.0.2 + ts-pattern: 5.9.0 + transitivePeerDependencies: + - jsdom + - typescript + + '@pandacss/postcss@1.8.1(typescript@5.9.2)': + dependencies: + '@pandacss/node': 1.8.1(typescript@5.9.2) + postcss: 8.5.6 + transitivePeerDependencies: + - jsdom + - typescript + + '@pandacss/preset-base@1.8.1': + dependencies: + '@pandacss/types': 1.8.1 + + '@pandacss/preset-panda@1.8.1': + dependencies: + '@pandacss/types': 1.8.1 + + '@pandacss/reporter@1.8.1': + dependencies: + '@pandacss/core': 1.8.1 + '@pandacss/generator': 1.8.1 + '@pandacss/logger': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/types': 1.8.1 + table: 6.9.0 + wordwrapjs: 5.1.1 + + '@pandacss/shared@1.8.1': {} + + '@pandacss/token-dictionary@1.8.1': + dependencies: + '@pandacss/logger': 1.8.1 + '@pandacss/shared': 1.8.1 + '@pandacss/types': 1.8.1 + picomatch: 4.0.3 + ts-pattern: 5.9.0 + + '@pandacss/types@1.8.1': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.13)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.13)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/rect': 1.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.13)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.13)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.13)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/rect@1.1.1': {} + + '@rolldown/pluginutils@1.0.0-beta.9': {} + + '@rollup/rollup-android-arm-eabi@4.41.1': + optional: true + + '@rollup/rollup-android-arm-eabi@4.53.3': + optional: true + + '@rollup/rollup-android-arm64@4.41.1': + optional: true + + '@rollup/rollup-android-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.41.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-x64@4.41.1': + optional: true + + '@rollup/rollup-darwin-x64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.41.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.41.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.41.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.41.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.41.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.41.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.41.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.41.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.41.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.41.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.41.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.3': + optional: true + + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + + '@sindresorhus/is@4.6.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@stitches/react@1.2.8(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@tanstack/form-core@1.11.0': + dependencies: + '@tanstack/store': 0.7.0 + + '@tanstack/history@1.139.0': {} + + '@tanstack/query-core@5.100.5': {} + + '@tanstack/react-form@1.11.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/form-core': 1.11.0 + '@tanstack/react-store': 0.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + decode-formdata: 0.9.0 + devalue: 5.1.1 + react: 19.2.4 + transitivePeerDependencies: + - react-dom + + '@tanstack/react-query@5.100.5(react@19.2.4)': + dependencies: + '@tanstack/query-core': 5.100.5 + react: 19.2.4 + + '@tanstack/react-router-devtools@1.139.12(@tanstack/react-router@1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.139.12)(@types/node@20.19.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.3)': + dependencies: + '@tanstack/react-router': 1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-devtools-core': 1.139.12(@tanstack/router-core@1.139.12)(@types/node@20.19.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + vite: 7.2.4(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + optionalDependencies: + '@tanstack/router-core': 1.139.12 + transitivePeerDependencies: + - '@types/node' + - csstype + - jiti + - less + - lightningcss + - sass + - sass-embedded + - solid-js + - stylus + - sugarss + - terser + - tsx + - yaml + + '@tanstack/react-router@1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/history': 1.139.0 + '@tanstack/react-store': 0.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-core': 1.139.12 + isbot: 5.1.32 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/react-store@0.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/store': 0.7.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.5.0(react@19.2.4) + + '@tanstack/react-store@0.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/store': 0.8.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + + '@tanstack/router-core@1.139.12': + dependencies: + '@tanstack/history': 1.139.0 + '@tanstack/store': 0.8.0 + cookie-es: 2.0.0 + seroval: 1.4.0 + seroval-plugins: 1.4.0(seroval@1.4.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/router-devtools-core@1.139.12(@tanstack/router-core@1.139.12)(@types/node@20.19.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.3)': + dependencies: + '@tanstack/router-core': 1.139.12 + clsx: 2.1.1 + goober: 2.1.18(csstype@3.2.3) + solid-js: 1.9.10 + tiny-invariant: 1.3.3 + vite: 7.2.4(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + optionalDependencies: + csstype: 3.2.3 + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + '@tanstack/router-generator@1.139.12': + dependencies: + '@tanstack/router-core': 1.139.12 + '@tanstack/router-utils': 1.139.0 + '@tanstack/virtual-file-routes': 1.139.0 + prettier: 3.7.3 + recast: 0.23.11 + source-map: 0.7.6 + tsx: 4.20.6 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.139.12(@tanstack/react-router@1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@tanstack/router-core': 1.139.12 + '@tanstack/router-generator': 1.139.12 + '@tanstack/router-utils': 1.139.0 + '@tanstack/virtual-file-routes': 1.139.0 + babel-dead-code-elimination: 1.0.10 + chokidar: 3.6.0 + unplugin: 2.3.11 + zod: 3.25.76 + optionalDependencies: + '@tanstack/react-router': 1.139.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + vite: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.139.0': + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + ansis: 4.2.0 + diff: 8.0.2 + pathe: 2.0.3 + tinyglobby: 0.2.15 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.7.0': {} + + '@tanstack/store@0.8.0': {} + + '@tanstack/virtual-file-routes@1.139.0': {} + + '@tootallnate/once@2.0.0': {} + + '@ts-morph/common@0.28.1': + dependencies: + minimatch: 10.0.1 + path-browserify: 1.0.1 + tinyglobby: 0.2.15 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.27.3 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.27.4 + '@babel/types': 7.27.3 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.27.3 + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.0.4 + '@types/keyv': 3.1.4 + '@types/node': 16.18.126 + '@types/responselike': 1.0.3 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.7': {} + + '@types/estree@1.0.8': {} + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 16.18.126 + + '@types/http-cache-semantics@4.0.4': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 16.18.126 + + '@types/ms@2.1.0': {} + + '@types/node@14.18.63': {} + + '@types/node@16.18.126': {} + + '@types/node@17.0.45': {} + + '@types/node@20.19.1': + dependencies: + undici-types: 6.21.0 + + '@types/plist@3.0.5': + dependencies: + '@types/node': 16.18.126 + xmlbuilder: 15.1.1 + optional: true + + '@types/react-dom@19.2.3(@types/react@19.2.13)': + dependencies: + '@types/react': 19.2.13 + + '@types/react@19.2.13': + dependencies: + csstype: 3.2.3 + + '@types/regex-escape@3.4.1': {} + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 16.18.126 + + '@types/verror@1.10.11': + optional: true + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 16.18.126 + optional: true + + '@vitejs/plugin-react@4.5.0(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3))': + dependencies: + '@babel/core': 7.27.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.4) + '@rolldown/pluginutils': 1.0.0-beta.9 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@4.1.6': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.6(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + + '@vitest/pretty-format@4.1.6': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.6': + dependencies: + '@vitest/utils': 4.1.6 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.6': + dependencies: + '@vitest/pretty-format': 4.1.6 + '@vitest/utils': 4.1.6 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.6': {} + + '@vitest/utils@4.1.6': + dependencies: + '@vitest/pretty-format': 4.1.6 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@vue/compiler-core@3.5.25': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.25 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.25': + dependencies: + '@vue/compiler-core': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/compiler-sfc@3.5.25': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.25 + '@vue/compiler-dom': 3.5.25 + '@vue/compiler-ssr': 3.5.25 + '@vue/shared': 3.5.25 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.25': + dependencies: + '@vue/compiler-dom': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/shared@3.5.25': {} + + '@xmldom/xmldom@0.8.10': {} + + abbrev@1.1.1: {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn@8.15.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.3: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@5.0.0: + dependencies: + type-fest: 1.4.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + app-builder-bin@5.0.0-alpha.12: {} + + app-builder-lib@26.0.12(dmg-builder@26.0.12)(electron-builder-squirrel-windows@26.0.12): + dependencies: + '@develar/schema-utils': 2.6.5 + '@electron/asar': 3.2.18 + '@electron/fuses': 1.8.0 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.1 + '@electron/rebuild': 3.7.0 + '@electron/universal': 2.0.1 + '@malept/flatpak-bundler': 0.4.0 + '@types/fs-extra': 9.0.13 + async-exit-hook: 2.0.1 + builder-util: 26.0.11 + builder-util-runtime: 9.3.1 + chromium-pickle-js: 0.2.0 + config-file-ts: 0.2.8-rc1 + debug: 4.4.1 + dmg-builder: 26.0.12(electron-builder-squirrel-windows@26.0.12) + dotenv: 16.5.0 + dotenv-expand: 11.0.7 + ejs: 3.1.10 + electron-builder-squirrel-windows: 26.0.12(dmg-builder@26.0.12) + electron-publish: 26.0.11 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + is-ci: 3.0.1 + isbinaryfile: 5.0.4 + js-yaml: 4.1.0 + json5: 2.2.3 + lazy-val: 1.0.5 + minimatch: 10.0.1 + plist: 3.1.0 + resedit: 1.7.2 + semver: 7.7.2 + tar: 6.2.1 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + transitivePeerDependencies: + - bluebird + - supports-color + + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + assert-plus@1.0.0: + optional: true + + assertion-error@2.0.1: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + astral-regex@2.0.0: {} + + async-exit-hook@2.0.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + author-regex@1.0.0: {} + + axios@0.25.0: + dependencies: + follow-redirects: 1.15.9 + transitivePeerDependencies: + - debug + + axios@1.13.4: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-dead-code-elimination@1.0.10: + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.27.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.9.19: {} + + big-integer@1.6.52: {} + + binary-extensions@2.3.0: {} + + binary@0.3.0: + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird@3.4.7: {} + + bluebird@3.7.2: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + boolean@3.2.0: + optional: true + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.25.0: + dependencies: + caniuse-lite: 1.0.30001720 + electron-to-chromium: 1.5.161 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.0) + + browserslist@4.28.0: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001766 + electron-to-chromium: 1.5.283 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.0) + + buffer-crc32@0.2.13: {} + + buffer-equal@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer-indexof-polyfill@1.0.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffers@0.1.1: {} + + builder-util-runtime@9.3.1: + dependencies: + debug: 4.4.1 + sax: 1.4.1 + transitivePeerDependencies: + - supports-color + + builder-util@26.0.11: + dependencies: + 7zip-bin: 5.2.0 + '@types/debug': 4.1.12 + app-builder-bin: 5.0.0-alpha.12 + builder-util-runtime: 9.3.1 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + fs-extra: 10.1.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-ci: 3.0.1 + js-yaml: 4.1.0 + sanitize-filename: 1.6.3 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + transitivePeerDependencies: + - supports-color + + bundle-n-require@1.1.2: + dependencies: + esbuild: 0.25.5 + node-eval: 2.0.0 + + bytes@3.1.2: {} + + cac@6.7.14: {} + + cacache@16.1.3: + dependencies: + '@npmcli/fs': 2.1.2 + '@npmcli/move-file': 2.0.1 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 8.1.0 + infer-owner: 1.0.4 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 9.0.1 + tar: 6.2.1 + unique-filename: 2.0.1 + transitivePeerDependencies: + - bluebird + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.28.0 + caniuse-lite: 1.0.30001720 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001720: {} + + caniuse-lite@1.0.30001766: {} + + chai@6.2.2: {} + + chainsaw@0.1.0: + dependencies: + traverse: 0.3.9 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.3.0: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@2.0.0: {} + + chromium-pickle-js@0.2.0: {} + + ci-info@3.9.0: {} + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@4.0.0: + dependencies: + restore-cursor: 4.0.0 + + cli-spinners@2.9.2: {} + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + + cli-truncate@3.1.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 5.1.2 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + optional: true + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + clone@1.0.4: {} + + clsx@2.1.1: {} + + code-block-writer@13.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@11.0.0: {} + + commander@4.1.1: {} + + commander@5.1.0: {} + + commander@9.4.1: {} + + commander@9.5.0: + optional: true + + compare-version@0.1.2: {} + + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + concat-map@0.0.1: {} + + concurrently@7.6.0: + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.17.21 + rxjs: 7.8.2 + shell-quote: 1.8.2 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + confbox@0.2.2: {} + + config-file-ts@0.2.8-rc1: + dependencies: + glob: 10.4.5 + typescript: 5.9.2 + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-es@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + core-util-is@1.0.2: + optional: true + + core-util-is@1.0.3: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + + crc@3.8.0: + dependencies: + buffer: 5.7.1 + optional: true + + cross-dirname@0.1.0: + optional: true + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn-windows-exe@1.2.0: + dependencies: + '@malept/cross-spawn-promise': 1.1.1 + is-wsl: 2.2.0 + which: 2.0.2 + + cross-spawn@6.0.6: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cross-zip@4.0.1: {} + + crosspath@2.0.0: + dependencies: + '@types/node': 17.0.45 + + cssesc@3.0.0: {} + + cssnano-utils@5.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + csstype@3.2.3: {} + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.27.1 + + dayjs@1.11.13: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-formdata@0.9.0: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + detect-libc@2.0.4: {} + + detect-node-es@1.1.0: {} + + detect-node@2.1.0: + optional: true + + devalue@5.1.1: {} + + diff@8.0.2: {} + + dir-compare@3.3.0: + dependencies: + buffer-equal: 1.0.1 + minimatch: 3.1.2 + + dir-compare@4.2.0: + dependencies: + minimatch: 3.1.2 + p-limit: 3.1.0 + + dmg-builder@26.0.12(electron-builder-squirrel-windows@26.0.12): + dependencies: + app-builder-lib: 26.0.12(dmg-builder@26.0.12)(electron-builder-squirrel-windows@26.0.12) + builder-util: 26.0.11 + builder-util-runtime: 9.3.1 + fs-extra: 10.1.0 + iconv-lite: 0.6.3 + js-yaml: 4.1.0 + optionalDependencies: + dmg-license: 1.0.11 + transitivePeerDependencies: + - bluebird + - electron-builder-squirrel-windows + - supports-color + + dmg-license@1.0.11: + dependencies: + '@types/plist': 3.0.5 + '@types/verror': 1.10.11 + ajv: 6.12.6 + crc: 3.8.0 + iconv-corefoundation: 1.1.7 + plist: 3.1.0 + smart-buffer: 4.2.0 + verror: 1.10.1 + optional: true + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.5.0 + + dotenv@16.5.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + electron-builder-squirrel-windows@26.0.12(dmg-builder@26.0.12): + dependencies: + app-builder-lib: 26.0.12(dmg-builder@26.0.12)(electron-builder-squirrel-windows@26.0.12) + builder-util: 26.0.11 + electron-winstaller: 5.4.0 + transitivePeerDependencies: + - bluebird + - dmg-builder + - supports-color + + electron-builder@26.0.12(electron-builder-squirrel-windows@26.0.12): + dependencies: + app-builder-lib: 26.0.12(dmg-builder@26.0.12)(electron-builder-squirrel-windows@26.0.12) + builder-util: 26.0.11 + builder-util-runtime: 9.3.1 + chalk: 4.1.2 + dmg-builder: 26.0.12(electron-builder-squirrel-windows@26.0.12) + fs-extra: 10.1.0 + is-ci: 3.0.1 + lazy-val: 1.0.5 + simple-update-notifier: 2.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - electron-builder-squirrel-windows + - supports-color + + electron-debug@3.2.0: + dependencies: + electron-is-dev: 1.2.0 + electron-localshortcut: 3.2.1 + transitivePeerDependencies: + - supports-color + + electron-devtools-installer@3.2.1: + dependencies: + rimraf: 3.0.2 + semver: 7.7.2 + tslib: 2.8.1 + unzip-crx-3: 0.2.0 + + electron-installer-common@0.10.4: + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 1.1.1 + debug: 4.4.1 + fs-extra: 9.1.0 + glob: 7.2.3 + lodash: 4.17.21 + parse-author: 2.0.0 + semver: 7.7.2 + tmp-promise: 3.0.3 + optionalDependencies: + '@types/fs-extra': 9.0.13 + transitivePeerDependencies: + - supports-color + optional: true + + electron-installer-debian@3.2.0: + dependencies: + '@malept/cross-spawn-promise': 1.1.1 + debug: 4.4.1 + electron-installer-common: 0.10.4 + fs-extra: 9.1.0 + get-folder-size: 2.0.1 + lodash: 4.17.21 + word-wrap: 1.2.5 + yargs: 16.2.0 + transitivePeerDependencies: + - supports-color + optional: true + + electron-installer-redhat@3.4.0: + dependencies: + '@malept/cross-spawn-promise': 1.1.1 + debug: 4.4.1 + electron-installer-common: 0.10.4 + fs-extra: 9.1.0 + lodash: 4.17.21 + word-wrap: 1.2.5 + yargs: 16.2.0 + transitivePeerDependencies: + - supports-color + optional: true + + electron-is-accelerator@0.1.2: {} + + electron-is-dev@1.2.0: {} + + electron-localshortcut@3.2.1: + dependencies: + debug: 4.4.1 + electron-is-accelerator: 0.1.2 + keyboardevent-from-electron-accelerator: 2.0.0 + keyboardevents-areequal: 0.2.2 + transitivePeerDependencies: + - supports-color + + electron-packager@17.1.2: + dependencies: + '@electron/asar': 3.4.1 + '@electron/get': 2.0.3 + '@electron/notarize': 1.2.4 + '@electron/osx-sign': 1.3.3 + '@electron/universal': 1.5.1 + cross-spawn-windows-exe: 1.2.0 + debug: 4.4.1 + extract-zip: 2.0.1 + filenamify: 4.3.0 + fs-extra: 11.3.0 + galactus: 1.0.0 + get-package-info: 1.0.0 + junk: 3.1.0 + parse-author: 2.0.0 + plist: 3.1.0 + rcedit: 3.1.0 + resolve: 1.22.10 + semver: 7.7.2 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - supports-color + + electron-publish@26.0.11: + dependencies: + '@types/fs-extra': 9.0.13 + builder-util: 26.0.11 + builder-util-runtime: 9.3.1 + chalk: 4.1.2 + form-data: 4.0.2 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + + electron-squirrel-startup@1.0.1: + dependencies: + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + electron-to-chromium@1.5.161: {} + + electron-to-chromium@1.5.283: {} + + electron-vite@3.1.0(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)): + dependencies: + '@babel/core': 7.27.4 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.27.4) + cac: 6.7.14 + esbuild: 0.25.5 + magic-string: 0.30.17 + picocolors: 1.1.1 + vite: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + + electron-winstaller@5.4.0: + dependencies: + '@electron/asar': 3.4.1 + debug: 4.4.1 + fs-extra: 7.0.1 + lodash: 4.17.21 + temp: 0.9.4 + optionalDependencies: + '@electron/windows-sign': 1.2.2 + transitivePeerDependencies: + - supports-color + + electron@22.3.27: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 16.18.126 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@2.0.0: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + entities@3.0.1: {} + + entities@4.5.0: {} + + env-paths@2.2.1: {} + + err-code@2.0.3: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es6-error@4.1.1: + optional: true + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: + optional: true + + esprima@4.0.1: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + etag@1.8.1: {} + + eventemitter3@5.0.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + exceljs@4.4.0: + dependencies: + archiver: 5.3.2 + dayjs: 1.11.13 + fast-csv: 4.3.6 + jszip: 3.10.1 + readable-stream: 3.6.2 + saxes: 5.0.1 + tmp: 0.2.3 + unzipper: 0.10.14 + uuid: 8.3.2 + + execa@1.0.0: + dependencies: + cross-spawn: 6.0.6 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + execa@7.2.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + + expect-type@1.3.0: {} + + exponential-backoff@3.1.3: {} + + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.1 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.8: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.1 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + extsprintf@1.4.1: + optional: true + + fast-csv@4.3.6: + dependencies: + '@fast-csv/format': 4.3.5 + '@fast-csv/parse': 4.3.6 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-uri@3.1.0: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.4.5(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + filename-reserved-regex@2.0.0: {} + + filenamify@4.3.0: + dependencies: + filename-reserved-regex: 2.0.0 + strip-outer: 1.0.1 + trim-repeated: 1.0.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@2.1.0: + dependencies: + locate-path: 2.0.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flora-colossus@2.0.0: + dependencies: + debug: 4.4.1 + fs-extra: 10.1.0 + transitivePeerDependencies: + - supports-color + + follow-redirects@1.15.9: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + fstream@1.0.12: + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + + function-bind@1.1.2: {} + + galactus@1.0.0: + dependencies: + debug: 4.4.1 + flora-colossus: 2.0.0 + fs-extra: 10.1.0 + transitivePeerDependencies: + - supports-color + + gar@1.0.4: + optional: true + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-folder-size@2.0.1: + dependencies: + gar: 1.0.4 + tiny-each-async: 2.0.3 + optional: true + + get-installed-path@2.1.1: + dependencies: + global-modules: 1.0.0 + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-package-info@1.0.0: + dependencies: + bluebird: 3.7.2 + debug: 2.6.9 + lodash.get: 4.4.2 + read-pkg-up: 2.0.0 + transitivePeerDependencies: + - supports-color + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stdin@9.0.0: {} + + get-stream@4.1.0: + dependencies: + pump: 3.0.2 + + get-stream@5.2.0: + dependencies: + pump: 3.0.2 + + get-stream@6.0.1: {} + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.0.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.9 + once: 1.4.0 + + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.2 + serialize-error: 7.0.1 + optional: true + + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + + globals@11.12.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + + goober@2.1.18(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + + gopd@1.2.0: {} + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + graceful-fs@4.2.11: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + + hono@4.11.7: {} + + hookable@5.5.3: {} + + hosted-git-info@2.8.9: {} + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + + http-cache-semantics@4.2.0: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + human-signals@4.3.1: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + husky@8.0.3: {} + + i18next@25.8.13(typescript@5.9.2): + dependencies: + '@babel/runtime': 7.28.6 + optionalDependencies: + typescript: 5.9.2 + + iconv-corefoundation@1.1.7: + dependencies: + cli-truncate: 2.1.0 + node-addon-api: 1.7.2 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore-by-default@1.0.1: {} + + ignore@5.2.4: {} + + immediate@3.0.6: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + infer-owner@1.0.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@3.0.1: {} + + interpret@3.1.1: {} + + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-lambda@1.0.1: {} + + is-number@7.0.0: {} + + is-promise@4.0.0: {} + + is-stream@1.1.0: {} + + is-stream@3.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-windows@1.0.2: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@1.0.0: {} + + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.4: {} + + isbot@5.1.32: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + javascript-stringify@2.1.0: {} + + jiti@2.6.1: {} + + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + + jose@6.1.3: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-stringify-safe@5.0.1: + optional: true + + json5@2.2.3: {} + + jsonc-parser@3.1.0: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + junk@3.1.0: {} + + keyboardevent-from-electron-accelerator@2.0.0: {} + + keyboardevents-areequal@0.2.2: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@4.1.5: {} + + knip@6.7.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + formatly: 0.3.0 + get-tsconfig: 4.14.0 + jiti: 2.6.1 + minimist: 1.2.8 + oxc-parser: 0.127.0 + oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + picomatch: 4.0.4 + smol-toml: 1.6.1 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.16 + unbash: 3.0.0 + yaml: 2.8.3 + zod: 4.3.6 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + lazy-val@1.0.5: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lilconfig@2.1.0: {} + + linkify-it@4.0.1: + dependencies: + uc.micro: 1.0.6 + + lint-staged@13.3.0: + dependencies: + chalk: 5.3.0 + commander: 11.0.0 + debug: 4.3.4 + execa: 7.2.0 + lilconfig: 2.1.0 + listr2: 6.6.1 + micromatch: 4.0.5 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.3.1 + transitivePeerDependencies: + - enquirer + - supports-color + + listenercount@1.0.1: {} + + listr2@5.0.8: + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.4.1 + rxjs: 7.8.2 + through: 2.3.8 + wrap-ansi: 7.0.0 + + listr2@6.6.1: + dependencies: + cli-truncate: 3.1.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 5.0.1 + rfdc: 1.4.1 + wrap-ansi: 8.1.0 + + load-json-file@2.0.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 2.2.0 + pify: 2.3.0 + strip-bom: 3.0.0 + + locate-path@2.0.0: + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.escaperegexp@4.1.2: {} + + lodash.flatten@4.4.0: {} + + lodash.get@4.4.2: {} + + lodash.groupby@4.6.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isequal@4.5.0: {} + + lodash.isfunction@3.0.9: {} + + lodash.isnil@4.0.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isundefined@3.0.1: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.truncate@4.4.2: {} + + lodash.union@4.6.0: {} + + lodash.uniq@4.5.0: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@4.0.0: + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + + log-update@5.0.1: + dependencies: + ansi-escapes: 5.0.0 + cli-cursor: 4.0.0 + slice-ansi: 5.0.0 + strip-ansi: 7.1.0 + wrap-ansi: 8.1.0 + + look-it-up@2.1.0: {} + + lowercase-keys@2.0.0: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@7.18.3: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-fetch-happen@10.2.1: + dependencies: + agentkeepalive: 4.6.0 + cacache: 16.1.3 + http-cache-semantics: 4.2.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 2.1.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 7.0.0 + ssri: 9.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + + map-age-cleaner@0.1.3: + dependencies: + p-defer: 1.0.0 + + markdown-it@13.0.1: + dependencies: + argparse: 2.0.1 + entities: 3.0.1 + linkify-it: 4.0.1 + mdurl: 1.0.1 + uc.micro: 1.0.6 + + markdownlint-cli@0.32.2: + dependencies: + commander: 9.4.1 + get-stdin: 9.0.0 + glob: 8.0.3 + ignore: 5.2.4 + js-yaml: 4.1.0 + jsonc-parser: 3.1.0 + markdownlint: 0.26.2 + markdownlint-rule-helpers: 0.17.2 + minimatch: 5.1.6 + run-con: 1.2.12 + + markdownlint-rule-helpers@0.17.2: {} + + markdownlint@0.26.2: + dependencies: + markdown-it: 13.0.1 + + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + + math-intrinsics@1.1.0: {} + + mdurl@1.0.1: {} + + media-typer@1.1.0: {} + + mem@4.3.0: + dependencies: + map-age-cleaner: 0.1.3 + mimic-fn: 2.1.0 + p-is-promise: 2.1.0 + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + microdiff@1.5.0: {} + + micromatch@4.0.5: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.0 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + + minipass-fetch@2.1.2: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.7: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + ms@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + negotiator@0.6.4: {} + + negotiator@1.0.0: {} + + nice-try@1.0.5: {} + + node-abi@3.75.0: + dependencies: + semver: 7.7.2 + + node-addon-api@1.7.2: + optional: true + + node-api-version@0.2.1: + dependencies: + semver: 7.7.2 + + node-eval@2.0.0: + dependencies: + path-is-absolute: 1.0.1 + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-releases@2.0.19: {} + + node-releases@2.0.27: {} + + nodemon@2.0.22: + dependencies: + chokidar: 3.6.0 + debug: 3.2.7(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 5.7.2 + simple-update-notifier: 1.1.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + nopt@6.0.0: + dependencies: + abbrev: 1.1.1 + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.10 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + normalize-path@3.0.0: {} + + normalize-url@6.1.0: {} + + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: + optional: true + + object-path@0.11.8: {} + + obug@2.1.1: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + outdent@0.8.0: {} + + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.19.1 + '@oxc-resolver/binding-android-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-x64': 11.19.1 + '@oxc-resolver/binding-freebsd-x64': 11.19.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.19.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.19.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-musl': 11.19.1 + '@oxc-resolver/binding-openharmony-arm64': 11.19.1 + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + p-cancelable@2.1.1: {} + + p-defer@1.0.0: {} + + p-finally@1.0.0: {} + + p-is-promise@2.1.0: {} + + p-limit@1.3.0: + dependencies: + p-try: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@5.0.0: + dependencies: + yocto-queue: 1.2.2 + + p-locate@2.0.0: + dependencies: + p-limit: 1.3.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-try@1.0.0: {} + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.6.0: {} + + pako@1.0.11: {} + + parse-author@2.0.0: + dependencies: + author-regex: 1.0.0 + + parse-json@2.2.0: + dependencies: + error-ex: 1.3.2 + + parse-passwd@1.0.0: {} + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-exists@3.0.0: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@2.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@8.3.0: {} + + path-type@2.0.0: + dependencies: + pify: 2.3.0 + + pathe@2.0.3: {} + + pe-library@0.4.1: {} + + pend@1.2.0: {} + + perfect-debounce@1.0.0: {} + + phosphor-react@1.4.1(react@19.2.4): + dependencies: + react: 19.2.4 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + picomatch@4.0.3: {} + + picomatch@4.0.4: {} + + pidtree@0.6.0: {} + + pify@2.3.0: {} + + pkce-challenge@5.0.1: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.8 + pathe: 2.0.3 + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pluralize@8.0.0: {} + + postcss-discard-duplicates@7.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-empty@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-merge-rules@7.0.7(postcss@8.5.6): + dependencies: + browserslist: 4.28.0 + caniuse-api: 3.0.0 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-minify-selectors@7.0.5(postcss@8.5.6): + dependencies: + cssesc: 3.0.0 + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-nested@7.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-normalize-whitespace@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.4: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postject@1.0.0-alpha.6: + dependencies: + commander: 9.5.0 + optional: true + + prettier@2.8.8: {} + + prettier@3.2.5: {} + + prettier@3.7.3: {} + + proc-log@2.0.1: {} + + process-nextick-args@2.0.1: {} + + progress@2.0.3: {} + + promise-inflight@1.0.1: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + pstree.remy@1.1.8: {} + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + quick-lru@5.1.1: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rcedit@3.1.0: + dependencies: + cross-spawn-windows-exe: 1.2.0 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-hot-toast@2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + csstype: 3.2.3 + goober: 2.1.18(csstype@3.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + react-i18next@16.5.4(i18next@25.8.13(typescript@5.9.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.2): + dependencies: + '@babel/runtime': 7.28.6 + html-parse-stringify: 3.0.1 + i18next: 25.8.13(typescript@5.9.2) + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + typescript: 5.9.2 + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.13)(react@19.2.4): + dependencies: + react: 19.2.4 + react-style-singleton: 2.2.3(@types/react@19.2.13)(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + react-remove-scroll@2.7.2(@types/react@19.2.13)(react@19.2.4): + dependencies: + react: 19.2.4 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.13)(react@19.2.4) + react-style-singleton: 2.2.3(@types/react@19.2.13)(react@19.2.4) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.13)(react@19.2.4) + use-sidecar: 1.1.3(@types/react@19.2.13)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + + react-style-singleton@2.2.3(@types/react@19.2.13)(react@19.2.4): + dependencies: + get-nonce: 1.0.1 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + react@19.2.4: {} + + read-binary-file-arch@1.0.6: + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + read-pkg-up@2.0.0: + dependencies: + find-up: 2.1.0 + read-pkg: 2.0.0 + + read-pkg@2.0.0: + dependencies: + load-json-file: 2.0.0 + normalize-package-data: 2.5.0 + path-type: 2.0.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + rechoir@0.8.0: + dependencies: + resolve: 1.22.10 + + regex-escape@3.4.11: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resedit@1.7.2: + dependencies: + pe-library: 0.4.1 + + resolve-alpn@1.2.1: {} + + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + + resolve-package@1.0.1: + dependencies: + get-installed-path: 2.1.1 + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@4.0.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.12.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@2.6.3: + dependencies: + glob: 7.2.3 + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + + rollup@4.41.1: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.41.1 + '@rollup/rollup-android-arm64': 4.41.1 + '@rollup/rollup-darwin-arm64': 4.41.1 + '@rollup/rollup-darwin-x64': 4.41.1 + '@rollup/rollup-freebsd-arm64': 4.41.1 + '@rollup/rollup-freebsd-x64': 4.41.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.41.1 + '@rollup/rollup-linux-arm-musleabihf': 4.41.1 + '@rollup/rollup-linux-arm64-gnu': 4.41.1 + '@rollup/rollup-linux-arm64-musl': 4.41.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.41.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.41.1 + '@rollup/rollup-linux-riscv64-gnu': 4.41.1 + '@rollup/rollup-linux-riscv64-musl': 4.41.1 + '@rollup/rollup-linux-s390x-gnu': 4.41.1 + '@rollup/rollup-linux-x64-gnu': 4.41.1 + '@rollup/rollup-linux-x64-musl': 4.41.1 + '@rollup/rollup-win32-arm64-msvc': 4.41.1 + '@rollup/rollup-win32-ia32-msvc': 4.41.1 + '@rollup/rollup-win32-x64-msvc': 4.41.1 + fsevents: 2.3.3 + + rollup@4.53.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + run-con@1.2.12: + dependencies: + deep-extend: 0.6.0 + ini: 3.0.1 + minimist: 1.2.8 + strip-json-comments: 3.1.1 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sanitize-filename@1.6.3: + dependencies: + truncate-utf8-bytes: 1.0.2 + + sax@1.4.1: {} + + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver-compare@1.0.0: + optional: true + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.0.0: {} + + semver@7.7.2: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + + seroval-plugins@1.3.3(seroval@1.3.2): + dependencies: + seroval: 1.3.2 + + seroval-plugins@1.4.0(seroval@1.4.0): + dependencies: + seroval: 1.4.0 + + seroval@1.3.2: {} + + seroval@1.4.0: {} + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setimmediate@1.0.5: {} + + setprototypeof@1.2.0: {} + + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@1.0.0: {} + + shebang-regex@3.0.0: {} + + shell-quote@1.8.2: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-update-notifier@1.1.0: + dependencies: + semver: 7.0.0 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.2 + + sisteransi@1.0.5: {} + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + smart-buffer@4.2.0: {} + + smol-toml@1.6.1: {} + + socks-proxy-agent@7.0.0: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + solid-js@1.9.10: + dependencies: + csstype: 3.2.3 + seroval: 1.3.2 + seroval-plugins: 1.3.3(seroval@1.3.2) + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + spawn-command@0.0.2: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.21 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.21 + + spdx-license-ids@3.0.21: {} + + sprintf-js@1.1.3: + optional: true + + ssri@9.0.1: + dependencies: + minipass: 3.3.6 + + stackback@0.0.2: {} + + stat-mode@1.0.0: {} + + statuses@2.0.2: {} + + std-env@4.1.0: {} + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom@3.0.0: {} + + strip-eof@1.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-json-comments@5.0.3: {} + + strip-outer@1.0.1: + dependencies: + escape-string-regexp: 1.0.5 + + sudo-prompt@9.2.1: {} + + sumchecker@3.0.1: + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + + temp@0.9.4: + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.3 + + through@2.3.8: {} + + tiny-async-pool@1.3.0: + dependencies: + semver: 5.7.2 + + tiny-each-async@2.0.3: + optional: true + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.1.2: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.5(picomatch@4.0.2) + picomatch: 4.0.2 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.3 + + tmp@0.2.3: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + touch@3.1.1: {} + + tr46@0.0.3: {} + + traverse@0.3.9: {} + + tree-kill@1.2.2: {} + + trim-repeated@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + + ts-evaluator@1.2.0(typescript@5.9.2): + dependencies: + ansi-colors: 4.1.3 + crosspath: 2.0.0 + object-path: 0.11.8 + typescript: 5.9.2 + + ts-morph@27.0.2: + dependencies: + '@ts-morph/common': 0.28.1 + code-block-writer: 13.0.3 + + ts-pattern@5.9.0: {} + + tsconfck@3.1.6(typescript@5.9.2): + optionalDependencies: + typescript: 5.9.2 + + tslib@2.8.1: {} + + tsx@4.20.6: + dependencies: + esbuild: 0.25.5 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + + type-fest@0.13.1: + optional: true + + type-fest@0.21.3: {} + + type-fest@1.4.0: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.2: {} + + typescript@5.9.3: {} + + uc.micro@1.0.6: {} + + unbash@3.0.0: {} + + undefsafe@2.0.5: {} + + undici-types@6.21.0: {} + + unique-filename@2.0.1: + dependencies: + unique-slug: 3.0.0 + + unique-slug@3.0.0: + dependencies: + imurmurhash: 0.1.4 + + universalify@0.1.2: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + unzip-crx-3@0.2.0: + dependencies: + jszip: 3.10.1 + mkdirp: 0.5.6 + yaku: 0.16.7 + + unzipper@0.10.14: + dependencies: + big-integer: 1.6.52 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.11 + listenercount: 1.0.1 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + update-browserslist-db@1.1.3(browserslist@4.25.0): + dependencies: + browserslist: 4.25.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + update-browserslist-db@1.2.3(browserslist@4.28.0): + dependencies: + browserslist: 4.28.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.13)(react@19.2.4): + dependencies: + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + use-sidecar@1.1.3(@types/react@19.2.13)(react@19.2.4): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + use-sync-external-store@1.5.0(react@19.2.4): + dependencies: + react: 19.2.4 + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + username@5.1.0: + dependencies: + execa: 1.0.0 + mem: 4.3.0 + + utf8-byte-length@1.0.5: {} + + util-deprecate@1.0.2: {} + + uuid@8.3.2: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + vary@1.1.2: {} + + verror@1.10.1: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.4.1 + optional: true + + vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.5(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.4 + rollup: 4.41.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 20.19.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + tsx: 4.20.6 + yaml: 2.8.3 + + vite@7.2.4(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3): + dependencies: + esbuild: 0.25.5 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + tsx: 4.20.6 + yaml: 2.8.3 + + vitest@4.1.6(@types/node@20.19.1)(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.6 + '@vitest/mocker': 4.1.6(vite@6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)) + '@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.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 6.3.5(@types/node@20.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.1 + transitivePeerDependencies: + - msw + + void-elements@3.1.0: {} + + wait-on@6.0.1: + dependencies: + axios: 0.25.0 + joi: 17.13.3 + lodash: 4.17.21 + minimist: 1.2.8 + rxjs: 7.8.2 + transitivePeerDependencies: + - debug + + walk-up-path@4.0.0: {} + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@3.0.1: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: + optional: true + + wordwrapjs@5.1.1: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + xmlbuilder@15.1.1: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yaku@0.16.7: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml@2.3.1: {} + + yaml@2.8.3: {} + + yargs-parser@20.2.9: + optional: true + + yargs-parser@21.1.1: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + optional: true + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yarn-or-npm@3.0.1: + dependencies: + cross-spawn: 6.0.6 + pkg-dir: 4.2.0 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.2: {} + + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + + zod-to-json-schema@3.25.1(zod@4.0.14): + dependencies: + zod: 4.0.14 + + zod@3.25.76: {} + + zod@4.0.14: {} + + zod@4.3.6: {} + + zustand@5.0.7(@types/react@19.2.13)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + optionalDependencies: + '@types/react': 19.2.13 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 00000000..041eb937 --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +allowBuilds: + "@biomejs/biome": true + electron-winstaller: true + electron: true + esbuild: true + +blockExoticSubdeps: false diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs new file mode 100644 index 00000000..1bfc8f1d --- /dev/null +++ b/frontend/postcss.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + "@pandacss/dev/postcss": {}, + }, +}; diff --git a/frontend/src/main/createWindow.ts b/frontend/src/main/createWindow.ts new file mode 100644 index 00000000..379be1e7 --- /dev/null +++ b/frontend/src/main/createWindow.ts @@ -0,0 +1,73 @@ +import { join } from "node:path"; +import { is } from "@electron-toolkit/utils"; +import { BrowserWindow, shell } from "electron"; + +import { DEV_PORT, EXTERNAL_URLS, isDebug, isProduction } from "./env"; +import { resolveHTMLPath } from "./utils"; + +export let mainWindow: BrowserWindow | null; + +/** + * Configures the main `BrowserWindow` with features and handlers + * @param window Main window created on the main process + */ +function configureWindow(window: BrowserWindow | null) { + if (!window) { + throw new Error('"mainWindow" is not defined'); + } + if (isDebug) { + window.webContents.openDevTools(); + } + if (isProduction) { + window.maximize(); + } + + window.loadURL(resolveHTMLPath()); + + // window.webContents.openDevTools(); + + /** + * HANDLERS + */ + window.on("ready-to-show", () => { + window.show(); + }); + + window.webContents.setWindowOpenHandler(({ url }) => { + if (EXTERNAL_URLS.find((val) => url.includes(val))) { + shell.openExternal(url); + } + return { action: "deny" }; + }); + + // and handlers + mainWindow?.on("closed", () => { + mainWindow = null; + }); +} + +export default function createWindow() { + // Creates the browser window. + mainWindow = new BrowserWindow({ + width: 1366, + height: 768, + autoHideMenuBar: true, + webPreferences: { + preload: join(__dirname, "../preload/index.js"), + sandbox: false, + }, + }); + + // Add configuration + configureWindow(mainWindow); + + // HMR for renderer base on electron-vite cli. + // Load the remote URL for development or the local html file for production. + if (is.dev) { + mainWindow.loadURL(`http://localhost:${DEV_PORT}`); + } else { + mainWindow.loadFile(join(__dirname, "../renderer/index.html")); + } + + return mainWindow; +} diff --git a/frontend/src/main/env.ts b/frontend/src/main/env.ts new file mode 100644 index 00000000..4b7c0e76 --- /dev/null +++ b/frontend/src/main/env.ts @@ -0,0 +1,54 @@ +/// + +import { homedir } from "node:os"; +import path from "node:path"; +import { is } from "@electron-toolkit/utils"; + +// ------------------------ +// CONFIG VARIABLES +// ------------------------ + +/** + * Port for the development app + */ +export const DEV_PORT = 3000; + +/** + * Custom URI scheme name + */ +export const URI_SCHEME = "aymurai.app"; + +/** + * Exports folder + */ +export const EXPORTS_FOLDER = path.resolve(homedir(), "Documents/AymurAI"); + +/** + * URLs that must be opened in a native browser tab + */ +export const EXTERNAL_URLS = [ + "https://www.datagenero.org/", + "https://accounts.google.com/o/oauth2/v2/auth", + "https://docs.google.com/spreadsheets", + "https://www.aymurai.info", +]; + +/** + * Is the app in development mode? + */ +export const isDebug = is.dev; + +/** + * Is the app in production mode? + */ +export const isProduction = !is.dev; + +/** + * Is the app running on Windows? + */ +export const isWindows = process.platform === "win32"; + +/** + * Is the app running on macOS? + */ +export const isMac = process.platform === "darwin"; diff --git a/frontend/src/main/extensions.ts b/frontend/src/main/extensions.ts new file mode 100644 index 00000000..836b32a9 --- /dev/null +++ b/frontend/src/main/extensions.ts @@ -0,0 +1,17 @@ +import installer, { + REACT_DEVELOPER_TOOLS, + EMBER_INSPECTOR, +} from "electron-devtools-installer"; + +export const installExtensions = async (): Promise => { + const extensions = [EMBER_INSPECTOR, REACT_DEVELOPER_TOOLS]; + + return installer(extensions, { + forceDownload: true, + loadExtensionOptions: { + allowFileAccess: true, + }, + }) + .then(console.log) + .catch(console.error); +}; diff --git a/frontend/src/main/index.ts b/frontend/src/main/index.ts new file mode 100644 index 00000000..1f133afc --- /dev/null +++ b/frontend/src/main/index.ts @@ -0,0 +1,54 @@ +import { electronApp, optimizer } from "@electron-toolkit/utils"; +import { BrowserWindow, app, ipcMain } from "electron"; + +import createWindow from "./createWindow"; +import { installExtensions } from "./extensions"; +import { excel, feedback, subprocess, taskbar } from "./utils"; + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(async () => { + // Set app user model id for windows + electronApp.setAppUserModelId("com.electron"); + + // Default open or close DevTools by F12 in development + // and ignore CommandOrControl + R in production. + // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils + app.on("browser-window-created", (_, window) => { + optimizer.watchWindowShortcuts(window); + }); + + ipcMain.handle("EXPORT_FEEDBACK", (_, fileName: string, data: object) => + feedback.export(fileName, data), + ); + ipcMain.handle("EXCEL_READ", excel.read); + ipcMain.handle("EXCEL_WRITE", (_, buffer: ArrayBuffer) => + excel.write(buffer), + ); + ipcMain.handle("EXCEL_OPEN", excel.open); + + // TASKBAR + ipcMain.handle("TASKBAR_NOTIFY", taskbar.notify); + + ipcMain.handle("RUN_BATCH", subprocess.run); + + await installExtensions(); + createWindow(); + + app.on("activate", () => { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on("window-all-closed", async () => { + subprocess.stop(); + if (process.platform !== "darwin") { + app.quit(); + } +}); diff --git a/frontend/src/main/utils/__tests__/excel.test.ts b/frontend/src/main/utils/__tests__/excel.test.ts new file mode 100644 index 00000000..235565e7 --- /dev/null +++ b/frontend/src/main/utils/__tests__/excel.test.ts @@ -0,0 +1,52 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { Workbook } from "exceljs"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("electron", () => ({ + shell: { + openPath: vi.fn(), + }, +})); + +vi.mock("@electron-toolkit/utils", () => ({ + is: { + dev: true, + }, +})); + +describe("main excel filesystem bridge", () => { + let tempHome: string; + + beforeEach(async () => { + tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "aymurai-excel-")); + vi.spyOn(os, "homedir").mockReturnValue(tempHome); + vi.resetModules(); + }); + + afterEach(async () => { + vi.restoreAllMocks(); + await fs.rm(tempHome, { recursive: true, force: true }); + }); + + it("returns the exact file bytes when reading the dataset workbook", async () => { + const { default: excel } = await import("../excel"); + const workbook = new Workbook(); + workbook.addWorksheet("set_de_datos").addRow(["ok"]); + + const original = await workbook.xlsx.writeBuffer(); + await excel.write(original); + + const loaded = await excel.read(); + expect(loaded.byteLength).toBe(original.byteLength); + + const reloaded = new Workbook(); + const loadedBuffer = Buffer.from(new Uint8Array(loaded as ArrayBuffer)); + const loadWorkbook = reloaded.xlsx.load.bind(reloaded.xlsx) as ( + data: unknown, + ) => Promise; + await expect(loadWorkbook(loadedBuffer)).resolves.toBeDefined(); + expect(reloaded.worksheets[0].getCell("A1").value).toBe("ok"); + }); +}); diff --git a/frontend/src/main/utils/excel.ts b/frontend/src/main/utils/excel.ts new file mode 100644 index 00000000..5548796b --- /dev/null +++ b/frontend/src/main/utils/excel.ts @@ -0,0 +1,44 @@ +import fs from "node:fs/promises"; +import { shell } from "electron"; + +import { EXPORTS_FOLDER } from "../env"; +import filesystem from "./filesystem"; + +const FILENAME = "set_de_datos.xlsx"; +const PATH = `${EXPORTS_FOLDER}/${FILENAME}`; + +/** + * Reads the raw data from the data_set excel file + * @returns The `Promise` read from the file + */ +function read() { + return fs.readFile(PATH).then(({ buffer }) => buffer); +} + +/** + * Writes a .xlsx file to the filesystem + * @param buffer Data buffer to write. This is the .xlsx file + */ +async function write(arrBuffer: ArrayBuffer) { + // Create directory if necessary + if (!(await filesystem.exists(EXPORTS_FOLDER))) { + await fs.mkdir(EXPORTS_FOLDER, { recursive: true }); + } + + await fs.writeFile(PATH, Buffer.from(arrBuffer)); +} + +/** + * Opens the previously created .xlsx using the default app + * @returns Message if the opening was succesfull or not + */ +function open() { + return shell.openPath(PATH); +} + +const excel = { + read, + write, + open, +}; +export default excel; diff --git a/frontend/src/main/utils/feedback.ts b/frontend/src/main/utils/feedback.ts new file mode 100644 index 00000000..0224e3f5 --- /dev/null +++ b/frontend/src/main/utils/feedback.ts @@ -0,0 +1,61 @@ +import { promises as fs } from "node:fs"; + +import { EXPORTS_FOLDER } from "../env"; +import filesystem from "./filesystem"; + +const FEEDBACK_FOLDER = `${EXPORTS_FOLDER}/feedback`; + +/** + * Gets the current date and time in the format `YYYY-MM-DD-HH_MM_SS` + */ +function getDate() { + const now = new Date(Date.now()); + const [date, time] = now.toISOString().split("T"); + + const fileDate = `${date}-${ + time // Time in format xx:yy:zz.zzz + .split(".")[0] // We are left with xx:yy:zz + .replace(/:/g, "_") // Transform it into xx_yy_zz + }`; + + return fileDate; +} + +function formatName(fileName: string) { + // Remove extension and spaces from the name + const formattedName = fileName + .replace(/\.docx/g, "") // Remove extension + .replace(/\s/g, "_"); // Remove whitespace + + return `aymurai--${formattedName}`; +} + +async function mkdir() { + await fs.mkdir(FEEDBACK_FOLDER, { recursive: true }); +} + +/** + * Parses a validation object into a JSON that is written into a feedback file + * @param fileName Name of the file + * @param content Validation object + */ +async function exportFeedback(fileName: string, content: object) { + const date = getDate(); + const name = formatName(fileName); + const json = JSON.stringify(content); + + // Create directory if necessary + if (!(await filesystem.exists(FEEDBACK_FOLDER))) { + await mkdir(); + } + + // Create file + await fs.writeFile(`${FEEDBACK_FOLDER}/${name}--${date}.json`, json, { + flag: "w", + }); +} + +const feedback = { + export: exportFeedback, +}; +export default feedback; diff --git a/frontend/src/main/utils/filesystem.ts b/frontend/src/main/utils/filesystem.ts new file mode 100644 index 00000000..bba048e0 --- /dev/null +++ b/frontend/src/main/utils/filesystem.ts @@ -0,0 +1,20 @@ +import fs from "node:fs/promises"; + +/** + * Check for the existence of a specific director or file + * @param path Path to check + * @returns `true` if the directory/file exists, `false` otherwise + */ +async function exists(path: string) { + try { + await fs.access(path, fs.constants.F_OK); + return true; + } catch { + return false; + } +} + +const filesystem = { + exists, +}; +export default filesystem; diff --git a/frontend/src/main/utils/index.ts b/frontend/src/main/utils/index.ts new file mode 100644 index 00000000..7c4176e3 --- /dev/null +++ b/frontend/src/main/utils/index.ts @@ -0,0 +1,7 @@ +import excel from "./excel"; +import feedback from "./feedback"; +import resolveHTMLPath from "./resolveHTMLPath"; +import taskbar from "./taskbar"; +export { excel, feedback, resolveHTMLPath, taskbar }; + +export * as subprocess from "./subprocess"; diff --git a/frontend/src/main/utils/resolveHTMLPath.ts b/frontend/src/main/utils/resolveHTMLPath.ts new file mode 100644 index 00000000..e7f0e539 --- /dev/null +++ b/frontend/src/main/utils/resolveHTMLPath.ts @@ -0,0 +1,20 @@ +import path from "node:path"; +import { URL } from "node:url"; + +import { DEV_PORT, isDebug } from "../env"; + +/** + * Retrieves the HMTL path to the requested file + * @param fileName Name of the file, default to `index.html` + * @returns An HTML path to the file, wether on the file system or the web + */ +export default function resolveHTMLPath(fileName = "index.html") { + if (isDebug) { + const url = new URL(`http://localhost:${DEV_PORT}`); + + return url.href; + } + + const pathToFile = path.resolve(__dirname, "../app", fileName); + return `file://${pathToFile}`; +} diff --git a/frontend/src/main/utils/subprocess.ts b/frontend/src/main/utils/subprocess.ts new file mode 100644 index 00000000..0dd06696 --- /dev/null +++ b/frontend/src/main/utils/subprocess.ts @@ -0,0 +1,31 @@ +import { exec, spawn } from "node:child_process"; +import path from "node:path"; +import { app } from "electron"; + +export const run = () => { + const batFilePath = path.join(app.getPath("exe"), "../run_server.bat"); + const child = spawn(batFilePath); + + return new Promise((ok, no) => { + if (!child) return no(new Error("Child process not found")); + + child.on("spawn", () => { + ok(true); + }); + child.on("error", (err) => { + no(err); + }); + }); +}; + +export const stop = (): Promise => { + return new Promise((ok, no) => { + exec( + 'powershell "& Get-Process -Name python | ? { $_.Path -eq \\"$env:USERPROFILE\\miniconda3\\envs\\aymurai-backend\\python.exe\\"} | Stop-Process"', + (err) => { + if (err) return no(err); + ok(true); + }, + ); + }); +}; diff --git a/frontend/src/main/utils/taskbar.ts b/frontend/src/main/utils/taskbar.ts new file mode 100644 index 00000000..dca71e34 --- /dev/null +++ b/frontend/src/main/utils/taskbar.ts @@ -0,0 +1,27 @@ +import { app } from "electron"; +import { mainWindow } from "../createWindow"; +import { isMac, isWindows } from "../env"; + +/** + * Notify the user through the taskbar + */ +const notify = () => { + if (isWindows) { + if (!mainWindow) return; + + // Flash taskbar icon (windows) + mainWindow.flashFrame(true); + + mainWindow.once("focus", () => mainWindow?.flashFrame(false)); + } else if (isMac) { + if (!app || !app.dock) return; + + // Bounce dock icon (macOS) + app.dock.bounce("informational"); + } +}; + +const taskbar = { + notify, +}; +export default taskbar; diff --git a/frontend/src/preload/index.d.ts b/frontend/src/preload/index.d.ts new file mode 100644 index 00000000..5d8869bd --- /dev/null +++ b/frontend/src/preload/index.d.ts @@ -0,0 +1,22 @@ +declare global { + interface Window { + filesystem?: { + feedback: { + export: (fileName: string, data: object) => Promise; + }; + excel: { + read: () => Promise; + write: (arrBuffer: ArrayBuffer) => Promise; + open: () => Promise; + }; + }; + taskbar?: { + notify: () => Promise; + }; + electronAPI?: { + runBatch: () => Promise; + }; + } +} + +export {}; diff --git a/frontend/src/preload/index.ts b/frontend/src/preload/index.ts new file mode 100644 index 00000000..a1a0ebbc --- /dev/null +++ b/frontend/src/preload/index.ts @@ -0,0 +1,39 @@ +import { electronAPI } from "@electron-toolkit/preload"; +import { contextBridge, ipcRenderer } from "electron"; + +// Custom APIs for renderer +const api = {}; + +// Use `contextBridge` APIs to expose Electron APIs to +// renderer only if context isolation is enabled, otherwise +// just add to the DOM global. +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld("filesystem", { + feedback: { + export: (fileName: string, object: object) => + ipcRenderer.invoke("EXPORT_FEEDBACK", fileName, object), + }, + excel: { + read: () => ipcRenderer.invoke("EXCEL_READ"), + write: (buffer: Buffer) => ipcRenderer.invoke("EXCEL_WRITE", buffer), + open: () => ipcRenderer.invoke("EXCEL_OPEN"), + }, + }); + + contextBridge.exposeInMainWorld("taskbar", { + notify: () => ipcRenderer.invoke("TASKBAR_NOTIFY"), + }); + + contextBridge.exposeInMainWorld("electronAPI", { + runBatch: (result: unknown) => ipcRenderer.invoke("RUN_BATCH", result), + }); + } catch (error) { + console.error(error); + } +} else { + // @ts-ignore (define in dts) + window.electron = electronAPI; + // @ts-ignore (define in dts) + window.api = api; +} diff --git a/frontend/src/renderer/index.html b/frontend/src/renderer/index.html new file mode 100644 index 00000000..93cd96fa --- /dev/null +++ b/frontend/src/renderer/index.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + AymurAI + + + +
+ + + + \ No newline at end of file diff --git a/frontend/src/renderer/public/audio/notification.mp3 b/frontend/src/renderer/public/audio/notification.mp3 new file mode 100644 index 00000000..6b4b277f Binary files /dev/null and b/frontend/src/renderer/public/audio/notification.mp3 differ diff --git a/frontend/src/renderer/public/brand/aymurai-hor-darkpurple.svg b/frontend/src/renderer/public/brand/aymurai-hor-darkpurple.svg new file mode 100644 index 00000000..efde8df1 --- /dev/null +++ b/frontend/src/renderer/public/brand/aymurai-hor-darkpurple.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/renderer/public/brand/aymurai-iso-darkpurple.svg b/frontend/src/renderer/public/brand/aymurai-iso-darkpurple.svg new file mode 100644 index 00000000..9fa89d34 --- /dev/null +++ b/frontend/src/renderer/public/brand/aymurai-iso-darkpurple.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/renderer/public/brand/aymurai-iso-purple.svg b/frontend/src/renderer/public/brand/aymurai-iso-purple.svg new file mode 100644 index 00000000..35114f7c --- /dev/null +++ b/frontend/src/renderer/public/brand/aymurai-iso-purple.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/renderer/public/brand/aymurai-vert-darkpurple.svg b/frontend/src/renderer/public/brand/aymurai-vert-darkpurple.svg new file mode 100644 index 00000000..452bbe89 --- /dev/null +++ b/frontend/src/renderer/public/brand/aymurai-vert-darkpurple.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/renderer/public/brand/aymurai-vert-purple.png b/frontend/src/renderer/public/brand/aymurai-vert-purple.png new file mode 100644 index 00000000..fa78e70d Binary files /dev/null and b/frontend/src/renderer/public/brand/aymurai-vert-purple.png differ diff --git a/frontend/src/renderer/public/brand/datagenero.svg b/frontend/src/renderer/public/brand/datagenero.svg new file mode 100644 index 00000000..a07225aa --- /dev/null +++ b/frontend/src/renderer/public/brand/datagenero.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/renderer/public/brand/iso-white.png b/frontend/src/renderer/public/brand/iso-white.png new file mode 100644 index 00000000..5c1359f1 Binary files /dev/null and b/frontend/src/renderer/public/brand/iso-white.png differ diff --git a/frontend/src/renderer/public/brand/logo.jpg b/frontend/src/renderer/public/brand/logo.jpg new file mode 100644 index 00000000..a8e38723 Binary files /dev/null and b/frontend/src/renderer/public/brand/logo.jpg differ diff --git a/frontend/src/renderer/public/button-icons/add-all.svg b/frontend/src/renderer/public/button-icons/add-all.svg new file mode 100644 index 00000000..d601b7f1 --- /dev/null +++ b/frontend/src/renderer/public/button-icons/add-all.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/renderer/public/button-icons/add-one.svg b/frontend/src/renderer/public/button-icons/add-one.svg new file mode 100644 index 00000000..3ecb521c --- /dev/null +++ b/frontend/src/renderer/public/button-icons/add-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/renderer/public/button-icons/delete-all.svg b/frontend/src/renderer/public/button-icons/delete-all.svg new file mode 100644 index 00000000..9a3f48bf --- /dev/null +++ b/frontend/src/renderer/public/button-icons/delete-all.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/renderer/public/button-icons/delete-one.svg b/frontend/src/renderer/public/button-icons/delete-one.svg new file mode 100644 index 00000000..aebeacbe --- /dev/null +++ b/frontend/src/renderer/public/button-icons/delete-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/renderer/public/button-icons/replace-all.svg b/frontend/src/renderer/public/button-icons/replace-all.svg new file mode 100644 index 00000000..109ec00d --- /dev/null +++ b/frontend/src/renderer/public/button-icons/replace-all.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/renderer/public/button-icons/replace-one.svg b/frontend/src/renderer/public/button-icons/replace-one.svg new file mode 100644 index 00000000..409bb51b --- /dev/null +++ b/frontend/src/renderer/public/button-icons/replace-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/renderer/public/favicon.ico b/frontend/src/renderer/public/favicon.ico new file mode 100644 index 00000000..7e589465 Binary files /dev/null and b/frontend/src/renderer/public/favicon.ico differ diff --git a/frontend/src/renderer/public/manifest.json b/frontend/src/renderer/public/manifest.json new file mode 100644 index 00000000..3536cc5d --- /dev/null +++ b/frontend/src/renderer/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Electron App", + "name": "Electron Desktop App", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/frontend/src/renderer/public/onboarding-steps/step1.png b/frontend/src/renderer/public/onboarding-steps/step1.png new file mode 100644 index 00000000..1e560fe4 Binary files /dev/null and b/frontend/src/renderer/public/onboarding-steps/step1.png differ diff --git a/frontend/src/renderer/public/onboarding-steps/step2.png b/frontend/src/renderer/public/onboarding-steps/step2.png new file mode 100644 index 00000000..51454b27 Binary files /dev/null and b/frontend/src/renderer/public/onboarding-steps/step2.png differ diff --git a/frontend/src/renderer/public/onboarding-steps/step3.png b/frontend/src/renderer/public/onboarding-steps/step3.png new file mode 100644 index 00000000..9afdd3f7 Binary files /dev/null and b/frontend/src/renderer/public/onboarding-steps/step3.png differ diff --git a/frontend/src/renderer/public/onboarding-steps/step4.png b/frontend/src/renderer/public/onboarding-steps/step4.png new file mode 100644 index 00000000..01abcb98 Binary files /dev/null and b/frontend/src/renderer/public/onboarding-steps/step4.png differ diff --git a/frontend/src/renderer/public/robots.txt b/frontend/src/renderer/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/frontend/src/renderer/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/frontend/src/renderer/src/app.tsx b/frontend/src/renderer/src/app.tsx new file mode 100644 index 00000000..da40d36e --- /dev/null +++ b/frontend/src/renderer/src/app.tsx @@ -0,0 +1,49 @@ +import { + RouterProvider, + createMemoryHistory, + createRouter, +} from "@tanstack/react-router"; +import { Toaster } from "react-hot-toast"; + +import { ThemeProvider } from "@/components"; +import FileProvider from "@/context/File"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import * as TanstackReactQuery from "@/features/ReactQueryProvider"; + +// Import the generated route tree +import { routeTree } from "./routeTree.gen"; + +const TanStackQueryProviderContext = TanstackReactQuery.getContext(); + +const history = + import.meta.env.VITE_APP_MODE === "electron" + ? createMemoryHistory({ initialEntries: ["/"] }) + : undefined; +const router = createRouter({ + routeTree, + history, + context: { ...TanStackQueryProviderContext }, + defaultViewTransition: true, +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export default function App() { + return ( + + {/* Stitches global styles */} + + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/anonymizer-label-select.tsx b/frontend/src/renderer/src/components/anonymizer/anonymizer-label-select.tsx new file mode 100644 index 00000000..c4d5e774 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/anonymizer-label-select.tsx @@ -0,0 +1,370 @@ +import * as RadixSelect from "@radix-ui/react-select"; +import { CaretDown, Check, MagnifyingGlass } from "phosphor-react"; +import { useEffect, useId, useMemo, useRef, useState } from "react"; + +import type { SelectOption } from "@/components/ui/select"; +import { + ANONYMIZER_CATEGORY_NAMES, + getAnonymizerLabelsForCategory, +} from "@/constants/anonymizer-categories"; +import { css, sva } from "@/styled/css"; +import { styled } from "@/styled/jsx"; +import { stack } from "@/styled/patterns"; +import { normalizeEntityText } from "@/utils/anonymizer/entity-similarity"; + +interface AnonymizerLabelSelectProps { + options: SelectOption[]; + value?: string; + onChange?: (value: SelectOption) => void; + onOpenChange?: (open: boolean) => void; + placeholder?: string; + disabled?: boolean; + size?: "md" | "sm"; + currentCategory?: string; +} + +interface LabelSection { + category: string; + labels: SelectOption[]; +} + +const select = sva({ + slots: [ + "container", + "trigger", + "value", + "caret", + "content", + "search", + "searchInput", + "viewport", + "groupLabel", + "item", + "itemIndicator", + "empty", + ], + base: { + container: { ...stack.raw({ gap: "1" }), width: "full" }, + trigger: { + display: "flex", + alignItems: "center", + gap: "2", + width: "full", + bg: "white", + border: "primary", + rounded: "sm", + cursor: "pointer", + textAlign: "left", + appearance: "none", + textStyle: "label.md.default", + color: "text.default", + + "&[data-state='open']": { + boxShadow: "[0px 2px 2px rgba(0, 0, 0, 0.16)]", + borderColor: "[#110041]", + }, + "&:focus-visible": { + outlineColor: "brand.primary", + outlineWidth: "0.5", + outlineStyle: "solid", + outlineOffset: "0.5", + }, + "&[data-disabled]": { + bg: "bg.primary", + cursor: "not-allowed", + color: "text.lighter", + }, + "&[data-placeholder]": { + color: "text.lighter", + }, + }, + value: { + flex: "[1]", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + caret: { + flexShrink: "0", + color: "text.default", + transition: "[transform 0.15s ease]", + + "[data-state='open'] > &": { + transform: "[rotate(180deg)]", + }, + }, + content: { + bg: "white", + rounded: "sm", + boxShadow: "[0px 8px 16px rgba(0, 0, 0, 0.08)]", + border: "secondary", + overflow: "hidden", + zIndex: "50", + minWidth: "[var(--radix-select-trigger-width)]", + }, + search: { + display: "flex", + alignItems: "center", + gap: "2", + px: "3", + py: "2", + borderBottom: "[1px solid #E5E1DD]", + color: "text.lighter", + }, + searchInput: { + width: "full", + minW: "0", + outline: "none", + color: "text.default", + textStyle: "label.md.default", + "&::placeholder": { + color: "text.lighter", + }, + }, + viewport: { + maxHeight: "[360px]", + overflowY: "auto", + py: "1", + }, + groupLabel: { + px: "3", + pt: "3", + pb: "1", + textStyle: "label.sm.default", + color: "text.lighter", + textTransform: "uppercase", + }, + item: { + display: "flex", + alignItems: "center", + gap: "2", + textStyle: "label.md.default", + color: "text.default", + cursor: "pointer", + outline: "none", + userSelect: "none", + + "&[data-highlighted]": { + bg: "bg.primary-alternative", + }, + "&[data-state='checked']": { + bg: "bg.primary-alternative", + }, + }, + itemIndicator: { + color: "brand.primary", + display: "flex", + alignItems: "center", + flexShrink: "0", + }, + empty: { + px: "3", + py: "4", + textStyle: "label.md.default", + color: "text.lighter", + }, + }, + variants: { + size: { + md: { + trigger: { px: "3", py: "3" }, + item: { px: "3", py: "3" }, + }, + sm: { + trigger: { px: "3", py: "1" }, + item: { px: "3", py: "3" }, + }, + }, + }, + defaultVariants: { + size: "md", + }, +}); + +const technicalId = css({ + color: "text.lighter", + textStyle: "label.sm.default", + whiteSpace: "nowrap", +}); + +function labelMatches(option: SelectOption, query: string) { + const normalizedQuery = normalizeEntityText(query); + if (!normalizedQuery) return true; + + const haystacks = [option.id, option.text, option.shortText ?? ""] + .map((value) => normalizeEntityText(value)) + .filter(Boolean); + + if (haystacks.some((value) => value.includes(normalizedQuery))) return true; + + const labelTokens = haystacks.flatMap((value) => value.split(" ")); + return normalizedQuery + .split(" ") + .every((queryToken) => + labelTokens.some((labelToken) => labelToken.includes(queryToken)), + ); +} + +function buildSections( + options: SelectOption[], + query: string, + currentCategory?: string, +): LabelSection[] { + const optionById = new Map(options.map((option) => [option.id, option])); + const seen = new Set(); + const categories = currentCategory + ? [ + currentCategory, + ...ANONYMIZER_CATEGORY_NAMES.filter( + (category) => category !== currentCategory, + ), + ] + : ANONYMIZER_CATEGORY_NAMES; + + return categories + .map((category) => { + const labels = getAnonymizerLabelsForCategory(category) + .map((label) => optionById.get(label.id)) + .filter((label): label is SelectOption => !!label) + .filter((label) => { + if (seen.has(label.id)) return false; + return labelMatches(label, query); + }); + + labels.forEach((label) => seen.add(label.id)); + + return { category, labels }; + }) + .filter((section) => section.labels.length > 0); +} + +export default function AnonymizerLabelSelect({ + options, + value, + onChange, + onOpenChange, + placeholder = "", + disabled = false, + size = "md", + currentCategory, +}: AnonymizerLabelSelectProps) { + const [query, setQuery] = useState(""); + const [open, setOpen] = useState(false); + const triggerId = useId(); + const searchInputRef = useRef(null); + const classes = select({ size }); + + const selectedOption = options.find((option) => option.id === value); + const sections = useMemo( + () => buildSections(options, query, currentCategory), + [currentCategory, options, query], + ); + + useEffect(() => { + if (!open) return; + const timer = window.setTimeout( + () => searchInputRef.current?.focus({ preventScroll: true }), + 0, + ); + return () => window.clearTimeout(timer); + }, [open]); + + const handleOpenChange = (nextOpen: boolean) => { + setOpen(nextOpen); + onOpenChange?.(nextOpen); + if (!nextOpen) setQuery(""); + }; + + const handleChange = (id: string) => { + const option = options.find((item) => item.id === id); + if (option) onChange?.(option); + }; + + return ( +
+ + + {/* biome-ignore lint/a11y/useSemanticElements lint/a11y/useAriaPropsForRole: Radix merges role, aria-expanded, aria-controls, and tabIndex onto this div at runtime via asChild */} +
+ + {selectedOption?.shortText ?? selectedOption?.text ?? placeholder} + + + +
+
+ + + +
+ + { + setQuery(event.target.value); + window.requestAnimationFrame(() => + searchInputRef.current?.focus({ preventScroll: true }), + ); + }} + onPointerDown={(event) => event.stopPropagation()} + onKeyDownCapture={(event) => { + if (event.key !== "Escape") event.stopPropagation(); + }} + onKeyDown={(event) => { + if (event.key !== "Escape") event.stopPropagation(); + }} + /> +
+ + {sections.length === 0 ? ( +
Sin resultados
+ ) : ( + sections.map((section) => ( + + + {section.category} + + {section.labels.map((option) => ( + + + + + + {option.text} + + {option.id} + + ))} + + )) + )} +
+
+
+
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/config-tab.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/config-tab.tsx new file mode 100644 index 00000000..16c956d9 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/config-tab.tsx @@ -0,0 +1,232 @@ +import { HStack, Stack, styled } from "@/styled/jsx"; +import type { AnonymizerLabels } from "@/types/aymurai"; +import { useState } from "react"; + +import Button from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import Input from "@/components/ui/input"; +import BaseSwitch from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + ANONYMIZER_CATEGORY_NAMES, + getAnonymizerLabelsForCategory, +} from "@/constants/anonymizer-categories"; +import { EXCLUDED_TAGS } from "@/constants/excluded-tags"; +import { + useExcludedTagsConfig, + useExcludedTagsConfigActions, +} from "@/store/useLocal"; +import { css } from "@/styled/css"; +import { Trash } from "phosphor-react"; +import { Label } from "./label"; +import LabelManagerSection from "./section"; + +const iconButton = css({ + cursor: "pointer", + color: "text.lighter", + bg: "transparent", + border: "[1px solid #BCBAB8]", + rounded: "sm", + display: "flex", + alignItems: "center", + justifyContent: "center", + p: "1.5", + flexShrink: "0", + "&:hover": { + color: "white", + bg: "action.hover", + borderColor: "action.hover", + }, +}); + +const tooltipContent = css({ + bg: "action.hover", + color: "white", + px: "1.5", + py: "0.5", + rounded: "sm", + fontSize: "[12px]", + boxShadow: "[none]", +}); + +interface ToggleProps { + name: string; + value: boolean; + onToggle: (value: boolean) => void; +} +function Switch({ name, value, onToggle }: ToggleProps) { + return ( + + + + + {name} + + + + + + Incluir o excluir esta categoría de la anonimización + + + ); +} + +interface LabelConfigTabProps { + expandedSections: Record; + onSectionOpenChange: (section: string, open: boolean) => void; +} + +const EXCLUDED_TERMS_SECTION = "Términos excluidos"; + +export default function LabelConfigTab({ + expandedSections, + onSectionOpenChange, +}: LabelConfigTabProps) { + const { tags: storedTags, words: storedWords } = useExcludedTagsConfig(); + const { setTags, setWords } = useExcludedTagsConfigActions(); + + const [toggles, setToggles] = useState>( + storedTags ?? EXCLUDED_TAGS, + ); + const [excludedWords, setExcludedWords] = useState(storedWords); + const [inputValue, setInputValue] = useState(""); + const [clearDialogOpen, setClearDialogOpen] = useState(false); + + function handleToggle(id: string, value: boolean) { + const next = { ...toggles, [id]: value }; + setToggles(next); + setTags(next as Record); + } + + function handleAddWord() { + const trimmed = inputValue.trim(); + if (!trimmed || excludedWords.includes(trimmed)) return; + const next = [...excludedWords, trimmed]; + setExcludedWords(next); + setWords(next); + setInputValue(""); + } + + function handleRemoveWord(word: string) { + const next = excludedWords.filter((w) => w !== word); + setExcludedWords(next); + setWords(next); + } + + function handleClearAllWords() { + setExcludedWords([]); + setWords([]); + } + + return ( + + + {ANONYMIZER_CATEGORY_NAMES.map((category, index) => ( + + {index > 0 && } + onSectionOpenChange(category, open)} + > + + {getAnonymizerLabelsForCategory(category).map((label) => ( + handleToggle(label.id, value)} + /> + ))} + + + + ))} + + + onSectionOpenChange(EXCLUDED_TERMS_SECTION, open) + } + headerAction={ + excludedWords.length > 0 ? ( + + ) : undefined + } + > + + + setInputValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleAddWord()} + /> + + + {excludedWords.length > 0 && ( + + {excludedWords.map((word) => ( + + ))} + + )} + + + + + + Borrar términos excluidos + + + Esta acción no se puede deshacer. ¿Deseas continuar? + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/entity-tab.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/entity-tab.tsx new file mode 100644 index 00000000..91e6c096 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/entity-tab.tsx @@ -0,0 +1,959 @@ +import AnonymizerLabelSelect from "@/components/anonymizer/anonymizer-label-select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + ANONYMIZER_CATEGORY_NAMES, + getAnonymizerCategoryForLabel, + getLabelsPrioritizingCategory, +} from "@/constants/anonymizer-categories"; +import { useFileDispatch, useFiles } from "@/hooks"; +import { useEntityGroups } from "@/hooks/useEntityGroups"; +import { + mergeGroups, + moveMentionToGroup, + removePredictionValueByCanonicalId, + removePredictionsByCanonicalId, + updatePredictionsByCanonicalId, +} from "@/reducers/file/actions"; +import { useHoverState } from "@/store/useHoverState"; +import { css } from "@/styled/css"; +import { HStack, Stack, styled } from "@/styled/jsx"; +import type { AllLabels, AllLabelsWithSufix } from "@/types/aymurai"; +import { + DndContext, + type DragEndEvent, + type DragOverEvent, + DragOverlay, + type DragStartEvent, + KeyboardSensor, + PointerSensor, + closestCenter, + useDraggable, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + SortableContext, + arrayMove, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { + ArrowsLeftRight, + DotsSixVertical, + PlusCircle, + Trash, + Warning, + XCircle, +} from "phosphor-react"; +import { useEffect, useMemo, useState } from "react"; + +import type { EntityGroup } from "@/hooks/useEntityGroups"; +import { + useExcludedTagsConfig, + useGroupOrder, + useGroupOrderActions, +} from "@/store/useLocal"; +import { normalizeEntityText } from "@/utils/anonymizer/entity-similarity"; +import { filterActivePredictions } from "@/utils/anonymizer/predictions"; +import MergeDialog from "./merge-dialog"; +import RemoveDialog from "./remove-dialog"; +import LabelManagerSection from "./section"; +import SortableGroup from "./sortable-group"; + +function normalizeText(t: string) { + return normalizeEntityText(t); +} + +const GROUP_PREFIX = "group:"; +const TEXT_PREFIX = "text:"; + +function groupDndId(id: string) { + return `${GROUP_PREFIX}${id}`; +} +function textDndId(canonicalId: string, normText: string) { + return `${TEXT_PREFIX}${canonicalId}:::${normText}`; +} +function parseGroupDndId(id: string): string | null { + return id.startsWith(GROUP_PREFIX) ? id.slice(GROUP_PREFIX.length) : null; +} +function parseTextDndId( + id: string, +): { canonicalId: string; text: string } | null { + if (!id.startsWith(TEXT_PREFIX)) return null; + const rest = id.slice(TEXT_PREFIX.length); + const sep = rest.indexOf(":::"); + if (sep === -1) return null; + return { canonicalId: rest.slice(0, sep), text: rest.slice(sep + 3) }; +} + +const iconButton = css({ + cursor: "pointer", + color: "text.lighter", + bg: "transparent", + border: "[1px solid #BCBAB8]", + rounded: "sm", + display: "flex", + alignItems: "center", + justifyContent: "center", + p: "1.5", + flexShrink: "0", + "&:hover": { + color: "white", + bg: "action.hover", + borderColor: "action.hover", + }, +}); + +const suffixBadge = css({ + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + px: "1.5", + py: "0.5", + bg: "[#E6E8FF]", + color: "[#3F479D]", + rounded: "xs", + fontVariantNumeric: "tabular-nums", + userSelect: "none", + flexShrink: "1", + minW: "0", + maxW: "full", + overflowWrap: "anywhere", + wordBreak: "break-word", + whiteSpace: "normal", + textAlign: "right", + fontSize: "[11px]", + fontWeight: "bold", + letterSpacing: "wide", +}); + +const textItemSplitHighlight = { + bg: "[#D9DCFF]", + boxShadow: "[inset 0 0 0 1px #8A92D8]", +} as const; + +const textItemConfirmingSplitHighlight = { + bg: "[#C8CDF8]", + boxShadow: "[inset 0 0 0 2px #6F78CE]", +} as const; + +const textItemStyle = css({ + display: "flex", + alignItems: "flex-start", + gap: "1", + px: "1.5", + py: "0.5", + bg: "bg.primary-alternative", + rounded: "xs", + minW: "0", + w: "full", + boxSizing: "border-box", + "&:hover": textItemSplitHighlight, +}); + +const textItemDraggingStyle = css({ opacity: "0.35" }); +const textItemPendingSplitStyle = css(textItemSplitHighlight); +const textItemConfirmingSplitStyle = css({ + ...textItemConfirmingSplitHighlight, + "&:hover": textItemConfirmingSplitHighlight, +}); + +const textContextMenu = css({ + position: "fixed", + zIndex: "60", + minW: "[150px]", + bg: "bg.primary", + border: "[1px solid #BCBAB8]", + rounded: "sm", + boxShadow: "[0_8px_20px_rgba(17,0,65,0.14)]", + p: "1", +}); + +const textContextMenuButton = css({ + display: "flex", + alignItems: "center", + gap: "1.5", + w: "full", + px: "2", + py: "1.5", + bg: "transparent", + border: "none", + rounded: "xs", + cursor: "pointer", + color: "text.default", + textStyle: "label.md.default", + textAlign: "left", + "&:hover": { + bg: "[#D9DCFF]", + color: "[#1B0D58]", + }, + "&:focus-visible": { + outline: "[2px solid #3F479D]", + outlineOffset: "[1px]", + }, +}); + +const groupCardStyle = css({ + bg: "bg.primary", + border: "[1px solid #BCBAB8]", + rounded: "sm", + minW: "0", + w: "full", +}); + +// Used for both drag-over drop target and hover — same inset outline + violet bg +const groupHoverStyle = css({ + bg: "bg.primary-alternative", + boxShadow: "[inset 0 0 0 2px #3F479D]", +}); + +const duplicateBadge = css({ + display: "inline-flex", + alignItems: "center", + gap: "1", + px: "1.5", + py: "0.5", + bg: "[#ECEEFF]", + color: "[#3F479D]", + border: "[1px solid #7B84D4]", + rounded: "xs", + fontSize: "[11px]", + fontWeight: "semibold", + cursor: "pointer", + maxW: "full", + minW: "0", + overflow: "hidden", + "& > span": { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + "&:hover": { bg: "[#D8DBFF]" }, +}); + +const groupHeader = css({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + gap: "1", + minW: "0", +}); + +const groupHeaderLeft = css({ + display: "flex", + alignItems: "center", + minW: "0", + flex: "1", + overflow: "hidden", +}); + +interface DraggableTextChipProps { + canonicalId: string; + normalizedText: string; + displayText: string; + onRemove: () => void; + onCreateGroup: (position: { x: number; y: number }) => void; + canCreateGroup: boolean; + isSplitTarget: boolean; + isSplitActionHovered: boolean; +} + +function DraggableTextChip({ + canonicalId, + normalizedText, + displayText, + onRemove, + onCreateGroup, + canCreateGroup, + isSplitTarget, + isSplitActionHovered, +}: DraggableTextChipProps) { + const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ + id: textDndId(canonicalId, normalizedText), + data: { type: "text", canonicalId, text: normalizedText, displayText }, + }); + + return ( +
{ + if (!canCreateGroup) return; + event.preventDefault(); + event.stopPropagation(); + onCreateGroup({ x: event.clientX, y: event.clientY }); + }} + > + + + {displayText} + + +
+ ); +} + +function TextChipOverlay({ text }: { text: string }) { + return ( +
+ + {text} +
+ ); +} + +interface LabelEntityTabProps { + expandedGroups: Record; + expandedSections: Record; + onDerivedGroupRemove: (canonicalId: string) => void; + onDerivedValueRemove: (canonicalId: string, value: string) => void; + onDerivedLabelChange: ( + canonicalId: string, + labelId: string | undefined, + ) => void; + onGroupOpenChange: (canonicalId: string, open: boolean) => void; + onSectionOpenChange: (section: string, open: boolean) => void; +} + +export default function LabelEntityTab({ + expandedGroups, + expandedSections, + onDerivedGroupRemove, + onDerivedValueRemove, + onDerivedLabelChange, + onGroupOpenChange, + onSectionOpenChange, +}: LabelEntityTabProps) { + const files = useFiles(); + const dispatch = useFileDispatch(); + const { tags, words } = useExcludedTagsConfig(); + const activeFiles = useMemo( + () => + files.map((file) => ({ + ...file, + predictions: filterActivePredictions(file.predictions, tags, words), + })), + [files, tags, words], + ); + const groups = useEntityGroups(activeFiles); + const { + hoveredCanonicalId, + lastEditedCanonicalId, + setHoveredCanonicalId, + setLastEditedCanonicalId, + } = useHoverState(); + + const { groupOrder } = useGroupOrder(); + const { setGroupOrder } = useGroupOrderActions(); + + const [pendingRemoval, setPendingRemoval] = useState<{ + canonicalId: string; + label: string; + } | null>(null); + const [pendingMerge, setPendingMerge] = useState<{ + source: EntityGroup; + target: EntityGroup; + } | null>(null); + const [activeTextDrag, setActiveTextDrag] = useState<{ + normalizedText: string; + displayText: string; + fromCanonicalId: string; + } | null>(null); + const [overGroupId, setOverGroupId] = useState(null); + const [textMenu, setTextMenu] = useState<{ + x: number; + y: number; + canonicalId: string; + normalizedText: string; + } | null>(null); + const [textMenuActionHovered, setTextMenuActionHovered] = useState(false); + + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const groupsByCategory = useMemo(() => { + const result: Record = Object.fromEntries( + ANONYMIZER_CATEGORY_NAMES.map((c) => [c, []]), + ); + for (const g of groups) { + const cat = getAnonymizerCategoryForLabel(g.renderBase); + result[cat] ??= []; + result[cat].push(g); + } + return result; + }, [groups]); + + useEffect(() => { + if (!textMenu) return; + + const close = () => { + setTextMenu(null); + setTextMenuActionHovered(false); + }; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") close(); + }; + + window.addEventListener("click", close); + window.addEventListener("contextmenu", close); + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("scroll", close, true); + + return () => { + window.removeEventListener("click", close); + window.removeEventListener("contextmenu", close); + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("scroll", close, true); + }; + }, [textMenu]); + + function getOrderedGroups(category: string): EntityGroup[] { + const catGroups = groupsByCategory[category] ?? []; + const order = groupOrder?.[category]; + if (!order) return catGroups; + const byId = new Map(catGroups.map((g) => [g.canonicalId, g])); + const ordered: EntityGroup[] = []; + for (const id of order) { + const g = byId.get(id); + if (g) { + ordered.push(g); + byId.delete(id); + } + } + for (const g of byId.values()) ordered.push(g); + return ordered; + } + + function handleDragStart(event: DragStartEvent) { + const data = event.active.data.current; + if (data?.type === "text") { + setActiveTextDrag({ + normalizedText: data.text, + displayText: data.displayText ?? data.text, + fromCanonicalId: data.canonicalId, + }); + } + } + + function handleDragOver(event: DragOverEvent) { + if (!activeTextDrag) { + setOverGroupId(null); + return; + } + const overId = event.over?.id as string | undefined; + if (!overId) { + setOverGroupId(null); + return; + } + const groupId = + parseGroupDndId(overId) ?? parseTextDndId(overId)?.canonicalId ?? null; + setOverGroupId(groupId); + } + + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event; + setActiveTextDrag(null); + setOverGroupId(null); + if (!over) return; + + const activeId = active.id as string; + const overId = over.id as string; + const textParsed = parseTextDndId(activeId); + + if (textParsed) { + const targetCanonicalId = + parseGroupDndId(overId) ?? parseTextDndId(overId)?.canonicalId ?? null; + if (!targetCanonicalId || targetCanonicalId === textParsed.canonicalId) + return; + const sourceGroup = groups.find( + (g) => g.canonicalId === textParsed.canonicalId, + ); + const targetGroup = groups.find( + (g) => g.canonicalId === targetCanonicalId, + ); + if (!sourceGroup || !targetGroup) return; + for (const mention of sourceGroup.mentions.filter( + (m) => normalizeText(m.text) === normalizeText(textParsed.text), + )) { + dispatch( + moveMentionToGroup( + mention.mentionId, + targetGroup.canonicalId, + targetGroup.renderBase as AllLabels | AllLabelsWithSufix, + ), + ); + } + setLastEditedCanonicalId(targetGroup.canonicalId); + return; + } + + const groupParsed = parseGroupDndId(activeId); + if (groupParsed) { + const targetCanonicalId = parseGroupDndId(overId); + if (!targetCanonicalId || targetCanonicalId === groupParsed) return; + const sourceGroup = groups.find((g) => g.canonicalId === groupParsed); + if (!sourceGroup) return; + const category = getAnonymizerCategoryForLabel(sourceGroup.renderBase); + if (!category) return; + const orderedGroups = getOrderedGroups(category); + const ids = orderedGroups.map((g) => g.canonicalId); + const oldIdx = ids.indexOf(groupParsed); + const newIdx = ids.indexOf(targetCanonicalId); + if (oldIdx === -1 || newIdx === -1) return; + setGroupOrder({ + ...groupOrder, + [category]: arrayMove(ids, oldIdx, newIdx), + }); + } + } + + function handleLabelChange(canonicalId: string, labelId: string | undefined) { + if (!labelId) return; + dispatch( + updatePredictionsByCanonicalId( + canonicalId, + labelId as AllLabels | AllLabelsWithSufix, + ), + ); + onDerivedLabelChange(canonicalId, labelId); + setLastEditedCanonicalId(canonicalId); + } + + function handleRemoveValue(canonicalId: string, normalizedText: string) { + dispatch(removePredictionValueByCanonicalId(canonicalId, normalizedText)); + onDerivedValueRemove(canonicalId, normalizedText); + } + + function handleCreateGroupFromText() { + if (!textMenu) return; + + const sourceGroup = groups.find( + (group) => group.canonicalId === textMenu.canonicalId, + ); + if (!sourceGroup) { + setTextMenu(null); + setTextMenuActionHovered(false); + return; + } + + const targetCanonicalId = crypto.randomUUID(); + const targetLabel = sourceGroup.renderBase as + | AllLabels + | AllLabelsWithSufix; + const mentionsToSplit = sourceGroup.mentions.filter( + (mention) => normalizeText(mention.text) === textMenu.normalizedText, + ); + if (mentionsToSplit.length === 0) { + setTextMenu(null); + setTextMenuActionHovered(false); + return; + } + + for (const mention of mentionsToSplit) { + dispatch( + moveMentionToGroup(mention.mentionId, targetCanonicalId, targetLabel), + ); + } + + const category = getAnonymizerCategoryForLabel(sourceGroup.renderBase); + const existingOrder = groupOrder?.[category]; + if (existingOrder) { + const sourceIndex = existingOrder.indexOf(sourceGroup.canonicalId); + const nextOrder = existingOrder.filter((id) => id !== targetCanonicalId); + nextOrder.splice( + sourceIndex === -1 ? nextOrder.length : sourceIndex + 1, + 0, + targetCanonicalId, + ); + setGroupOrder({ ...groupOrder, [category]: nextOrder }); + } + + setLastEditedCanonicalId(targetCanonicalId); + setTextMenu(null); + setTextMenuActionHovered(false); + } + + function confirmRemoval() { + if (!pendingRemoval) return; + dispatch(removePredictionsByCanonicalId(pendingRemoval.canonicalId)); + onDerivedGroupRemove(pendingRemoval.canonicalId); + setPendingRemoval(null); + } + + function confirmMerge() { + if (!pendingMerge) return; + dispatch( + mergeGroups( + pendingMerge.source.canonicalId, + pendingMerge.target.canonicalId, + pendingMerge.target.renderBase as AllLabels | AllLabelsWithSufix, + ), + ); + setPendingMerge(null); + setLastEditedCanonicalId(null); + } + + return ( + <> + { + if (!open) setPendingRemoval(null); + }} + onConfirm={confirmRemoval} + /> + setPendingMerge(null)} + onConfirm={confirmMerge} + /> + + + {textMenu && ( +
event.stopPropagation()} + onContextMenu={(event) => { + event.preventDefault(); + event.stopPropagation(); + }} + onPointerEnter={() => setTextMenuActionHovered(true)} + onPointerLeave={() => setTextMenuActionHovered(false)} + > + +
+ )} + + + {ANONYMIZER_CATEGORY_NAMES.map((category, i) => { + const orderedGroups = getOrderedGroups(category); + if (orderedGroups.length === 0) return null; + const labelOptions = getLabelsPrioritizingCategory(category); + + return ( + + {i > 0 && } + onSectionOpenChange(category, open)} + > + + + groupDndId(g.canonicalId), + )} + strategy={verticalListSortingStrategy} + > + {orderedGroups.map((group) => { + const isDropTarget = + activeTextDrag !== null && + overGroupId === group.canonicalId; + const duplicatePrimary = group.duplicateOf + ? groups.find( + (g) => g.canonicalId === group.duplicateOf, + ) + : null; + + // When the user recently edited THIS group and it became a primary with a + // duplicate pointing to it, show the badge on this group instead. + const reverseDuplicate = + !group.isDuplicate && + group.canonicalId === lastEditedCanonicalId + ? (groups.find( + (g) => + g.isDuplicate && + g.duplicateOf === group.canonicalId, + ) ?? null) + : null; + const badgeTarget = + reverseDuplicate ?? duplicatePrimary; + const isGroupOpen = + expandedGroups[group.canonicalId] ?? true; + + return ( + + onGroupOpenChange(group.canonicalId, !isGroupOpen) + } + toggleLabel={ + isGroupOpen + ? `Colapsar ${group.renderToken}` + : `Expandir ${group.renderToken}` + } + > + + setHoveredCanonicalId(group.canonicalId) + } + onMouseLeave={() => setHoveredCanonicalId(null)} + > +
+
+ {badgeTarget && ( + + + + + + + Grupo idéntico a{" "} + {badgeTarget.renderToken}. Clic para + unificar. + + + + )} +
+ + {group.renderToken} + +
+ + +
+ + handleLabelChange( + group.canonicalId, + opt.id, + ) + } + /> +
+ +
+ + {isGroupOpen && ( + + {group.uniqueTexts.flatMap((normText, idx) => + group.displayTexts[idx].map((verbatim) => ( + + handleRemoveValue( + group.canonicalId, + normText, + ) + } + canCreateGroup={ + group.mentions.length > 1 + } + onCreateGroup={(position) => { + setTextMenu({ + ...position, + canonicalId: group.canonicalId, + normalizedText: normText, + }); + setTextMenuActionHovered(false); + }} + isSplitTarget={ + textMenu?.canonicalId === + group.canonicalId && + textMenu.normalizedText === normText + } + isSplitActionHovered={ + textMenuActionHovered + } + /> + )), + )} + {group.uniqueTexts.length === 0 && ( + + Sin menciones + + )} + + )} +
+
+ ); + })} +
+
+
+
+ ); + })} +
+ + + {activeTextDrag && ( + + )} + +
+ + ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/index.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/index.tsx new file mode 100644 index 00000000..f556ca9a --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/index.tsx @@ -0,0 +1,190 @@ +import { useState } from "react"; + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useFileDispatch } from "@/hooks"; +import { + removePredictionValueByCanonicalId, + removePredictionsByCanonicalId, + updatePredictionsByCanonicalId, +} from "@/reducers/file/actions"; +import { css, sva } from "@/styled/css"; +import { HStack } from "@/styled/jsx"; +import { stack } from "@/styled/patterns"; +import type { AllLabels, AllLabelsWithSufix } from "@/types/aymurai"; +import LabelConfigTab from "./config-tab"; +import LabelEntityTab from "./entity-tab"; +import LabelManagerTab from "./tab"; + +type ExpandedState = Record; + +const styles = sva({ + slots: ["container", "header", "body", "close"], + base: { + container: { + ...stack.raw({ gap: "0" }), + width: "[400px]", + maxWidth: "[400px]", + h: "full", + minH: "0", + flexShrink: "0", + overflow: "hidden", + overflowX: "hidden", + + bg: "bg.primary", + }, + header: { + px: "5", + py: "6", + pb: "4", + flexShrink: "0", + bg: "bg.primary", + borderBottom: "[1px solid #BCBAB8]", + zIndex: "1", + }, + body: { + px: "5", + pt: "6", + pb: "6", + flex: "1", + minH: "0", + overflowY: "auto", + overflowX: "hidden", + }, + close: { + cursor: "pointer", + }, + }, +}); + +const tooltipContent = css({ + bg: "action.hover", + color: "white", + px: "1.5", + py: "0.5", + rounded: "sm", + fontSize: "[12px]", + boxShadow: "[none]", +}); + +interface LabelManagerProps { + onClose: () => void; +} +export default function LabelManager({ onClose }: LabelManagerProps) { + const [selectedTab, setSelectedTab] = useState<"entity" | "config">("entity"); + const [expandedEntitySections, setExpandedEntitySections] = + useState({}); + const [expandedEntityGroups, setExpandedEntityGroups] = + useState({}); + const [expandedConfigSections, setExpandedConfigSections] = + useState({}); + const dispatch = useFileDispatch(); + + const classes = styles(); + + function handleDerivedGroupRemove(canonicalId: string) { + dispatch(removePredictionsByCanonicalId(canonicalId)); + } + + function handleDerivedValueRemove(canonicalId: string, value: string) { + dispatch(removePredictionValueByCanonicalId(canonicalId, value)); + } + + function handleDerivedLabelChange( + canonicalId: string, + labelId: string | undefined, + ) { + if (!labelId) return; + dispatch( + updatePredictionsByCanonicalId( + canonicalId, + labelId as AllLabels | AllLabelsWithSufix, + ), + ); + } + + function handleEntitySectionOpenChange(section: string, open: boolean) { + setExpandedEntitySections((prev) => ({ ...prev, [section]: open })); + } + + function handleEntityGroupOpenChange(canonicalId: string, open: boolean) { + setExpandedEntityGroups((prev) => ({ ...prev, [canonicalId]: open })); + } + + function handleConfigSectionOpenChange(section: string, open: boolean) { + setExpandedConfigSections((prev) => ({ ...prev, [section]: open })); + } + + return ( +
+ + + + + + setSelectedTab("entity")} + > + Entidades + + + + Revisar, editar y organizar los grupos de entidades reconocidos + + + + + setSelectedTab("config")} + > + Configuración + + + + Elegir qué categorías anonimizar y definir términos excluidos + + + + + + + + + Cerrar gestor de etiquetas + + + + +
+ {selectedTab === "entity" ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/label.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/label.tsx new file mode 100644 index 00000000..45b25748 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/label.tsx @@ -0,0 +1,40 @@ +import { css } from "@/styled/css"; +import { HStack, styled } from "@/styled/jsx"; +import { XCircle } from "phosphor-react"; + +interface LabelProps { + children: string | string[]; + onRemove: () => void; +} +export function Label({ children, onRemove }: LabelProps) { + return ( + + {children} + + + ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/merge-dialog.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/merge-dialog.tsx new file mode 100644 index 00000000..f84a1ce0 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/merge-dialog.tsx @@ -0,0 +1,48 @@ +import Button from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogTitle, +} from "@/components/ui/dialog"; +import type { EntityGroup } from "@/hooks/useEntityGroups"; + +interface MergeDialogProps { + source: EntityGroup | null; + target: EntityGroup | null; + onClose: () => void; + onConfirm: () => void; +} + +export default function MergeDialog({ + source, + target, + onClose, + onConfirm, +}: MergeDialogProps) { + return ( + { + if (!open) onClose(); + }} + > + + Unificar grupos + {source && target && ( +

+ ¿Deseas unificar {source.renderToken} con{" "} + {target.renderToken}?
+ Todas las menciones de {source.renderToken} pasarán a + pertenecer a {target.renderToken} y el grupo original será + eliminado. +

+ )} + + + + +
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/remove-dialog.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/remove-dialog.tsx new file mode 100644 index 00000000..c6d82475 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/remove-dialog.tsx @@ -0,0 +1,36 @@ +import Button from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogTitle, +} from "@/components/ui/dialog"; + +interface RemoveDialogProps { + isOpen: boolean; + label: string; + onClose: (open: boolean) => void; + onConfirm: () => void; +} +export default function RemoveDialog({ + isOpen, + onClose, + onConfirm, + label, +}: RemoveDialogProps) { + return ( + + + Eliminar todas las ocurrencias +

+ ¿Deseas eliminar todas las ocurrencias del grupo con la etiqueta{" "} + {label}? +

+ + + + +
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/section.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/section.tsx new file mode 100644 index 00000000..6055e2e1 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/section.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; + +import { css } from "@/styled/css"; +import { HStack, Stack, styled } from "@/styled/jsx"; +import { CaretUp } from "phosphor-react"; + +const header = css({ + cursor: "pointer", +}); + +const caret = css({ + transition: "transform", + transitionDuration: "[0.2s]", + transitionTimingFunction: "[ease]", +}); + +const contentOuter = css({ + display: "grid", + transition: "[grid-template-rows]", + transitionDuration: "[0.3s]", + transitionTimingFunction: "[ease]", +}); + +const contentInner = css({ + minHeight: "[0]", + overflow: "hidden", +}); + +interface SectionProps { + children: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; + title: string; + headerAction?: React.ReactNode; +} +export default function LabelManagerSection({ + open: controlledOpen, + onOpenChange, + title: sectionTitle, + children, + headerAction, +}: SectionProps) { + const [uncontrolledOpen, setUncontrolledOpen] = useState(true); + const open = controlledOpen ?? uncontrolledOpen; + + function toggleOpen() { + const nextOpen = !open; + onOpenChange?.(nextOpen); + if (controlledOpen === undefined) setUncontrolledOpen(nextOpen); + } + + return ( + + + +
+ +
+ + {sectionTitle} + +
+ {headerAction} +
+
+
{children}
+
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/sortable-group.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/sortable-group.tsx new file mode 100644 index 00000000..988657f8 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/sortable-group.tsx @@ -0,0 +1,103 @@ +import { css } from "@/styled/css"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { DotsSixVertical } from "phosphor-react"; +import { type PointerEvent, type ReactNode, useRef } from "react"; + +const handle = css({ + cursor: "grab", + color: "text.lighter", + display: "flex", + alignItems: "center", + p: "0", + bg: "transparent", + border: "none", + "&:hover": { color: "text.default" }, + "&:active": { cursor: "grabbing" }, +}); + +const wrapper = css({ + display: "flex", + alignItems: "flex-start", + gap: "2", + minW: "0", + w: "full", + transition: "[opacity 0.2s, box-shadow 0.2s]", +}); + +const content = css({ + flex: "1", + minW: "0", +}); + +interface SortableGroupProps { + id: string; + children: ReactNode; + isOpen?: boolean; + onToggleOpen?: () => void; + toggleLabel?: string; +} + +export default function SortableGroup({ + id, + children, + isOpen, + onToggleOpen, + toggleLabel, +}: SortableGroupProps) { + const pointerStartRef = useRef<{ x: number; y: number } | null>(null); + const movedRef = useRef(false); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + boxShadow: isDragging ? "0 4px 12px rgba(0,0,0,0.15)" : "none", + }; + + function handlePointerDown(event: PointerEvent) { + pointerStartRef.current = { x: event.clientX, y: event.clientY }; + movedRef.current = false; + } + + function handlePointerMove(event: PointerEvent) { + const pointerStart = pointerStartRef.current; + if (!pointerStart) return; + + const dx = event.clientX - pointerStart.x; + const dy = event.clientY - pointerStart.y; + if (Math.hypot(dx, dy) > 4) movedRef.current = true; + } + + function handleClick() { + if (movedRef.current) return; + onToggleOpen?.(); + } + + return ( +
+ +
{children}
+
+ ); +} diff --git a/frontend/src/renderer/src/components/anonymizer/label-manager/tab.tsx b/frontend/src/renderer/src/components/anonymizer/label-manager/tab.tsx new file mode 100644 index 00000000..926897e2 --- /dev/null +++ b/frontend/src/renderer/src/components/anonymizer/label-manager/tab.tsx @@ -0,0 +1,33 @@ +import { Tab } from "@/components/tabs"; +import { css } from "@/styled/css"; +import { type ButtonHTMLAttributes, forwardRef } from "react"; + +const button = css({ + cursor: "pointer", +}); + +interface LabelManagerTabProps + extends Omit, "children"> { + children: string; + onClick: () => void; + isSelected: boolean; +} +const LabelManagerTab = forwardRef( + function LabelManagerTab({ onClick, children, isSelected, ...props }, ref) { + return ( + + ); + }, +); + +export default LabelManagerTab; diff --git a/frontend/src/renderer/src/components/arrow/LeftArrow.tsx b/frontend/src/renderer/src/components/arrow/LeftArrow.tsx new file mode 100644 index 00000000..5ba65537 --- /dev/null +++ b/frontend/src/renderer/src/components/arrow/LeftArrow.tsx @@ -0,0 +1,13 @@ +export default function LeftArrow() { + return ( + + + + ); +} diff --git a/frontend/src/renderer/src/components/arrow/RightArrow.tsx b/frontend/src/renderer/src/components/arrow/RightArrow.tsx new file mode 100644 index 00000000..446b8a6f --- /dev/null +++ b/frontend/src/renderer/src/components/arrow/RightArrow.tsx @@ -0,0 +1,16 @@ +export default function RightArrow() { + return ( + + + + ); +} diff --git a/frontend/src/renderer/src/components/arrow/index.ts b/frontend/src/renderer/src/components/arrow/index.ts new file mode 100644 index 00000000..e1939cb8 --- /dev/null +++ b/frontend/src/renderer/src/components/arrow/index.ts @@ -0,0 +1,9 @@ +import LeftArrow from "./LeftArrow"; +import RightArrow from "./RightArrow"; + +const Arrow = { + Right: RightArrow, + Left: LeftArrow, +}; + +export default Arrow; diff --git a/frontend/src/renderer/src/components/brand/built-by.tsx b/frontend/src/renderer/src/components/brand/built-by.tsx new file mode 100644 index 00000000..dd7a4a79 --- /dev/null +++ b/frontend/src/renderer/src/components/brand/built-by.tsx @@ -0,0 +1,29 @@ +import { DATAGENERO_URL } from "@/constants/config"; +import { styled } from "@/styled/jsx"; +import { type StackStyles, stack } from "@/styled/patterns"; +import { useTranslation } from "react-i18next"; + +interface BuiltByProps { + size?: number; + gap?: StackStyles["gap"]; +} +export default function BuiltBy({ size = 150, gap = "2" }: BuiltByProps) { + const { t } = useTranslation(); + return ( + + + {t("platformBuiltBy")} + + DataGenero isologo + + ); +} diff --git a/frontend/src/renderer/src/components/checkbox/checkbox-group/CheckboxGroup.styles.ts b/frontend/src/renderer/src/components/checkbox/checkbox-group/CheckboxGroup.styles.ts new file mode 100644 index 00000000..f727702b --- /dev/null +++ b/frontend/src/renderer/src/components/checkbox/checkbox-group/CheckboxGroup.styles.ts @@ -0,0 +1,30 @@ +import { styled } from "@/styles"; + +export const Legend = styled("legend", { + fontWeight: "$default", + fontSize: "$subtitleSm", + lineHeight: "$subtitleSm", + color: "$textLighter", + + mb: "$xs", +}); + +export const Group = styled("fieldset", { + display: "flex", + gap: "$m", + flexWrap: "wrap", + + variants: { + direction: { + horizontal: { + flexDirection: "row", + }, + vertical: { + flexDirection: "column", + }, + }, + }, + defaultVariants: { + direction: "horizontal", + }, +}); diff --git a/frontend/src/renderer/src/components/checkbox/checkbox-group/index.tsx b/frontend/src/renderer/src/components/checkbox/checkbox-group/index.tsx new file mode 100644 index 00000000..93d4fc96 --- /dev/null +++ b/frontend/src/renderer/src/components/checkbox/checkbox-group/index.tsx @@ -0,0 +1,39 @@ +import { + Children, + type ReactElement, + type ReactNode, + cloneElement, + isValidElement, +} from "react"; + +import type { Props as CheckboxProps } from "../checkbox"; +import { Group, Legend } from "./CheckboxGroup.styles"; + +interface Props { + children: ReactNode; + title?: string; + name: string; + direction?: "horizontal" | "vertical"; +} +export default function CheckboxGroup({ + title, + name, + direction = "horizontal", + children, +}: Props) { + // Inject the name to the children + const checkboxWithName = Children.map(children, (child) => { + if (isValidElement(child)) { + return cloneElement(child as ReactElement, { + name, + }); + } + }); + + return ( + + {title && {title}} + {checkboxWithName} + + ); +} diff --git a/frontend/src/renderer/src/components/checkbox/checkbox/Checkbox.styles.ts b/frontend/src/renderer/src/components/checkbox/checkbox/Checkbox.styles.ts new file mode 100644 index 00000000..c29cec78 --- /dev/null +++ b/frontend/src/renderer/src/components/checkbox/checkbox/Checkbox.styles.ts @@ -0,0 +1,143 @@ +import { styled } from "@/styles"; + +export const Input = styled("input", { + position: "absolute", + top: 0, + left: 0, + + opacity: 0, +}); + +export const Checkbox = styled("div", { + display: "flex", + alignItems: "center", + justifyContent: "center", + + width: 18, + height: 18, + p: 1, + + borderRadius: "$xs", + boxSizing: "border-box", +}); + +export const Wrapper = styled("label", { + // Unchecked + $$checkbox_bg_default: "$colors$white", + $$checkbox_border_default: "$sizes$xxs solid $colors$actionDefaultAlt", + // hover + $$checkbox_bgHover_default: "$colors$white", + $$checkbox_borderHover_default: "1px solid $colors$borderPrimary", + // disabled + $$checkbox_bgDisabled_default: "$colors$actionDisabled", + $$checkbox_borderDisabled_default: "$sizes$xxs solid $colors$borderPrimary", + // focus + $$checkbox_bgFocus_default: "$colors$white", + $$checkbox_borderFocus_default: "1px solid $colors$borderPrimary", + + // Checked + $$checkbox_bg_checked: "$colors$actionDefaultAlt", + $$checkbox_border_checked: "none", + // hover + $$checkbox_bgHover_checked: "$colors$actionHover", + $$checkbox_borderHover_checked: "none", + // disabled + $$checkbox_bgDisabled_checked: "$colors$actionDisabled", + $$checkbox_borderDisabled_checked: "none", + // focus + $$checkbox_bgFocus_checked: "$colors$actionDefaultAlt", + $$checkbox_borderFocus_checked: "none", + + // Focus + $$checkbox_outlineFocus_noText: "3px solid $colors$borderPrimaryAlt", + $$checkbox_outlineFocus_Text: "3px solid $colors$borderPrimary", + + position: "relative", + + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: "$s", + + transitionDuration: "$transitions$s", + transitionProperty: "outline, border-radius, border", + transitionTimingFunction: "ease", + + variants: { + hasText: { + true: { + p: "$s", + "&:focus-within": { + outline: "$$checkbox_outlineFocus_Text", + borderRadius: "3px", + }, + }, + false: { + "&:focus-within": { + outline: "$$checkbox_outlineFocus_noText", + borderRadius: "$xs", + }, + // Focus for checkbox + [`& > input:checked:focus-visible + ${Checkbox}`]: { + bg: "$$checkbox_bgFocus_checked", + b: "$$checkbox_borderFocus_checked", + }, + [`& > input:focus-visible + ${Checkbox}`]: { + bg: "$$checkbox_bgFocus_default", + b: "$$checkbox_borderFocus_default", + }, + }, + }, + isDisabled: { + true: { + "&, & > *": { + cursor: "not-allowed", + }, + }, + false: { + "&, & > *": { + cursor: "pointer", + }, + }, + }, + }, + defaultVariants: { + hasText: false, + isDisabled: false, + }, + + // Checkbox styles + [`& > ${Checkbox}`]: { + b: "$$checkbox_border_default", + bg: "$$checkbox_bg_default", + }, + [`& > input:checked + ${Checkbox}`]: { + bg: "$$checkbox_bg_checked", + border: "$$checkbox_border_checked", + }, + + // Hide SVG when unchecked + [`& > input:not(:checked) + ${Checkbox} svg`]: { + display: "none", + }, + + // Hover + [`&:hover > ${Checkbox}`]: { + b: "$$checkbox_borderHover_default", + bg: "$$checkbox_bgHover_default", + }, + [`&:hover > input:checked + ${Checkbox}`]: { + b: "$$checkbox_borderHover_checked", + bg: "$$checkbox_bgHover_checked", + }, + + // Disabled + [`& > input:disabled + ${Checkbox}`]: { + bg: "$$checkbox_bgDisabled_default", + b: "$$checkbox_borderDisabled_default", + }, + [`& > input:checked:disabled + ${Checkbox}`]: { + bg: "$$checkbox_bgDisabled_checked", + b: "$$checkbox_borderDisabled_checked", + }, +}); diff --git a/frontend/src/renderer/src/components/checkbox/checkbox/index.tsx b/frontend/src/renderer/src/components/checkbox/checkbox/index.tsx new file mode 100644 index 00000000..37568f73 --- /dev/null +++ b/frontend/src/renderer/src/components/checkbox/checkbox/index.tsx @@ -0,0 +1,65 @@ +import { Check } from "phosphor-react"; +import { + type ChangeEventHandler, + type ReactNode, + forwardRef, + useImperativeHandle, + useState, +} from "react"; + +import type { CSS } from "@/styles"; +import { colors } from "@/styles/tokens"; +import { Input, Checkbox as StyledCheckbox, Wrapper } from "./Checkbox.styles"; + +export interface Props { + children?: ReactNode; + disabled?: boolean; + checked?: boolean; + onChange?: (value: boolean) => void; + name?: string; + css?: CSS; +} +export default forwardRef<{ value: boolean }, Props>(function Checkbox( + { disabled = false, checked = false, name, onChange, children, css }, + ref, +) { + const [isChecked, setIsChecked] = useState(checked); + + const hasText = !!children; + + // Only exposes `value` object to the parent component + useImperativeHandle( + ref, + () => { + return { + value: isChecked, + }; + }, + [isChecked], + ); + + const handleToggle: ChangeEventHandler = (e) => { + setIsChecked(e.target.checked); + onChange?.(isChecked); + }; + + const iconColor = disabled + ? colors.textOnButtonDisabled + : colors.textOnButtonAlternative; + + return ( + + + + + + {children} + + ); +}); diff --git a/frontend/src/renderer/src/components/checkbox/index.ts b/frontend/src/renderer/src/components/checkbox/index.ts new file mode 100644 index 00000000..eebc899c --- /dev/null +++ b/frontend/src/renderer/src/components/checkbox/index.ts @@ -0,0 +1,4 @@ +import Checkbox from "./checkbox"; +import CheckboxGroup from "./checkbox-group"; + +export { Checkbox, CheckboxGroup }; diff --git a/frontend/src/renderer/src/components/decision-tabs/DecisionTabs.styles.ts b/frontend/src/renderer/src/components/decision-tabs/DecisionTabs.styles.ts new file mode 100644 index 00000000..0dd8c7a1 --- /dev/null +++ b/frontend/src/renderer/src/components/decision-tabs/DecisionTabs.styles.ts @@ -0,0 +1,16 @@ +import { styled } from "@/styles"; + +export const PlusButton = styled("button", { + boxShadow: "4px 0px 4px rgba(0, 0, 0, 0.05)", + + // Marked as important because of Stitches hierarchy + px: "$s !important", + py: "$s !important", + // b: '2px solid $actionDefaultAlt', + width: 48, + height: 51, + "&:disabled": { + boxShadow: "none", + color: "$textLighter", + }, +}); diff --git a/frontend/src/renderer/src/components/decision-tabs/index.tsx b/frontend/src/renderer/src/components/decision-tabs/index.tsx new file mode 100644 index 00000000..a6520c8d --- /dev/null +++ b/frontend/src/renderer/src/components/decision-tabs/index.tsx @@ -0,0 +1,52 @@ +import { Tab, TabName } from "@/components"; +import { css } from "@/styled/css"; +import { Stack } from "@/styled/jsx"; +import nArray from "@/utils/nArray"; +import { Plus } from "phosphor-react"; + +const button = css({ + cursor: "pointer", + p: "2", + display: "flex", + alignItems: "center", + justifyContent: "center", + + height: "full", + width: "12", +}); + +interface Props { + selected: number; + decisionAmount: number; + addDecision: () => void; + selectDecision: (n: number) => void; +} +export default function DecisionTabs({ + selected, + decisionAmount, + addDecision, + selectDecision, +}: Props) { + const decisionArr = nArray(decisionAmount, undefined).map((_, i) => i); + + const selectDecisionHandler = (n: number) => () => selectDecision(n); + + return ( + + {decisionArr.map((dec) => ( + + Decisión {dec + 1} + + ))} + + + ); +} diff --git a/frontend/src/renderer/src/components/dialog/index.tsx b/frontend/src/renderer/src/components/dialog/index.tsx new file mode 100644 index 00000000..16c2539c --- /dev/null +++ b/frontend/src/renderer/src/components/dialog/index.tsx @@ -0,0 +1,109 @@ +import { styled } from "@/styles"; +import { X } from "phosphor-react"; +import type { FC, ReactNode } from "react"; +import Button from "../ui/button"; + +export interface DialogOption { + id: string; + label: string; + action: () => void; +} + +interface DialogProps { + isOpen: boolean; + title?: string; + message?: string; + options?: DialogOption[]; + onClose: () => void; + children?: ReactNode; +} + +export const Overlay = styled("div", { + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 0, 0, 0.5)", + display: "flex", + alignItems: "center", + justifyContent: "center", + zIndex: 1000, +}); + +export const DialogContainer = styled("div", { + backgroundColor: "$white", + borderRadius: "$xs", + padding: "$l", + minWidth: 300, + maxWidth: 700, + boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.1)", +}); + +export const DialogHeader = styled("div", { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + marginBottom: "$m", +}); + +export const DialogTitle = styled("h3", { + fontSize: "$titleMd", + fontWeight: 600, + color: "$textDefault", + margin: 0, +}); + +export const DialogMessage = styled("label", { + color: "$textDefault", + marginBottom: "$l", +}); + +export const DialogButtons = styled("div", { + display: "flex", + gap: "$s", + justifyContent: "flex-end", + alignItems: "center", + marginTop: "$xl", +}); + +const Dialog: FC = ({ + isOpen, + title = "", + message, + options, + onClose, + children, +}) => { + if (!isOpen) return null; + + return ( + + e.stopPropagation()}> + + {title} + + + {message && {message}} + {children} + {options && options.length > 0 && ( + + {options.map((option, index) => ( + + ))} + + )} + + + ); +}; + +export default Dialog; diff --git a/frontend/src/renderer/src/components/drop-area.tsx b/frontend/src/renderer/src/components/drop-area.tsx new file mode 100644 index 00000000..4226066f --- /dev/null +++ b/frontend/src/renderer/src/components/drop-area.tsx @@ -0,0 +1,153 @@ +import { WHITELISTED_EXTENSIONS } from "@/constants/config"; +import { css, cva } from "@/styled/css"; +import { Stack, styled } from "@/styled/jsx"; +import { File } from "phosphor-react"; +import { useCallback, useRef, useState } from "react"; + +function FileIcon() { + return ( + + + + ); +} + +const styles = cva({ + base: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + gap: "8", + + w: "full", + h: "[335px]", + p: "8", + + rounded: "sm", + border: "primary", + + bg: "[#F3F4FF]", + cursor: "pointer", + + transitionProperty: "[border-color, background-color, box-shadow]", + transitionTimingFunction: "default", + transitionDuration: "normal", + }, + variants: { + dragging: { + true: { + borderColor: "brand.primary", + bg: "bg.primary-alternative", + boxShadow: "[0px 0px 15px 0px #3F479D66]", + }, + }, + }, +}); + +interface DropAreaProps { + onDropFiles: (files: File[]) => void; + title: string; + description: string; + multiple?: boolean; +} + +export default function DropArea({ + onDropFiles, + title, + description, + multiple = true, +}: DropAreaProps) { + const [dragging, setDragging] = useState(false); + const inputRef = useRef(null); + const dragCounter = useRef(0); + + const accept = WHITELISTED_EXTENSIONS.map((ext) => `.${ext}`).join(","); + + const handleFiles = useCallback( + (fileList: FileList) => { + const files = Array.from(fileList).filter((file) => { + const ext = file.name.split(".").pop()?.toLowerCase(); + return ext && WHITELISTED_EXTENSIONS.includes(ext); + }); + if (files.length > 0) onDropFiles(files); + }, + [onDropFiles], + ); + + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current++; + setDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current--; + if (dragCounter.current === 0) setDragging(false); + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current = 0; + setDragging(false); + if (e.dataTransfer.files.length > 0) { + handleFiles(e.dataTransfer.files); + } + }, + [handleFiles], + ); + + const handleClick = () => inputRef.current?.click(); + + const handleChange: React.ChangeEventHandler = (e) => { + if (e.target.files) handleFiles(e.target.files); + }; + + return ( +
+ + + + {title} + + + {description} + + + +
+ ); +} diff --git a/frontend/src/renderer/src/components/feature-icon.tsx b/frontend/src/renderer/src/components/feature-icon.tsx new file mode 100644 index 00000000..d83f1d5b --- /dev/null +++ b/frontend/src/renderer/src/components/feature-icon.tsx @@ -0,0 +1,65 @@ +import { cva } from "@/styled/css"; +import { stack } from "@/styled/patterns"; +import type { FeatureFlowEnum } from "@/types/features"; +import { FEATURE_ICON } from "@/utils/config"; +import type { Icon } from "phosphor-react"; + +const styles = cva({ + base: { + ...stack.raw({ align: "center", justify: "center" }), + p: "4", + borderRadius: "[14px]", + }, + variants: { + disabled: { + true: { + bg: "bg.secondary", + color: "text.lighter", + }, + false: { + color: "text.default", + bg: "bg.primary-alternative", + }, + }, + size: { + lg: { + width: "[70px]", + height: "[70px]", + p: "[14px]", + rounded: "[14px]", + }, + sm: { + width: "10", + height: "10", + p: "2", + rounded: "lg", + }, + }, + }, + defaultVariants: { + disabled: false, + size: "lg", + }, +}); + +type FeatureIconProps = { + size: "lg" | "sm"; + disabled?: boolean; +} & ( + | { feature: FeatureFlowEnum; icon?: never } + | { feature?: never; icon: Icon } +); + +export default function FeatureIcon({ + feature, + size, + icon: OverrideIcon, + disabled = false, +}: FeatureIconProps) { + const Icon = OverrideIcon ?? FEATURE_ICON[feature]; + return ( +
+ +
+ ); +} diff --git a/frontend/src/renderer/src/components/features-menu.tsx b/frontend/src/renderer/src/components/features-menu.tsx new file mode 100644 index 00000000..3a158668 --- /dev/null +++ b/frontend/src/renderer/src/components/features-menu.tsx @@ -0,0 +1,64 @@ +import { useFileDispatch } from "@/hooks/useFiles"; +import { removeAllFiles } from "@/reducers/file/actions"; +import { Grid, Stack, styled } from "@/styled/jsx"; +import { FeatureFlowEnum, featureNamespace, getFeatureRouteSlug } from "@/types/features"; +import { Link } from "@tanstack/react-router"; +import { DotsNine, Gear } from "phosphor-react"; +import { useTranslation } from "react-i18next"; +import FeatureIcon from "./feature-icon"; +import Button from "./ui/button"; +import Card from "./ui/card"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; + +export default function FeaturesMenu() { + const { t } = useTranslation(); + const dispatch = useFileDispatch(); + const features = Object.values(FeatureFlowEnum); + + const handleClearFiles = () => { + dispatch(removeAllFiles()); + }; + + return ( + + + + + + + {features.map((feature) => ( + + + + + + {t("title", { ns: featureNamespace[feature] })} + + + + + ))} + + + + + + + {t("settings")} + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/file-annotator/FileAnnotator.styles.ts b/frontend/src/renderer/src/components/file-annotator/FileAnnotator.styles.ts new file mode 100644 index 00000000..5c825801 --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/FileAnnotator.styles.ts @@ -0,0 +1,30 @@ +import { styled } from "@/styles"; + +export const Container = styled("div", { + flex: 1, + minWidth: 0, + // boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)", + + zIndex: 1, + display: "flex", + flexDirection: "column", + overflow: "hidden", +}); + +export const File = styled("div", { + flex: 1, + overflowY: "scroll", + px: "$xl", + pb: "$xl", + // pt: "$l", + + "& p, & span, & em": { + fontFamily: "$file", + fontSize: 16, + lineHeight: "160%", + }, +}); + +export const Paragraph = styled("p", { + margin: "8px 0px", +}); diff --git a/frontend/src/renderer/src/components/file-annotator/SearchBar/Counter.tsx b/frontend/src/renderer/src/components/file-annotator/SearchBar/Counter.tsx new file mode 100644 index 00000000..93c909ed --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/SearchBar/Counter.tsx @@ -0,0 +1,66 @@ +import { css } from "@/styled/css"; +import { Stack, styled } from "@/styled/jsx"; +import { + CaretDown as NextIcon, + CaretUp as PreviousIcon, + XCircle, +} from "phosphor-react"; + +import { Button } from "@/components"; + +const counterClass = css({ + display: "flex", + alignItems: "center", + gap: "1", +}); + +interface Props { + count: number; + cursor: number; + next: () => void; + previous: () => void; + clear: () => void; + isSearching: boolean; +} + +export const Counter = ({ + count, + previous, + next, + cursor, + clear, + isSearching, +}: Props) => { + if (!isSearching) return null; + + return ( +
+ + + + + {count === 0 ? "0 ocurrencias" : `${cursor} de ${count}`} + + + + + +
+ ); +}; diff --git a/frontend/src/renderer/src/components/file-annotator/SearchBar/SearchBar.styles.ts b/frontend/src/renderer/src/components/file-annotator/SearchBar/SearchBar.styles.ts new file mode 100644 index 00000000..81c2e7eb --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/SearchBar/SearchBar.styles.ts @@ -0,0 +1,94 @@ +import { styled } from "@/styles"; + +export const WrapperSearch = styled("div", { + display: "flex", + alignItems: "center", + flexDirection: "row", + gap: "$s", + + p: 12, + height: 48, + + bg: "$bgSecondary", + border: "1px solid $borderPrimary", + borderRadius: "$xl", + + cursor: "text", + + "&:focus-within": { + border: "1px solid $borderPrimaryAlt", + boxShadow: "0px 2px 2px rgba(0, 0, 0, 0.16)", + }, +}); + +export const ContainerLabel = styled("div", { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + gap: "$s", + width: "100%", +}); + +export const WrapperLabel = styled("div", { + width: "100%", +}); + +export const WrapperSufixLabel = styled("div", { + display: "flex", + alignItems: "center", + flexDirection: "row", + gap: "$s", + + p: 12, + height: 48, + + bg: "$bgSecondary", + border: "1px solid $borderPrimary", + + cursor: "text", + + "&:focus-within": { + border: "1px solid $borderPrimaryAlt", + boxShadow: "0px 2px 2px rgba(0, 0, 0, 0.16)", + }, +}); + +export const InputContainer = styled("div", { + display: "flex", + alignItems: "center", + gap: "$xxs", + flex: 1, +}); + +export const Input = styled("input", { + flex: 1, + + fontSize: "$labelMd", + lineHeight: "$LabelMd", + color: "$textDefault", + + p: 0, + width: "$xxl", + + border: "none", + outline: "none", +}); + +export const Counter = styled("div", { + display: "flex", + alignItems: "center", + gap: "$xxs", +}); + +export const SuggestionContainer = styled("div", { + display: "flex", + gap: "$s", +}); + +export const Suggestion = styled("mark", { + backgroundColor: "$bgSecondaryAlt", + fontFamily: "$primary", + padding: "0px $sizes$s", + borderRadius: "$s", + cursor: "pointer", +}); diff --git a/frontend/src/renderer/src/components/file-annotator/SearchBar/index.tsx b/frontend/src/renderer/src/components/file-annotator/SearchBar/index.tsx new file mode 100644 index 00000000..c8c66b2d --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/SearchBar/index.tsx @@ -0,0 +1,198 @@ +import { + type ChangeEvent, + type KeyboardEvent, + useEffect, + useRef, + useState, +} from "react"; + +import AnonymizerLabelSelect from "@/components/anonymizer/anonymizer-label-select"; +import Button from "@/components/ui/button"; +import type { SelectOption } from "@/components/ui/select"; +import { useExcludedTagsConfig } from "@/store/useLocal"; +import { sva } from "@/styled/css"; +import { Grid, HStack, styled } from "@/styled/jsx"; +import { hstack } from "@/styled/patterns"; +import { getActiveAnonymizerLabelOptions } from "@/utils/anonymizer/labels"; +import { MagnifyingGlass } from "phosphor-react"; +import { SEARCH_MIN_LENGTH } from "../annotations"; +import { Counter } from "./Counter"; + +const searchClasses = sva({ + slots: ["searchBar", "input", "verticalHr"], + base: { + searchBar: { + ...hstack.raw({ alignItems: "center", gap: "2" }), + height: "12", + p: "3", + rounded: "3xl", + // minWidth: "[450px]", + width: "full", + border: "primary", + }, + input: { + outline: "none", + width: "full", + }, + verticalHr: { + width: "[1px]", + alignSelf: "stretch", + borderWidth: "0", + backgroundColor: "[#BCBAB8]", + height: "12", + }, + }, +}); + +interface Props { + isAnnotable?: boolean; + isLabelManagerOpen: boolean; + onSearchChange?: (value: string) => void; + onLabelChange?: (object: SelectOption | undefined) => void; + labelValue?: string; + onLabelManagerToggle: () => void; + matchesCount: number; + activeIndex: number | null; + onNext: () => void; + onPrevious: () => void; + onFocusDocument: () => void; +} + +export const SearchBar = ({ + isAnnotable = false, + isLabelManagerOpen, + onSearchChange, + onLabelChange, + labelValue, + onLabelManagerToggle, + matchesCount, + activeIndex, + onNext, + onPrevious, + onFocusDocument, +}: Props) => { + const [search, setSearch] = useState(""); + const { tags } = useExcludedTagsConfig(); + + const inputSearchRef = useRef(null); + + const classes = searchClasses(); + const labelOptions = getActiveAnonymizerLabelOptions(tags); + + useEffect(() => { + const isEditableTarget = (target: EventTarget | null) => { + if (!(target instanceof HTMLElement)) return false; + if (target === inputSearchRef.current) return false; + if (target.isContentEditable) return true; + return ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName); + }; + + const handleKeyDown = (event: globalThis.KeyboardEvent) => { + const isSearchShortcut = + (event.ctrlKey || event.metaKey) && + !event.altKey && + !event.shiftKey && + event.key.toLowerCase() === "b"; + + if (!isSearchShortcut || isEditableTarget(event.target)) return; + + event.preventDefault(); + inputSearchRef.current?.focus(); + inputSearchRef.current?.select(); + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); + + const changeSearchHandler = (e: ChangeEvent) => { + const text = e.target.value; + setSearch(text); + onSearchChange?.(e.target.value); + }; + + const clickSearchHandler = () => { + if (inputSearchRef.current) { + inputSearchRef.current.select(); + } + }; + + const handleClear = () => { + setSearch(""); + onSearchChange?.(""); + }; + + const handleInputKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + event.preventDefault(); + handleClear(); + onFocusDocument(); + }; + + const searchFocus = () => inputSearchRef.current?.focus(); + + const changeLabelSelectHandler = (e: SelectOption | undefined) => { + onLabelChange?.(e); + }; + + return ( + +
+ + + + + = SEARCH_MIN_LENGTH} + /> +
+ {isAnnotable && ( + +
+ + Aplicar etiquetas + +
+ +
+ {!isLabelManagerOpen && ( + <> +
+ + + )} +
+ )} +
+ ); +}; diff --git a/frontend/src/renderer/src/components/file-annotator/annotations.ts b/frontend/src/renderer/src/components/file-annotator/annotations.ts new file mode 100644 index 00000000..de14b733 --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/annotations.ts @@ -0,0 +1,171 @@ +import type { + AllLabels, + AllLabelsWithSufix, + PredictLabel, +} from "@/types/aymurai"; +import type { Paragraph } from "@/types/file"; +import { includes } from "@/utils/regex"; +import type { Annotation } from "./types"; + +export const SEARCH_MIN_LENGTH = 3; + +export interface SearchMatch { + id: string; + paragraphId: string; + start: number; + end: number; + index: number; +} + +/** + * Converts a predict label to an annotation + * @param labels List of predict labels from the API + * @returns List of annotations + */ +const labelToAnnotation = (labels: PredictLabel[]): Annotation[] => { + return labels.map( + ({ start_char, end_char, attrs, paragraphId, mentionId }) => ({ + start: start_char, + end: end_char, + type: "tag", + tag: attrs.aymurai_label, + paragraphId, + canonical_entity_id: attrs.canonical_entity_id, + mentionId, + }), + ); +}; + +/** + * Finds the indexes where the search string is found in the paragraph + * @param paragraph Paragraph to search in + * @param search Search string + * @returns List of indexes where the search string is found + */ +const findSearchIndexes = (paragraph: string, search: string) => { + const regex = includes(search); + + const indexes: number[] = []; + let match = regex.exec(paragraph); + + while (match) { + indexes.push(match.index); + match = regex.exec(paragraph); + } + + return indexes; +}; + +/** + * Appends search annotations to the list of label annotations + * @param arr List of label annotations + * @param search Search string + * @param paragraph Paragraph to search in + * @returns List of annotations with search annotations appended + */ +const getSearchAnnotations = ( + searchMatches: SearchMatch[], + label: AllLabels | AllLabelsWithSufix | null, + activeSearchMatchId: string | null, +): Annotation[] => { + return searchMatches.map( + (match) => + ({ + start: match.start, + end: match.end, + type: "search", + tag: label, + paragraphId: match.paragraphId, + searchMatchId: match.id, + searchIndex: match.index, + isActive: match.id === activeSearchMatchId, + }) as Annotation, + ); +}; + +/** + * Converts the predictions array into a map with the paragraph id as key + * @param predictions List of labels predicted by AymurAI + * @returns A map with the paragraph id as key and an array of predictions as value + */ +export const predictionsToMap = ( + predictions: PredictLabel[], +): Map => { + const map = new Map(); + + for (const token of predictions) { + // Ignore characters + if ([":", ",", ".", ")", "(", "-", "_"].includes(token.text)) continue; + + const paragraphPredictions = map.get(token.paragraphId); + if (paragraphPredictions) { + paragraphPredictions.push(token); + } else { + map.set(token.paragraphId, [token]); + } + } + + return map; +}; + +export const createSearchMatches = ( + paragraphs: Paragraph[], + search: string, +): SearchMatch[] => { + if (!search || search.length < SEARCH_MIN_LENGTH) return []; + + const matches: SearchMatch[] = []; + paragraphs.forEach((paragraph) => { + const indexes = findSearchIndexes(paragraph.value, search); + indexes.forEach((start) => { + const index = matches.length; + matches.push({ + id: `${paragraph.id}:${start}:${index}`, + paragraphId: paragraph.id, + start, + end: start + search.length, + index, + }); + }); + }); + + return matches; +}; + +export const searchMatchesToMap = ( + matches: SearchMatch[], +): Map => { + const map = new Map(); + + for (const match of matches) { + const paragraphMatches = map.get(match.paragraphId) ?? []; + paragraphMatches.push(match); + map.set(match.paragraphId, paragraphMatches); + } + + return map; +}; + +/** + * Transform the predictions made by AymurAI into annotations. Also append each search result as an annotation. + * @param predictions List of AymurAI predictions + * @param search Search string + * @param paragraph Paragraph to analyze + * @param searchLabel Label to assign to the search annotations + * @returns List of annotations ready to be displayed + */ +export const createAnnotationsWithSearch = ( + predictions: PredictLabel[], + searchMatches: SearchMatch[], + searchLabel: AllLabels | AllLabelsWithSufix | null, + activeSearchMatchId: string | null, +): Annotation[] => { + const matchingAnnotations = labelToAnnotation(predictions); + const searchAnnotations = getSearchAnnotations( + searchMatches, + searchLabel, + activeSearchMatchId, + ); + + return [...matchingAnnotations, ...searchAnnotations]; +}; diff --git a/frontend/src/renderer/src/components/file-annotator/generateSplits.ts b/frontend/src/renderer/src/components/file-annotator/generateSplits.ts new file mode 100644 index 00000000..24bb977a --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/generateSplits.ts @@ -0,0 +1,119 @@ +import type { Annotation, Split } from "./types"; + +const addTrailingText = (splits: Split[], i: number, { start }: Annotation) => { + if (start > i) { + splits.push({ + end: start, + start: i, + type: "text", + }); + } +}; +const addLeadingText = (splits: Split[], i: number, { length }: string) => { + if (i < length) { + splits.push({ + end: length, + start: i, + type: "text", + }); + } +}; + +const sortTokens = (tokens: Annotation[]) => { + return tokens.sort((a, b) => a.start - b.start); +}; +const rangesOverlap = (a: Annotation, b: Annotation) => { + return a.start < b.end && b.start < a.end; +}; +const isRightConflicting = (splits: Split[], token: Annotation) => { + return (splits.at(-1)?.end ?? 0) > token.start; +}; + +const isLeftConflicting = (token: Annotation, next: Annotation | undefined) => { + if (!next) return false; + + return ( + token.type === "search" && next.type === "tag" && token.end > next.start + ); +}; + +const mergeSearchIntoTags = (tokens: Annotation[]): Annotation[] => { + const tags = tokens.filter((token) => token.type === "tag"); + const searches = tokens.filter((token) => token.type === "search"); + const overlappingSearchIds = new Set(); + + const enrichedTags = tags.map((tag) => { + const overlappingSearches = searches.filter((search) => + rangesOverlap(tag, search), + ); + if (overlappingSearches.length === 0) return tag; + + for (const search of overlappingSearches) { + if (search.searchMatchId) overlappingSearchIds.add(search.searchMatchId); + } + + const activeSearch = + overlappingSearches.find((search) => search.isActive) ?? + overlappingSearches[0]; + + return { + ...tag, + searchMatchId: activeSearch?.searchMatchId, + searchIndex: activeSearch?.searchIndex, + isActive: activeSearch?.isActive ?? false, + }; + }); + + const visibleSearches = searches.filter( + (search) => + !search.searchMatchId || !overlappingSearchIds.has(search.searchMatchId), + ); + + return [...enrichedTags, ...visibleSearches]; +}; + +/** + * Generates a list of splits from a paragraph and a list of annotations + * @param paragraph Paragraph text to split + * @param tokens List of annotations + * @returns List of splits + */ +export const generateSplits = ( + paragraph: string, + tokens: Annotation[], +): Split[] => { + if (!tokens.length) + return [ + { + start: 0, + end: paragraph.length, + type: "text", + }, + ]; + + const sortedTokens = sortTokens(mergeSearchIntoTags(tokens)); + const splits: Split[] = []; + // Represents the last index used to split + let i = 0; + + sortedTokens.forEach((token, tokenIndex) => { + // Avoid token superposition on search tokens + if ( + isRightConflicting(splits, token) || + isLeftConflicting(token, sortedTokens[tokenIndex + 1]) + ) + return; + + // Add trailing text split if necessary + addTrailingText(splits, i, token); + + splits.push(token); + // Move the cursor to the last position + i = token.end; + }); + + // Add a remaining split if the `i` has not reached the end + addLeadingText(splits, i, paragraph); + + return splits; +}; diff --git a/frontend/src/renderer/src/components/file-annotator/index.tsx b/frontend/src/renderer/src/components/file-annotator/index.tsx new file mode 100644 index 00000000..fdce8da6 --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/index.tsx @@ -0,0 +1,269 @@ +import { + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, + useTransition, +} from "react"; + +import { SearchBar } from "./SearchBar"; + +import type { SelectOption } from "@/components/ui/select"; +import AnnotationProvider, { useAnnotation } from "@/context/Annotation"; +import { useExcludedTagsConfig } from "@/store/useLocal"; +import { css } from "@/styled/css"; +import { HStack } from "@/styled/jsx"; +import type { AllLabels, PredictLabel } from "@/types/aymurai"; +import type { DocFile, Paragraph as ParagraphType } from "@/types/file"; +import { getActiveAnonymizerLabelOptions } from "@/utils/anonymizer/labels"; +import { filterActivePredictions } from "@/utils/anonymizer/predictions"; +import LabelManager from "../anonymizer/label-manager"; +import SearchAnnotation from "../file/search-annotation"; +import TagAnnotation from "../file/tag-annotation"; +import * as S from "./FileAnnotator.styles"; +import { + type SearchMatch, + createAnnotationsWithSearch, + createSearchMatches, + predictionsToMap, + searchMatchesToMap, +} from "./annotations"; +import { generateSplits } from "./generateSplits"; + +const labelManagerWrapper = css({ + display: "flex", + h: "full", + minH: "0", + flexShrink: "0", + "&[hidden]": { + display: "none", + }, +}); + +interface ParagraphProps { + children: string; + paragraph: ParagraphType; + predictions: PredictLabel[]; + searchMatches: SearchMatch[]; + activeSearchMatchId: string | null; +} +const Paragraph = memo( + ({ + children, + paragraph, + predictions, + searchMatches, + activeSearchMatchId, + }: ParagraphProps) => { + const { label } = useAnnotation(); + + const annotations = useMemo(() => { + return createAnnotationsWithSearch( + predictions, + searchMatches, + label, + activeSearchMatchId, + ); + }, [predictions, searchMatches, label, activeSearchMatchId]); + + const splits = generateSplits(children, annotations); + + return ( + + {splits.map((s) => { + const content = children.slice(s.start, s.end); + const key = `${s.type}-${s.start}-${s.end}`; + + switch (s.type) { + case "search": + return ( + + {content} + + ); + case "tag": + return ( + + {content} + + ); + case "text": + default: + return ( + + {content} + + ); + } + })} + + ); + }, +); + +interface Props { + file: DocFile; + isAnnotable?: boolean; +} +export default function FileAnnotator({ file, isAnnotable = false }: Props) { + const [search, setSearch] = useState(""); + const [activeSearchIndex, setActiveSearchIndex] = useState( + null, + ); + const [, startTransition] = useTransition(); + const fileRef = useRef(null); + + const [label, setLabel] = useState(null); + const [labelManagerOpen, setLabelManagerOpen] = useState(isAnnotable); + + const paragraphs = file.paragraphs ?? []; + const { tags, words } = useExcludedTagsConfig(); + const activeLabelOptions = useMemo( + () => getActiveAnonymizerLabelOptions(tags), + [tags], + ); + + useEffect(() => { + if (label && !activeLabelOptions.some((option) => option.id === label)) { + setLabel(null); + } + }, [activeLabelOptions, label]); + + const filteredPredictions = useMemo( + () => filterActivePredictions(file.predictions, tags, words), + [file.predictions, tags, words], + ); + + const predictionsMap = useMemo( + () => predictionsToMap(filteredPredictions), + [filteredPredictions], + ); + + const searchMatches = useMemo( + () => createSearchMatches(paragraphs, search), + [paragraphs, search], + ); + + const searchMatchesMap = useMemo( + () => searchMatchesToMap(searchMatches), + [searchMatches], + ); + + const activeSearchMatch = + activeSearchIndex === null + ? null + : (searchMatches[activeSearchIndex] ?? null); + const activeSearchMatchId = activeSearchMatch?.id ?? null; + + useEffect(() => { + if (searchMatches.length === 0) { + setActiveSearchIndex(null); + return; + } + + setActiveSearchIndex(0); + }, [searchMatches]); + + useEffect(() => { + if (!activeSearchMatchId) return; + + const timer = window.setTimeout(() => { + const container = fileRef.current; + const element = Array.from( + container?.querySelectorAll("[data-search-match-id]") ?? [], + ).find((el) => el.getAttribute("data-search-match-id") === activeSearchMatchId); + if (!element || !container) return; + + const containerRect = container.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + const scrollOffset = + elementRect.top - + containerRect.top - + containerRect.height / 2 + + elementRect.height / 2; + + container.scrollBy({ + top: scrollOffset, + behavior: "smooth", + }); + }, 50); + + return () => window.clearTimeout(timer); + }, [activeSearchMatchId]); + + const selectChangeHandler = (option?: SelectOption) => { + setLabel((option?.id as AllLabels) ?? null); + }; + + const toggleManagerLabel = () => { + setLabelManagerOpen(!labelManagerOpen); + }; + + const handleSearchChange = (value: string) => { + startTransition(() => setSearch(value)); + }; + + const handleSearchNext = useCallback(() => { + setActiveSearchIndex((current) => { + if (searchMatches.length === 0) return null; + if (current === null) return 0; + return Math.min(current + 1, searchMatches.length - 1); + }); + }, [searchMatches.length]); + + const handleSearchPrevious = useCallback(() => { + setActiveSearchIndex((current) => { + if (searchMatches.length === 0) return null; + if (current === null) return 0; + return Math.max(current - 1, 0); + }); + }, [searchMatches.length]); + + const focusDocument = useCallback(() => { + fileRef.current?.focus(); + }, []); + + return ( + + + + + + {paragraphs.map((p) => ( + + {p.value} + + ))} + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/file-annotator/types.ts b/frontend/src/renderer/src/components/file-annotator/types.ts new file mode 100644 index 00000000..cf62be88 --- /dev/null +++ b/frontend/src/renderer/src/components/file-annotator/types.ts @@ -0,0 +1,39 @@ +import type { AllLabels, AllLabelsWithSufix } from "@/types/aymurai"; + +export interface BaseAnnotation { + start: number; + end: number; +} + +interface BaseLabelAnnotation extends BaseAnnotation { + paragraphId: string; + tag?: AllLabels | AllLabelsWithSufix; + canonical_entity_id?: string | null; + mentionId?: string; + searchMatchId?: string; + searchIndex?: number; + isActive?: boolean; +} + +export interface LabelAnnotation extends BaseLabelAnnotation { + type: "tag"; +} + +export interface SearchAnnotation extends BaseLabelAnnotation { + type: "search"; +} + +export interface TextAnnotation extends BaseAnnotation { + type: "text"; +} + +export type Annotation = LabelAnnotation | SearchAnnotation | TextAnnotation; +export type Split = Annotation; + +export interface Metadata { + "data-start": number; + "data-end": number; + "data-tag"?: string; + "data-search-match-id"?: string; + "data-search-active"?: string; +} diff --git a/frontend/src/renderer/src/components/file-check/ErrorText.ts b/frontend/src/renderer/src/components/file-check/ErrorText.ts new file mode 100644 index 00000000..9626281f --- /dev/null +++ b/frontend/src/renderer/src/components/file-check/ErrorText.ts @@ -0,0 +1,13 @@ +import { styled } from "@/styles"; + +const ErrorText = styled("p", { + fontStyle: "italic", + color: "$errorPrimary", + textAlign: "center", + fontSize: "$subtitleSm", + lineHeight: "$subtitleSm", + fontWeight: "$default", + whiteSpace: "pre-line", +}); + +export default ErrorText; diff --git a/frontend/src/renderer/src/components/file-check/FileCheck.styles.ts b/frontend/src/renderer/src/components/file-check/FileCheck.styles.ts new file mode 100644 index 00000000..3ff2e007 --- /dev/null +++ b/frontend/src/renderer/src/components/file-check/FileCheck.styles.ts @@ -0,0 +1,53 @@ +import { styled } from "@/styles"; + +export const Wrapper = styled("div", { + display: "flex", + flexDirection: "column", + flexWrap: "wrap", + justifyContent: "center", + alignItems: "center", + gap: "$s", + + position: "relative", + + // Settings to enable ellipsis on file name + maxWidth: 150, + "& p": { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + width: "100%", + }, +}); + +export const Card = styled("div", { + display: "flex", + flexDirection: "column", + flexWrap: "wrap", + justifyContent: "center", + alignItems: "center", + + height: 200, + width: 150, + + borderWidth: "$sizes$xs", + borderStyle: "solid", + borderRadius: "$s", + boxShadow: "0px 0px $sizes$xs rgba(0, 0, 0, 0.1)", + + variants: { + hasError: { + true: { + bg: "$errorSecondary", + borderColor: "$errorPrimary", + }, + false: { + bg: "$bgPrimary", + borderColor: "$borderPrimary", + }, + }, + }, + defaultVariants: { + hasError: false, + }, +}); diff --git a/frontend/src/renderer/src/components/file-check/Icon.tsx b/frontend/src/renderer/src/components/file-check/Icon.tsx new file mode 100644 index 00000000..9358703e --- /dev/null +++ b/frontend/src/renderer/src/components/file-check/Icon.tsx @@ -0,0 +1,14 @@ +import { Spinner } from "@/components"; +import { CheckCircle, XCircle } from "phosphor-react"; + +import { colors } from "@/styles/tokens"; + +interface Props { + hasError: boolean; + isLoading: boolean; +} +export default function Icon({ hasError, isLoading }: Props) { + if (hasError) return ; + if (isLoading) return ; + return ; +} diff --git a/frontend/src/renderer/src/components/file-check/index.tsx b/frontend/src/renderer/src/components/file-check/index.tsx new file mode 100644 index 00000000..a6b3467f --- /dev/null +++ b/frontend/src/renderer/src/components/file-check/index.tsx @@ -0,0 +1,27 @@ +import { Text } from "@/components"; +import ErrorText from "./ErrorText"; +import { Card, Wrapper } from "./FileCheck.styles"; +import Icon from "./Icon"; + +interface Props { + fileName: string; + hasError?: boolean; + isLoading?: boolean; + errorMessage?: string; +} +export default function FileCheck({ + fileName, + hasError = false, + isLoading = false, + errorMessage = "Error de guardado\nVolvé a cargar el archivo", +}: Props) { + return ( + + + + + {fileName} + {hasError && {errorMessage}} + + ); +} diff --git a/frontend/src/renderer/src/components/file-preview/FilePreview.styles.ts b/frontend/src/renderer/src/components/file-preview/FilePreview.styles.ts new file mode 100644 index 00000000..0e968ff1 --- /dev/null +++ b/frontend/src/renderer/src/components/file-preview/FilePreview.styles.ts @@ -0,0 +1,70 @@ +import { styled } from "@/styles"; + +export const Wrapper = styled("div", { + display: "flex", + flexDirection: "column", + flexWrap: "wrap", + justifyContent: "start", + alignItems: "center", + gap: "$s", + + position: "relative", + + // Settings to enable ellipsis on file name + maxWidth: 150, + "& p": { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + width: "100%", + }, +}); + +export const FileContainer = styled("div", { + height: 200, + width: 150, + p: "$xs", + + boxShadow: "0px 0px $sizes$xs rgba(0, 0, 0, 0.1)", + borderRadius: "$s", + b: "$sizes$xs solid $borderPrimary", + + "& p, & span, & strong, & em": { + fontFamily: "$file", + fontSize: 8, + lineHeight: "100%", + my: "1em", + }, + + // Only shows text that fits into the box + overflow: "hidden", + + variants: { + isLoading: { + true: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + false: {}, + }, + + error: { + true: { + borderColor: "$colors$errorPrimary", + backgroundColor: "$colors$errorSecondary", + }, + false: {}, + }, + }, +}); + +export const Paragraph = styled("p", { + fontFamily: "$file", + fontSize: "$s", + lineHeight: "$s", + margin: 0, + padding: 0, + whiteSpace: "pre-wrap", + wordBreak: "break-word", +}); diff --git a/frontend/src/renderer/src/components/file-preview/index.tsx b/frontend/src/renderer/src/components/file-preview/index.tsx new file mode 100644 index 00000000..765e6cfe --- /dev/null +++ b/frontend/src/renderer/src/components/file-preview/index.tsx @@ -0,0 +1,89 @@ +import { FileX } from "phosphor-react"; + +import { Checkbox, Spinner, Text } from "@/components"; +import { useFileDispatch } from "@/hooks"; +import type { PredictStatus } from "@/hooks/usePredict"; +import { toggleSelected } from "@/reducers/file/actions"; +import type { DocFile } from "@/types/file"; + +import { FeatureFlowEnum, parseFeatureRouteSlug } from "@/types/features"; +import { useParams } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; +import * as S from "./FilePreview.styles"; + +interface Props { + file: DocFile; + status: PredictStatus; +} +export default function FilePreview({ file, status }: Props) { + const { feature: featureSlug } = useParams({ from: "/$feature/preview" }); + const feature = parseFeatureRouteSlug(featureSlug); + const { t } = useTranslation(); + const dispatch = useFileDispatch(); + + const isAnonymizer = feature === FeatureFlowEnum.Anonymizer; + const moreThanOneParagraph = file.paragraphs && file.paragraphs.length > 1; + const isError = status === "error"; + const isPending = status === "processing"; + + if (isError) { + return ( + + + + + + + {t("filePreview.loadError")} + + + ); + } + + if (isPending || !file.paragraphs) { + return ( + + + + + + ); + } + + return ( + + {!isAnonymizer && moreThanOneParagraph && ( + dispatch(toggleSelected(file.data.name))} + /> + )} + + + {file.paragraphs.map((p) => ( + + {p.value} + + ))} + + + + {file.data.name} + + + ); +} diff --git a/frontend/src/renderer/src/components/file-processing/FileProcessing.styles.ts b/frontend/src/renderer/src/components/file-processing/FileProcessing.styles.ts new file mode 100644 index 00000000..ec6c0d5c --- /dev/null +++ b/frontend/src/renderer/src/components/file-processing/FileProcessing.styles.ts @@ -0,0 +1,52 @@ +import { styled } from "@/styles"; + +export const BarContainer = styled("div", { + height: "$m", + width: "100%", + + bg: "$bgPrimary", + borderRadius: "$xxs", + + overflow: "hidden", +}); + +export const Bar = styled("div", { + width: "100%", + height: "100%", + + backgroundSize: "$sizes$xl $sizes$xl", + + transition: "width $s ease", + + variants: { + isCompleted: { + true: { + backgroundImage: `linear-gradient( + 135deg, + $actionDefaultAlt 37.50%, + $actionDefault 37.50%, + $actionDefault 50%, + $actionDefaultAlt 50%, + $actionDefaultAlt 87.50%, + $actionDefault 87.50%, + $actionDefault 100% + )`, + }, + false: { + backgroundImage: `linear-gradient( + 135deg, + $actionDefault 37.50%, + $actionDefaultAlt 37.50%, + $actionDefaultAlt 50%, + $actionDefault 50%, + $actionDefault 87.50%, + $actionDefaultAlt 87.50%, + $actionDefaultAlt 100% + )`, + }, + }, + }, + defaultVariants: { + isCompleted: false, + }, +}); diff --git a/frontend/src/renderer/src/components/file-processing/ProgressBar.tsx b/frontend/src/renderer/src/components/file-processing/ProgressBar.tsx new file mode 100644 index 00000000..222ee715 --- /dev/null +++ b/frontend/src/renderer/src/components/file-processing/ProgressBar.tsx @@ -0,0 +1,45 @@ +import { Label, Stack } from "@/components"; +import type { PredictStatus } from "@/hooks/usePredict"; +import { CheckCircle } from "phosphor-react"; +import { Bar, BarContainer } from "./FileProcessing.styles"; +import { ProgressLabel } from "./ProgressLabel"; + +interface Props { + fileName: string; + progress: number; + status: PredictStatus; +} +export default function ProgressBar({ fileName, progress, status }: Props) { + const getProgressText = () => { + const text = { + processing: `${progress}%`, + completed: ( + <> + Carga finalizada 100% + + ), + error: "Error de carga de archivo. Volvelo a intentar", + stopped: "Detuviste el procesamiento de este archivo", + }; + + return text[status]; + }; + + return ( + + {/* Texts */} + + + + {getProgressText()} + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/file-processing/ProgressLabel.ts b/frontend/src/renderer/src/components/file-processing/ProgressLabel.ts new file mode 100644 index 00000000..971ccaed --- /dev/null +++ b/frontend/src/renderer/src/components/file-processing/ProgressLabel.ts @@ -0,0 +1,29 @@ +import Label from "@/components/label"; +import { styled } from "@/styles"; + +export const ProgressLabel = styled(Label, { + display: "flex", + alignItems: "center", + gap: "$xs", + + variants: { + progress: { + processing: { + color: "$textDefault", + }, + error: { + color: "$errorPrimary", + }, + stopped: { + color: "$textLighter", + fontStyle: "italic", + }, + completed: { + color: "$actionPressed", + }, + }, + }, + defaultVariants: { + progress: "processing", + }, +}); diff --git a/frontend/src/renderer/src/components/file-processing/index.tsx b/frontend/src/renderer/src/components/file-processing/index.tsx new file mode 100644 index 00000000..6768cea2 --- /dev/null +++ b/frontend/src/renderer/src/components/file-processing/index.tsx @@ -0,0 +1,111 @@ +import { ArrowsLeftRight, Stop, X } from "phosphor-react"; +import { type ChangeEventHandler, type MouseEventHandler, useRef } from "react"; + +import HiddenInput from "@/components/hidden-input"; +import Button from "@/components/ui/button"; +import { useFileDispatch } from "@/hooks"; +import type { PredictStatus } from "@/hooks/usePredict"; +import { removeFile, replaceFile } from "@/reducers/file/actions"; +import { css } from "@/styled/css"; +import { Stack } from "@/styled/jsx"; +import { useQueryClient } from "@tanstack/react-query"; +import ProgressBar from "./ProgressBar"; + +function ActionButton({ + status, + onClick, +}: { status: PredictStatus; onClick: MouseEventHandler }) { + return ( + + ); +} + +interface Props { + fileName: string; + status: PredictStatus; + progress: number; + onAbort?: () => void; +} +export default function FileProcessing({ + fileName, + status, + progress, + onAbort, +}: Props) { + const inputRef = useRef(null); + const dispatch = useFileDispatch(); + const queryClient = useQueryClient(); + + const handleOpenFinder = () => { + inputRef.current?.click(); + }; + + const handleAddedFile: ChangeEventHandler = (e) => { + const rawFiles = e.target.files; + + if (rawFiles) { + const fileList = Array.from(rawFiles); + if (fileList.length > 0) { + queryClient.removeQueries({ + queryKey: ["file-parser", fileName], + exact: false, + }); + queryClient.removeQueries({ + queryKey: ["disambiguate", fileName], + exact: false, + }); + queryClient.removeQueries({ + predicate: (query) => { + const key = query.queryKey as unknown[]; + return key[0] === "predict" && key[2] === fileName; + }, + }); + dispatch(replaceFile(fileName, fileList[0])); + } + } + }; + + const handleStop = () => { + onAbort?.(); + }; + + const remove = () => { + onAbort?.(); + dispatch(removeFile(fileName)); + }; + + return ( + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/file-stepper/FileStepper.styles.ts b/frontend/src/renderer/src/components/file-stepper/FileStepper.styles.ts new file mode 100644 index 00000000..850c59e8 --- /dev/null +++ b/frontend/src/renderer/src/components/file-stepper/FileStepper.styles.ts @@ -0,0 +1,26 @@ +import { Button as BaseButton } from "@/components"; +import { styled } from "@/styles"; + +export const CaretButton = styled(BaseButton, { + boxShadow: "4px 0px 4px rgba(0, 0, 0, 0.05)", + + // Marked as important because of Stitches hierarchy + px: "0px !important", + py: "$s !important", + + "&:disabled": { + boxShadow: "none", + color: "$textLighter", + }, +}); + +export const Carousel = styled("div", { + display: "flex", + flexDirection: "row", + gap: "$s", + alignItems: "center", + wrap: "no-wrap", + flex: 1, + + overflowX: "scroll", +}); diff --git a/frontend/src/renderer/src/components/file-stepper/Icons.tsx b/frontend/src/renderer/src/components/file-stepper/Icons.tsx new file mode 100644 index 00000000..e5907826 --- /dev/null +++ b/frontend/src/renderer/src/components/file-stepper/Icons.tsx @@ -0,0 +1,26 @@ +import { CaretLeft, CaretRight, CheckCircle } from "phosphor-react"; + +import { colors } from "@/styles/tokens"; + +function ArrowRight() { + return ; +} +function ArrowLeft() { + return ; +} + +interface Props { + status: "completed" | "focus" | "default"; +} +function Check({ status }: Props) { + return status === "completed" ? ( + + ) : null; +} + +const Icons = { + ArrowRight, + ArrowLeft, + Check, +}; +export default Icons; diff --git a/frontend/src/renderer/src/components/file-stepper/index.tsx b/frontend/src/renderer/src/components/file-stepper/index.tsx new file mode 100644 index 00000000..1053e8c4 --- /dev/null +++ b/frontend/src/renderer/src/components/file-stepper/index.tsx @@ -0,0 +1,67 @@ +import { TabName as FileName, Tab as FileStep, Stack } from "@/components"; +import { useFiles } from "@/hooks"; +import type { DocFile } from "@/types/file"; +import { CaretButton, Carousel } from "./FileStepper.styles"; +import Icons from "./Icons"; +import { canMoveLeft as checkLeft, canMoveRight as checkRight } from "./utils"; + +interface Props { + selected: number; + nextFile: () => void; + previousFile: () => void; +} +export default function FileStepper({ + selected, + nextFile, + previousFile, +}: Props) { + const files = useFiles(); + + const canMoveLeft = checkLeft(selected, files); + const canMoveRight = checkRight(selected, files); + const isLeftDisabled = selected === 0; + const isRightDisabled = selected === files.length - 1; // -1 because we are using base 0 notation + + const getStatus = (current: number, file: DocFile) => { + if (file.validated) { + return "completed"; + } + return selected === current ? "focus" : "default"; + }; + + return ( + + {/* <- */} + {canMoveLeft && ( + + + + )} + + {/* List of files */} + + {files.map((file, i) => ( + + {file.data.name} + + + ))} + + + {/* -> */} + {canMoveRight && ( + + + + )} + + ); +} diff --git a/frontend/src/renderer/src/components/file-stepper/utils.ts b/frontend/src/renderer/src/components/file-stepper/utils.ts new file mode 100644 index 00000000..ef1e2034 --- /dev/null +++ b/frontend/src/renderer/src/components/file-stepper/utils.ts @@ -0,0 +1,27 @@ +import type { DocFile } from "@/types/file"; + +/** + * Check if the stepper can move to the left + * @param cur Current `selected` index + * @param state State array of files + * @returns `true` if the stepper can move to the left (or to a previous file), `false` otherwise + */ +export function canMoveLeft(cur: number, state: DocFile[]) { + const sliced = state.slice(0, cur); + + // Check if we find any file that is not validated yet + return !!sliced.find((file) => !file.validated); +} + +/** + * Check if the stepper can move to the right + * @param cur Current `selected` index + * @param state State array of files + * @returns `true` if the stepper can move to the right (or to a next file), `false` otherwise + */ +export function canMoveRight(cur: number, state: DocFile[]) { + const sliced = state.slice(cur + 1, state.length); + + // Check if we find any file that is not validated yet + return !!sliced.find((file) => !file.validated); +} diff --git a/frontend/src/renderer/src/components/file/annotation-popover/index.tsx b/frontend/src/renderer/src/components/file/annotation-popover/index.tsx new file mode 100644 index 00000000..e8693bde --- /dev/null +++ b/frontend/src/renderer/src/components/file/annotation-popover/index.tsx @@ -0,0 +1,191 @@ +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { css } from "@/styled/css"; +import type { AllLabels } from "@/types/aymurai"; +import type { FocusEventHandler, ReactNode } from "react"; +import { useEffect, useId, useRef, useState } from "react"; +import Tagger from "./tagger"; + +const CLOSE_DELAY_MS = 120; +const OPEN_EVENT = "aymurai:annotation-popover-open"; + +const triggerFocus = css({ + "&:focus-visible": { + outline: "[2px solid token(colors.action.hover)]", + outlineOffset: "[2px]", + borderRadius: "sm", + }, +}); + +interface AnnotationPopoverProps { + children: ReactNode; + onClickOne: (label: AllLabels) => void; + onClickAll: (label: AllLabels) => void; + onDeleteOne?: () => void; + onDeleteAll?: () => void; + onHoverChange?: (hovered: boolean) => void; +} + +export default function AnnotationPopover({ + children, + onClickAll, + onClickOne, + onDeleteOne, + onDeleteAll, + onHoverChange, +}: AnnotationPopoverProps) { + const popoverId = useId(); + const [open, setOpen] = useState(false); + const closeTimer = useRef | null>(null); + const isTriggerHovered = useRef(false); + const isContentHovered = useRef(false); + const isFocusInside = useRef(false); + const isKeyboardOpen = useRef(false); + + const cancelClose = () => { + if (closeTimer.current) { + clearTimeout(closeTimer.current); + closeTimer.current = null; + } + }; + + useEffect(() => { + const closeWhenAnotherOpens = (event: Event) => { + const openedId = (event as CustomEvent).detail; + if (openedId === popoverId) return; + + isTriggerHovered.current = false; + isContentHovered.current = false; + isFocusInside.current = false; + setOpen(false); + if (closeTimer.current) { + clearTimeout(closeTimer.current); + closeTimer.current = null; + } + }; + + window.addEventListener(OPEN_EVENT, closeWhenAnotherOpens); + + return () => { + window.removeEventListener(OPEN_EVENT, closeWhenAnotherOpens); + if (closeTimer.current) clearTimeout(closeTimer.current); + }; + }, [popoverId]); + + const setInteractive = (nextOpen: boolean, announce = false) => { + setOpen(nextOpen); + onHoverChange?.(nextOpen); + if (nextOpen && announce) { + window.dispatchEvent(new CustomEvent(OPEN_EVENT, { detail: popoverId })); + } + }; + + const scheduleClose = () => { + cancelClose(); + closeTimer.current = setTimeout(() => { + if ( + !isFocusInside.current && + !isTriggerHovered.current && + !isContentHovered.current + ) { + setInteractive(false); + } + }, CLOSE_DELAY_MS); + }; + + const handleBlur: FocusEventHandler = (e) => { + if (!e.currentTarget.contains(e.relatedTarget)) { + isFocusInside.current = false; + scheduleClose(); + } + }; + + return ( + { + if (nextOpen) { + cancelClose(); + setInteractive(true, true); + return; + } + if ( + isTriggerHovered.current || + isContentHovered.current || + isFocusInside.current + ) { + scheduleClose(); + return; + } + setInteractive(false); + }} + > + { + isTriggerHovered.current = true; + cancelClose(); + setInteractive(true, true); + }} + onMouseLeave={() => { + isTriggerHovered.current = false; + scheduleClose(); + }} + onFocus={() => { + isFocusInside.current = true; + cancelClose(); + setInteractive(true, true); + }} + onBlur={() => { + isFocusInside.current = false; + scheduleClose(); + }} + onKeyDown={(e) => { + if (e.key === " " || e.key === "Enter") { + isKeyboardOpen.current = true; + } + }} + > + {children} + + { + if (!isKeyboardOpen.current) e.preventDefault(); + }} + onCloseAutoFocus={(e) => { + if (!isKeyboardOpen.current) e.preventDefault(); + isKeyboardOpen.current = false; + }} + onMouseEnter={() => { + isContentHovered.current = true; + cancelClose(); + onHoverChange?.(true); + }} + onMouseLeave={() => { + isContentHovered.current = false; + scheduleClose(); + }} + onFocus={() => { + isFocusInside.current = true; + cancelClose(); + onHoverChange?.(true); + }} + onBlur={handleBlur} + > + + + + ); +} diff --git a/frontend/src/renderer/src/components/file/annotation-popover/tagger-button.tsx b/frontend/src/renderer/src/components/file/annotation-popover/tagger-button.tsx new file mode 100644 index 00000000..29501fd0 --- /dev/null +++ b/frontend/src/renderer/src/components/file/annotation-popover/tagger-button.tsx @@ -0,0 +1,79 @@ +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { sva } from "@/styled/css"; +import { styled } from "@/styled/jsx"; + +const button = sva({ + slots: ["button", "tooltipContent"], + base: { + button: { + p: "0.5", + rounded: "[6px]", + cursor: "pointer", + flexShrink: "0", + _hover: { + bg: "action.hover", + }, + }, + tooltipContent: { + bg: "action.hover", + color: "white", + px: "1", + py: "0.5", + rounded: "sm", + }, + }, + variants: { + disabled: { + true: { + button: { + cursor: "not-allowed", + _hover: { + bg: "transparent", + }, + }, + }, + }, + }, +}); + +interface TaggerButtonProps { + tooltip: string; + children: React.ReactNode; + onClick: () => void; + disabled?: boolean; +} +export default function TaggerButton({ + children, + tooltip, + onClick, + disabled = false, +}: TaggerButtonProps) { + const classes = button({ disabled }); + return ( + + + + + + +
+ {tooltip} +
+
+
+
+ ); +} diff --git a/frontend/src/renderer/src/components/file/annotation-popover/tagger.tsx b/frontend/src/renderer/src/components/file/annotation-popover/tagger.tsx new file mode 100644 index 00000000..2218a185 --- /dev/null +++ b/frontend/src/renderer/src/components/file/annotation-popover/tagger.tsx @@ -0,0 +1,174 @@ +import { useState } from "react"; + +import { sva } from "@/styled/css"; +import { styled } from "@/styled/jsx"; +import { hstack } from "@/styled/patterns"; + +import AnonymizerLabelSelect from "@/components/anonymizer/anonymizer-label-select"; +import type { SelectOption } from "@/components/ui/select"; +import { useAnnotation } from "@/context/Annotation"; +import { useExcludedTagsConfig } from "@/store/useLocal"; +import type { AllLabels } from "@/types/aymurai"; +import { getActiveAnonymizerLabelOptions } from "@/utils/anonymizer/labels"; + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +import TaggerButton from "./tagger-button"; + +const IMG_SIZE = 24; + +const tagger = sva({ + slots: ["container", "button", "divider", "tooltipContent"], + base: { + container: { + ...hstack.raw({ alignItems: "center", gap: "1" }), + p: "1", + bg: "action.alt-default", + rounded: "lg", + }, + button: { + cursor: "pointer", + }, + divider: { + alignSelf: "stretch", + width: "[1px]", + bg: "[#5960B0]", + + my: "1", + }, + tooltipContent: { + bg: "action.hover", + color: "white", + px: "1", + py: "0.5", + rounded: "sm", + }, + }, +}); + +interface MarkTaggerProps { + onClickOne: (label: AllLabels) => void; + onClickAll: (label: AllLabels) => void; + onDeleteOne?: () => void; + onDeleteAll?: () => void; +} +export default function Tagger({ + onClickAll, + onClickOne, + onDeleteOne, + onDeleteAll, +}: MarkTaggerProps) { + const { label: initialLabel } = useAnnotation(); + const { tags } = useExcludedTagsConfig(); + + const [label, setLabel] = useState(initialLabel); + const [isSelectOpen, setIsSelectOpen] = useState(false); + const options = getActiveAnonymizerLabelOptions(tags); + const activeLabel = + label && options.some((option) => option.id === label) ? label : null; + + const handleClickOne = () => { + if (!activeLabel) return; + onClickOne(activeLabel); + }; + const handleClickAll = () => { + if (!activeLabel) return; + onClickAll(activeLabel); + }; + + const handleLabelChange = (value: SelectOption) => { + setLabel(value.id as AllLabels); + }; + + const classes = tagger(); + return ( +
+ + + +
+ +
+
+ +
+ + Selecciona tipo de etiqueta + +
+
+
+
+
+ + Afectar una ocurrencia + +
+ + Afectar todas las ocurrencias + + {onDeleteOne && ( + <> +
+ + Eliminar esta ocurrencia + + + )} + {onDeleteAll && ( + <> +
+ + Eliminar todas las ocurrencias + + + )} +
+ ); +} diff --git a/frontend/src/renderer/src/components/file/manual-entity-resolution-dialog.tsx b/frontend/src/renderer/src/components/file/manual-entity-resolution-dialog.tsx new file mode 100644 index 00000000..ab15cfb6 --- /dev/null +++ b/frontend/src/renderer/src/components/file/manual-entity-resolution-dialog.tsx @@ -0,0 +1,227 @@ +import { useEffect, useMemo, useState } from "react"; + +import Button from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogTitle, +} from "@/components/ui/dialog"; +import Select from "@/components/ui/select"; +import { css } from "@/styled/css"; +import { Stack, styled } from "@/styled/jsx"; +import type { + ManualEntityResolution, + SimilarEntityGroupCandidate, +} from "@/utils/anonymizer/entity-similarity"; + +export interface ManualEntityResolutionRequest { + text: string; + label: string; + count: number; + candidates: SimilarEntityGroupCandidate[]; +} + +interface ManualEntityResolutionDialogProps { + request: ManualEntityResolutionRequest | null; + onClose: () => void; + onResolve: (resolution: ManualEntityResolution) => void; +} + +const candidateSummary = css({ + bg: "bg.primary", + border: "[1px solid #BCBAB8]", + rounded: "sm", + p: "2.5", + display: "grid", + gridTemplateColumns: "minmax(0, 1fr) max-content", + alignItems: "center", + columnGap: "3", + rowGap: "0.5", + minW: "0", +}); + +const candidateText = css({ + display: "grid", + gap: "0.5", + minW: "0", +}); + +const candidateToken = css({ + minW: "0", + overflowWrap: "anywhere", + wordBreak: "break-word", +}); + +const candidateReference = css({ + minW: "0", + color: "text.default", + overflowWrap: "anywhere", + wordBreak: "break-word", +}); + +const candidateMeta = css({ + display: "grid", + alignContent: "center", + justifyItems: "end", + gap: "0.5", + justifySelf: "end", + w: "[fit-content]", + maxW: "full", + px: "1.5", + py: "0.5", + bg: "[#E6E8FF]", + color: "[#3F479D]", + rounded: "xs", + whiteSpace: "nowrap", +}); + +const categoryLabel = css({ + fontSize: "[11px]", + fontWeight: "bold", + lineHeight: "[1.2]", +}); + +const scoreValue = css({ + fontSize: "[11px]", + fontWeight: "bold", + lineHeight: "[1.2]", +}); + +function getCandidateLabel(candidate: SimilarEntityGroupCandidate) { + const text = candidate.matchedText || "sin texto"; + return `${candidate.group.renderToken} - ${text} (${candidate.score}%)`; +} + +export default function ManualEntityResolutionDialog({ + request, + onClose, + onResolve, +}: ManualEntityResolutionDialogProps) { + const [selectedCanonicalId, setSelectedCanonicalId] = useState( + null, + ); + + useEffect(() => { + setSelectedCanonicalId(request?.candidates[0]?.group.canonicalId ?? null); + }, [request]); + + const candidate = useMemo(() => { + if (!request) return null; + return ( + request.candidates.find( + (item) => item.group.canonicalId === selectedCanonicalId, + ) ?? + request.candidates[0] ?? + null + ); + }, [request, selectedCanonicalId]); + + const candidateOptions = useMemo( + () => + request?.candidates.map((item) => ({ + id: item.group.canonicalId, + text: getCandidateLabel(item), + })) ?? [], + [request], + ); + + const isLabelConflict = candidate ? !candidate.labelMatches : false; + const selectedGroupText = candidate?.group.renderToken ?? ""; + const selectedGroupLabel = candidate?.group.renderBase ?? ""; + const mentionCount = request?.count ?? 0; + + return ( + { + if (!open) onClose(); + }} + > + + Resolver grupo de entidad + {request && candidate && ( + + + {isLabelConflict + ? `Encontramos texto similar en otra categoría. Seleccionaste ${request.label} para "${request.text}".` + : `Encontramos un grupo similar para "${request.text}".`} + + + {request.candidates.length > 1 && ( + + ); +} diff --git a/frontend/src/renderer/src/components/home/choose-host.tsx b/frontend/src/renderer/src/components/home/choose-host.tsx new file mode 100644 index 00000000..9ae555bb --- /dev/null +++ b/frontend/src/renderer/src/components/home/choose-host.tsx @@ -0,0 +1,66 @@ +import { Button } from "@/components"; +import { useRunLocalServer } from "@/services/aymurai"; +import { css } from "@/styled/css"; +import { Stack, styled } from "@/styled/jsx"; +import { useNavigate } from "@tanstack/react-router"; +import { HardDrives, Monitor } from "phosphor-react"; +import { useTranslation } from "react-i18next"; + +interface ChooseHostProps { + onRemoteClick: () => void; +} +export default function ChooseHost({ onRemoteClick }: ChooseHostProps) { + const navigate = useNavigate(); + const { run: runLocalServer, isRunning } = useRunLocalServer({ + onSuccess: () => + navigate({ + to: "/home/features", + }), + }); + + const { t } = useTranslation(); + + return ( + + + +

+ {t("home.host.howToConnect")} +

+ + +

+ {t("home.host.optionOr")} +

+ +
+
+
+ ); +} diff --git a/frontend/src/renderer/src/components/home/connect-to-host.tsx b/frontend/src/renderer/src/components/home/connect-to-host.tsx new file mode 100644 index 00000000..8581f422 --- /dev/null +++ b/frontend/src/renderer/src/components/home/connect-to-host.tsx @@ -0,0 +1,108 @@ +import { useConnectToHost } from "@/services/aymurai"; +import * as localStore from "@/store/useLocal"; +import { css } from "@/styled/css"; +import { Stack } from "@/styled/jsx"; +import { useNavigate } from "@tanstack/react-router"; +import { AxiosError } from "axios"; +import { ArrowLeft } from "phosphor-react"; +import { + type ChangeEventHandler, + type SubmitEventHandler, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { ZodError } from "zod"; +import Button from "../ui/button"; +import Input from "../ui/input"; + +const BackButton = ({ onClick }: { onClick: () => void }) => ( + +); + +interface ConnectToHostProps { + onBackClick: () => void; +} +export default function ConnectToHost({ onBackClick }: ConnectToHostProps) { + const navigate = useNavigate(); + const remoteHost = localStore.useServerHost() ?? ""; + const { setServerHost } = localStore.useServerHostActions(); + const { t } = useTranslation(); + + const [host, setHost] = useState(remoteHost); + + const { mutate: connectToHost, isPending, error, reset } = useConnectToHost(); + + const handleChange: ChangeEventHandler = (e) => { + setHost(e.target.value); + reset(); + }; + + const tryConnection: SubmitEventHandler = (e) => { + e.preventDefault(); + + connectToHost(host, { + onSuccess: () => { + setServerHost(host); + navigate({ + to: "/home/features", + }); + }, + }); + }; + + const errorMessage = (err: Error | null): string => { + console.error(err); + if (err instanceof AxiosError) { + if (err.code === "ERR_NETWORK") return t("home.host.errors.network"); + return t("home.host.errors.connection"); + } + + if (err instanceof ZodError) { + return t("home.host.errors.invalidResponse"); + } + + if (err instanceof TypeError) { + return t("home.host.errors.invalidUrl"); + } + + return t("home.host.errors.unknown"); + }; + + return ( + <> + + + +

+ {t("home.host.connectServerExplanation")} +

+ + + + + + +
+ + + ); +} diff --git a/frontend/src/renderer/src/components/home/stepper.tsx b/frontend/src/renderer/src/components/home/stepper.tsx new file mode 100644 index 00000000..2810ebab --- /dev/null +++ b/frontend/src/renderer/src/components/home/stepper.tsx @@ -0,0 +1,109 @@ +import { css, sva } from "@/styled/css"; +import { stack } from "@/styled/patterns"; +import { useTranslation } from "react-i18next"; + +const stepper = css({ + ...stack.raw({ align: "center", direction: "row" }), +}); + +const step = sva({ + slots: ["container", "circle", "text"], + base: { + container: stack.raw({ direction: "row", align: "center", gap: "2" }), + circle: { + ...stack.raw({ direction: "row", align: "center", justify: "center" }), + rounded: "full", + height: "9", + width: "9", + textStyle: "cta.md.strong", + }, + text: { + color: "text.default", + textStyle: "label.md.default", + display: "none", + visibility: "hidden", + }, + }, + variants: { + status: { + complete: { + circle: { + color: "text.onbutton-alternative", + bg: "action.alt-default", + }, + }, + pending: { + circle: { + color: "text.onbutton-default", + bg: "action.disabled", + }, + }, + in_progress: { + circle: { + color: "text.onbutton-default", + bg: "action.default", + border: "primary-alt", + }, + text: { display: "inline", visibility: "visible" }, + }, + }, + }, +}); + +type StepStatus = "complete" | "pending" | "in_progress"; +interface StepProps { + children: string; + status: StepStatus; + number: number; +} +function Step({ children, status, number }: StepProps) { + const classes = step({ status }); + // Used to hide text from visuals but expose to screen readers + const srOnly = css({ + srOnly: true, + }); + + return ( +
+ + + Paso {number}: {children} + + +
+ ); +} + +interface StepperProps { + currentStep: number; +} +export default function Stepper({ currentStep }: StepperProps) { + const { t } = useTranslation(); + + const status = (step: number): StepStatus => { + if (step === currentStep) return "in_progress"; + if (step > currentStep) return "pending"; + return "complete"; + }; + + return ( +
+ + {t("stepper.selection")} + + + {t("stepper.extraction")} + + + {t("stepper.validation")} + + + {t("stepper.finalization")} + +
+ ); +} diff --git a/frontend/src/renderer/src/components/how-it-works-modal.tsx b/frontend/src/renderer/src/components/how-it-works-modal.tsx new file mode 100644 index 00000000..92541d04 --- /dev/null +++ b/frontend/src/renderer/src/components/how-it-works-modal.tsx @@ -0,0 +1,81 @@ +import { SectionTitle } from "@/layout/section-title"; +import { css } from "@/styled/css"; +import { HStack, Stack } from "@/styled/jsx"; +import type { FeatureFlowEnum } from "@/types/features"; +import { Question, X } from "phosphor-react"; +import { useTranslation } from "react-i18next"; +import HowItWorks from "./how-it-works"; +import { Dialog, DialogClose, DialogContent, DialogTrigger } from "./ui/dialog"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; + +const tooltip = css({ + padding: "2", + textStyle: "label.md.default", +}); + +const modalButton = css({ + color: "text.lighter", + + padding: "0.5", + rounded: "sm", + + cursor: "pointer", + + transition: "colors", + + "&:hover": { + bg: "action.hover", + color: "text.onbutton-alternative", + }, +}); + +const closeButton = css({ + cursor: "pointer", +}); + +const content = css({ + overflowY: "auto", + maxH: "[90vh]", + maxW: "5xl!", +}); + +interface HowItWorksModalProps { + feature: FeatureFlowEnum; +} +export default function HowItWorksModal({ feature }: HowItWorksModalProps) { + const { t } = useTranslation(); + + return ( + + + + + + + + {t("howItWorks")} + + + + + {t("howItWorks")} + + + + + } + feature={feature} + /> + + + + ); +} diff --git a/frontend/src/renderer/src/components/how-it-works.tsx b/frontend/src/renderer/src/components/how-it-works.tsx new file mode 100644 index 00000000..7c8f3876 --- /dev/null +++ b/frontend/src/renderer/src/components/how-it-works.tsx @@ -0,0 +1,110 @@ +import { SectionTitle } from "@/layout/section-title"; +import { css } from "@/styled/css"; +import { Grid, Stack, styled } from "@/styled/jsx"; +import { hstack, stack } from "@/styled/patterns"; +import { type FeatureFlowEnum, featureNamespace } from "@/types/features"; +import { useTranslation } from "react-i18next"; + +const card = css({ + ...hstack.raw({ gap: "4", alignItems: "center" }), + + bg: "bg.secondary", + border: "primary", + rounded: "sm", + + px: "4", + py: "6", + + "& > img": { + width: "[200px]", + height: "[130px]", + objectFit: "contain", + flexShrink: "0", + }, +}); + +const stepStyle = css({ + ...stack.raw({ align: "center", justify: "center" }), + + bg: "action.alt-default", + color: "text.onbutton-alternative", + rounded: "full", + textStyle: "cta.md.strong", + + width: "9", + height: "9", +}); + +interface CardProps { + img: string; + imgAlt: string; + step: number; + title: string; + subtitle: string; +} +function Card({ img, imgAlt, step, title, subtitle }: CardProps) { + return ( +
+ {imgAlt} + +

{step}

+ + {title} + {subtitle} + +
+
+ ); +} + +interface HowItWorksProps { + title?: React.ReactNode; + feature: FeatureFlowEnum; +} +export default function HowItWorks({ title, feature }: HowItWorksProps) { + const { t } = useTranslation(); + const { t: tFeature } = useTranslation(featureNamespace[feature]); + + const renderTitle = + typeof title === "string" ? ( + {title} + ) : ( + (title ?? {t("howItWorks")}) + ); + + return ( + + {renderTitle} + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/index.ts b/frontend/src/renderer/src/components/index.ts new file mode 100644 index 00000000..15c0ea80 --- /dev/null +++ b/frontend/src/renderer/src/components/index.ts @@ -0,0 +1,23 @@ +export { default as Arrow } from "./arrow"; +export { Checkbox, CheckboxGroup } from "./checkbox"; +export { default as DecisionTabs } from "./decision-tabs"; +export { default as FileAnnotator } from "./file-annotator"; +export { default as FileCheck } from "./file-check"; +export { default as FilePreview } from "./file-preview"; +export { default as FileProcessing } from "./file-processing"; +export { default as FileStepper } from "./file-stepper"; +export { default as Grid } from "./grid"; +export { default as Label } from "./label"; +export { OnboardingGrid } from "./onboarding-grid"; +export { Radio, RadioGroup } from "./radio"; +export { default as Spinner } from "./spinner"; +export { default as Stack } from "./stack"; +export { default as Subtitle } from "./subtitle"; +export { Tab, TabName } from "./tabs"; +export { default as Text } from "./text"; +export { default as ThemeProvider } from "./theme-provider"; +export { default as Title } from "./title"; +export { default as Button } from "./ui/button"; +export { default as Input } from "./uncontrolled-input"; +export { ValidateDataset } from "./validate-dataset"; +export { default as ValidationForm } from "./validation-form"; diff --git a/frontend/src/renderer/src/components/label/index.ts b/frontend/src/renderer/src/components/label/index.ts new file mode 100644 index 00000000..08531a91 --- /dev/null +++ b/frontend/src/renderer/src/components/label/index.ts @@ -0,0 +1,42 @@ +import { styled } from "@/styles"; + +/** + * @deprecated use `textStyles` from PandaCSS instead. + */ +const Label = styled("label", { + variants: { + size: { + m: { + fontSize: "$labelMd", + lineHeight: "$labelMd", + }, + s: { + fontSize: "$labelSm", + lineHeight: "$labelSm", + }, + }, + weight: { + default: { + fontWeight: "$default", + }, + strong: { + fontWeight: "$strong", + }, + }, + status: { + default: { + color: "$textLighter", + }, + error: { + color: "$errorPrimary", + }, + }, + }, + defaultVariants: { + size: "m", + weight: "default", + status: "default", + }, +}); + +export default Label; diff --git a/frontend/src/renderer/src/components/layout/footer.tsx b/frontend/src/renderer/src/components/layout/footer.tsx new file mode 100644 index 00000000..6de643c4 --- /dev/null +++ b/frontend/src/renderer/src/components/layout/footer.tsx @@ -0,0 +1,35 @@ +import { css, cx } from "@/styled/css"; +import BuiltBy from "../brand/built-by"; + +const content = css({ + display: "flex", + flexDirection: "row", + alignItems: "center", + + borderTop: "primary", + px: "12", + py: "6", +}); +const childrenContainer = css({ + flex: "1", + display: "flex", + justifyContent: "flex-end", +}); + +interface FooterProps { + withBuiltBy?: boolean; + children?: React.ReactNode; + className?: string; +} +export default function Footer({ + withBuiltBy, + children, + className, +}: FooterProps) { + return ( +
+ {withBuiltBy && } +
{children}
+
+ ); +} diff --git a/frontend/src/renderer/src/components/layout/header.tsx b/frontend/src/renderer/src/components/layout/header.tsx new file mode 100644 index 00000000..f1b1d0a4 --- /dev/null +++ b/frontend/src/renderer/src/components/layout/header.tsx @@ -0,0 +1,68 @@ +import { useTutorialSeen } from "@/store/useLocal"; +import { css } from "@/styled/css"; +import { Divider, HStack, Stack, styled } from "@/styled/jsx"; +import type { FeatureFlowEnum } from "@/types/features"; +import { Link } from "@tanstack/react-router"; +import FeaturesMenu from "../features-menu"; +import HowItWorksModal from "../how-it-works-modal"; + +const header = css({ + position: "relative", + width: "full", + height: "24", + py: "6", + px: "12", + bg: "bg.secondary", + borderBottom: "[1px solid #BCBAB8]", + display: "flex", + alignItems: "center", + justifyContent: "space-between", +}); + +const centerSlot = css({ + position: "absolute", + left: "[50%]", + transform: "translateX(-50%)", +}); + +interface HeaderProps { + title?: string; + center?: React.ReactNode; + feature?: FeatureFlowEnum; + right?: React.ReactNode; +} + +export default function Header({ title, center, feature, right }: HeaderProps) { + const tutorialSeen = useTutorialSeen(feature!); + + const img = title + ? `${import.meta.env.BASE_URL}brand/aymurai-iso-darkpurple.svg` + : `${import.meta.env.BASE_URL}brand/aymurai-hor-darkpurple.svg`; + + return ( +
+ + + AymurAI logo + {title && ( + <> + + {title} + + )} + + + {center &&
{center}
} + + {tutorialSeen && feature && } + {right} + + +
+ ); +} diff --git a/frontend/src/renderer/src/components/layout/home-button.tsx b/frontend/src/renderer/src/components/layout/home-button.tsx new file mode 100644 index 00000000..777409d5 --- /dev/null +++ b/frontend/src/renderer/src/components/layout/home-button.tsx @@ -0,0 +1,16 @@ +import { css } from "@/styled/css"; +import { House } from "phosphor-react"; +import Link from "../ui/link"; + +export default function HomeButton() { + return ( + + + + ); +} diff --git a/frontend/src/renderer/src/components/layout/main-content.tsx b/frontend/src/renderer/src/components/layout/main-content.tsx new file mode 100644 index 00000000..139fd857 --- /dev/null +++ b/frontend/src/renderer/src/components/layout/main-content.tsx @@ -0,0 +1,48 @@ +import { cva, cx } from "@/styled/css"; +import { stack } from "@/styled/patterns"; + +const content = cva({ + base: { + flex: "1", + minH: "0", + + width: "full", + overflowY: "auto", + + bg: "bg.primary", + }, + variants: { + full: { + true: {}, + false: { + ...stack.raw({ align: "center", gap: "0" }), + + pt: { base: "6", xl: "16" }, + px: "8", + + "& > div.spacing": { + width: "full", + maxWidth: "5xl", + }, + }, + }, + }, + defaultVariants: { + full: false, + }, +}); + +interface MainContentProps { + children: React.ReactNode; + full?: boolean; +} +export default function MainContent({ + children, + full = false, +}: MainContentProps) { + return ( +
+
{children}
+
+ ); +} diff --git a/frontend/src/renderer/src/components/onboarding-grid/index.ts b/frontend/src/renderer/src/components/onboarding-grid/index.ts new file mode 100644 index 00000000..ecf7ff3a --- /dev/null +++ b/frontend/src/renderer/src/components/onboarding-grid/index.ts @@ -0,0 +1,15 @@ +import { styled } from "@/styles"; + +export const OnboardingGrid = styled("div", { + display: "grid", + justifyItems: "center", + gridTemplateColumns: "repeat(1, minmax(200px, 1fr))", + gap: "$xl 0", + + "@md": { + gridTemplateColumns: "repeat(4, minmax(160px, 1fr))", + }, + "@xl": { + gridTemplateColumns: "repeat(4, minmax(200px, 1fr))", + }, +}); diff --git a/frontend/src/renderer/src/components/radio/index.ts b/frontend/src/renderer/src/components/radio/index.ts new file mode 100644 index 00000000..8e8c2681 --- /dev/null +++ b/frontend/src/renderer/src/components/radio/index.ts @@ -0,0 +1,4 @@ +import Radio from "./radio"; +import RadioGroup from "./radio-group"; + +export { RadioGroup, Radio }; diff --git a/frontend/src/renderer/src/components/radio/radio-group/RadioGroup.styles.ts b/frontend/src/renderer/src/components/radio/radio-group/RadioGroup.styles.ts new file mode 100644 index 00000000..f727702b --- /dev/null +++ b/frontend/src/renderer/src/components/radio/radio-group/RadioGroup.styles.ts @@ -0,0 +1,30 @@ +import { styled } from "@/styles"; + +export const Legend = styled("legend", { + fontWeight: "$default", + fontSize: "$subtitleSm", + lineHeight: "$subtitleSm", + color: "$textLighter", + + mb: "$xs", +}); + +export const Group = styled("fieldset", { + display: "flex", + gap: "$m", + flexWrap: "wrap", + + variants: { + direction: { + horizontal: { + flexDirection: "row", + }, + vertical: { + flexDirection: "column", + }, + }, + }, + defaultVariants: { + direction: "horizontal", + }, +}); diff --git a/frontend/src/renderer/src/components/radio/radio-group/index.tsx b/frontend/src/renderer/src/components/radio/radio-group/index.tsx new file mode 100644 index 00000000..1f9e0066 --- /dev/null +++ b/frontend/src/renderer/src/components/radio/radio-group/index.tsx @@ -0,0 +1,39 @@ +import { + Children, + type ReactElement, + type ReactNode, + cloneElement, + isValidElement, +} from "react"; + +import type { Props as RadioProps } from "../radio"; +import { Group, Legend } from "./RadioGroup.styles"; + +interface Props { + children: ReactNode; + label?: string; + name: string; + direction?: "horizontal" | "vertical"; +} +export default function RadioGroup({ + label, + name, + direction = "horizontal", + children, +}: Props) { + // Inject the name to the children + const radiosWithName = Children.map(children, (child) => { + if (isValidElement(child)) { + return cloneElement(child as ReactElement, { + name, + }); + } + }); + + return ( + + {label && {label}} + {radiosWithName} + + ); +} diff --git a/frontend/src/renderer/src/components/radio/radio/Radio.styles.ts b/frontend/src/renderer/src/components/radio/radio/Radio.styles.ts new file mode 100644 index 00000000..874e81b6 --- /dev/null +++ b/frontend/src/renderer/src/components/radio/radio/Radio.styles.ts @@ -0,0 +1,143 @@ +import { styled } from "@/styles"; + +export const Input = styled("input", { + position: "absolute", + top: 0, + left: 0, + + opacity: 0, +}); + +export const Radio = styled("div", { + display: "flex", + alignItems: "center", + justifyContent: "center", + + width: 18, + height: 18, + p: 1, + + borderRadius: "100%", + boxSizing: "border-box", +}); + +export const Wrapper = styled("label", { + // Unchecked + $$radio_bg_default: "$colors$white", + $$radio_border_default: "$sizes$xxs solid $colors$actionDefaultAlt", + // hover + $$radio_bgHover_default: "$colors$white", + $$radio_borderHover_default: "1px solid $colors$borderPrimary", + // disabled + $$radio_bgDisabled_default: "$colors$actionDisabled", + $$radio_borderDisabled_default: "$sizes$xxs solid $colors$borderPrimary", + // focus + $$radio_bgFocus_default: "$colors$white", + $$radio_borderFocus_default: "1px solid $colors$borderPrimary", + + // Checked + $$radio_bg_checked: "$colors$actionDefaultAlt", + $$radio_border_checked: "5px solid $colors$actionDefaultAlt", + // hover + $$radio_bgHover_checked: "$colors$actionHover", + $$radio_borderHover_checked: "5px solid $colors$actionHover", + // disabled + $$radio_bgDisabled_checked: "$colors$actionDisabled", + $$radio_borderDisabled_checked: "none", + // focus + $$radio_bgFocus_checked: "$colors$actionDefaultAlt", + $$radio_borderFocus_checked: "5px solid $colors$actionDefaultAlt", + + // Focus + $$radio_outlineFocus_noText: "3px solid $colors$borderPrimaryAlt", + $$radio_outlineFocus_Text: "3px solid $colors$borderPrimary", + + position: "relative", + + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: "$s", + + transitionDuration: "$transitions$s", + transitionProperty: "outline, border-radius, border", + transitionTimingFunction: "ease", + + variants: { + hasText: { + true: { + p: "$s", + "&:focus-within": { + outline: "$$radio_outlineFocus_Text", + borderRadius: "3px", + }, + }, + false: { + "&:focus-within": { + outline: "$$radio_outlineFocus_noText", + borderRadius: "100%", + }, + // Focus for radio + [`& > input:checked:focus-visible + ${Radio}`]: { + bg: "$$radio_bgFocus_checked", + b: "$$radio_borderFocus_checked", + }, + [`& > input:focus-visible + ${Radio}`]: { + bg: "$$radio_bgFocus_default", + b: "$$radio_borderFocus_default", + }, + }, + }, + isDisabled: { + true: { + "&, & > *": { + cursor: "not-allowed", + }, + }, + false: { + "&, & > *": { + cursor: "pointer", + }, + }, + }, + }, + defaultVariants: { + hasText: "false", + isDisabled: "false", + }, + + // Radio styles + [`& > ${Radio}`]: { + b: "$$radio_border_default", + bg: "$$radio_bg_default", + }, + [`& > input:checked + ${Radio}`]: { + bg: "$$radio_bg_checked", + border: "$$radio_border_checked", + }, + + // Hide SVG when unchecked + [`& > input:not(:checked) + ${Radio} svg`]: { + display: "none", + }, + + // Hover + [`&:hover > ${Radio}`]: { + b: "$$radio_borderHover_default", + bg: "$$radio_bgHover_default", + }, + [`&:hover > input:checked + ${Radio}`]: { + b: "$$radio_borderHover_checked", + bg: "$$radio_bgHover_checked", + }, + + // Disabled + [`& > input:disabled + ${Radio}`]: { + bg: "$$radio_bgDisabled_default", + b: "$$radio_borderDisabled_default", + }, + [`& > input:checked:disabled + ${Radio}`]: { + bg: "$$radio_bgDisabled_checked", + b: "$$radio_borderDisabled_checked", + }, +}); diff --git a/frontend/src/renderer/src/components/radio/radio/index.tsx b/frontend/src/renderer/src/components/radio/radio/index.tsx new file mode 100644 index 00000000..57268d14 --- /dev/null +++ b/frontend/src/renderer/src/components/radio/radio/index.tsx @@ -0,0 +1,65 @@ +import { Circle } from "phosphor-react"; +import { + type ChangeEventHandler, + type ReactNode, + forwardRef, + useImperativeHandle, + useRef, +} from "react"; + +import type { CSS } from "@/styles"; +import { colors } from "@/styles/tokens"; +import { Input, Radio as StyledRadio, Wrapper } from "./Radio.styles"; + +export interface Props { + children?: ReactNode; + name?: string; + checked?: boolean; + disabled?: boolean; + onChange?: (value: boolean) => void; + css?: CSS; +} +export default forwardRef<{ value: boolean }, Props>(function Radio( + { name, checked = false, disabled = false, onChange, css, children }, + ref, +) { + const inputRef = useRef(null); + + const hasText = !!children; + + // Only exposes `value` object to the parent component + useImperativeHandle( + ref, + () => { + return { + value: inputRef.current?.checked ?? false, + }; + }, + [], + ); + + const handleToggle: ChangeEventHandler = (e) => { + onChange?.(e.target.checked); + }; + + const iconColor = disabled + ? colors.textOnButtonDisabled + : colors.textOnButtonAlternative; + + return ( + + + + + + {children} + + ); +}); diff --git a/frontend/src/renderer/src/components/spinner/index.tsx b/frontend/src/renderer/src/components/spinner/index.tsx new file mode 100644 index 00000000..78d6eed5 --- /dev/null +++ b/frontend/src/renderer/src/components/spinner/index.tsx @@ -0,0 +1,44 @@ +import { keyframes, styled } from "@/styles"; + +const spin = keyframes({ + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, +}); + +const SVG = styled("svg", { + animation: `${spin} 1s linear infinite`, +}); + +export default function Spinner() { + return ( + + + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/stack/index.ts b/frontend/src/renderer/src/components/stack/index.ts new file mode 100644 index 00000000..d6fae295 --- /dev/null +++ b/frontend/src/renderer/src/components/stack/index.ts @@ -0,0 +1,66 @@ +import { styled } from "@/styles"; + +/** + * @deprecated Use `Stack` from `@/styled/jsx` instead. + */ +const Stack = styled("div", { + display: "flex", + + variants: { + direction: { + row: { flexDirection: "row" }, + column: { flexDirection: "column" }, + "row-reverse": { flexDirection: "row-reverse" }, + "column-reverse": { flexDirection: "column-reverse" }, + }, + + wrap: { + wrap: { flexWrap: "wrap" }, + nowrap: { flexWrap: "nowrap" }, + "wrap-reverse": { flexWrap: "wrap-reverse" }, + }, + + justify: { + start: { justifyContent: "start" }, + end: { justifyContent: "end" }, + center: { justifyContent: "center" }, + "space-between": { justifyContent: "space-between" }, + "space-around": { justifyContent: "space-around" }, + "space-evenly": { justifyContent: "space-evenly" }, + }, + + align: { + start: { alignItems: "start" }, + end: { alignItems: "end" }, + center: { alignItems: "center" }, + stretch: { alignItems: "stretch" }, + baseline: { alignItems: "baseline" }, + }, + + spacing: { + none: { gap: 0 }, + xxs: { gap: "$xxs" }, + xs: { gap: "$xs" }, + s: { gap: "$s" }, + m: { gap: "$m" }, + l: { gap: "$l" }, + xl: { gap: "$xl" }, + }, + textColor: { + default: { + color: "$textDefault", + }, + }, + }, + + defaultVariants: { + direction: "row", + wrap: "wrap", + justify: "start", + align: "start", + spacing: "s", + textColor: "default", + }, +}); + +export default Stack; diff --git a/frontend/src/renderer/src/components/subtitle/index.ts b/frontend/src/renderer/src/components/subtitle/index.ts new file mode 100644 index 00000000..acdf6e5e --- /dev/null +++ b/frontend/src/renderer/src/components/subtitle/index.ts @@ -0,0 +1,39 @@ +import { styled } from "@/styles"; + +/** + * @deprecated Use `textStyle` from PandaCSS instead. + */ +const Subtitle = styled("p", { + variants: { + size: { + m: { + fontSize: "$subtitleMd", + lineHeight: "$subtitleMd", + }, + s: { + fontSize: "$subtitleSm", + lineHeight: "$subtitleSm", + }, + }, + weight: { + default: { + fontWeight: "$default", + }, + strong: { + fontWeight: "$strong", + }, + }, + textColor: { + default: { + color: "$textDefault", + }, + }, + }, + defaultVariants: { + size: "m", + weight: "default", + textColor: "default", + }, +}); + +export default Subtitle; diff --git a/frontend/src/renderer/src/components/tabs/index.ts b/frontend/src/renderer/src/components/tabs/index.ts new file mode 100644 index 00000000..320a5514 --- /dev/null +++ b/frontend/src/renderer/src/components/tabs/index.ts @@ -0,0 +1,56 @@ +import { styled } from "@/styles"; + +export const Tab = styled("div", { + borderRadius: "$xxs", + boxSizing: "border-box", + + display: "flex", + flexDirection: "row", + gap: "$s", + alignItems: "center", + + p: "$m", + + fontSize: 16, + + variants: { + status: { + completed: { + "& label, & span": { + color: "$textOnButtonAlternative", + }, + bg: "$actionPressed", + b: "none", + }, + focus: { + "& label, & span": { + color: "$textOnButtonDefault", + }, + bg: "$actionFocus", + boxShadow: "2px 2px 10px rgba(17, 0, 65, 0.25)", + b: "1px solid $borderPrimaryAlt", + }, + default: { + "& label, & span": { + color: "$textOnButtonDefault", + }, + bg: "$primaryAlt", + b: "none", + }, + }, + }, + defaultVariants: { + status: "default", + }, +}); + +export const TabName = styled("label", { + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", + + // width: 200, + + fontSize: "$labelMd", + lineHeight: "$labelMd", +}); diff --git a/frontend/src/renderer/src/components/text/index.ts b/frontend/src/renderer/src/components/text/index.ts new file mode 100644 index 00000000..c3e12968 --- /dev/null +++ b/frontend/src/renderer/src/components/text/index.ts @@ -0,0 +1,40 @@ +import { styled } from "@/styles"; + +const Text = styled("p", { + variants: { + size: { + m: { + fontSize: "$paragraphsMd", + lineHeight: "$paragraphsMd", + }, + s: { + fontSize: "$paragraphsSm", + lineHeight: "$paragraphsSm", + }, + xs: { + fontSize: "$paragraphsXsm", + lineHeight: "$paragraphsXsm", + }, + }, + weight: { + default: { + fontWeight: "$default", + }, + strong: { + fontWeight: "$strong", + }, + }, + textColor: { + default: { + color: "$textDefault", + }, + }, + }, + defaultVariants: { + size: "m", + weight: "default", + textColor: "default", + }, +}); + +export default Text; diff --git a/frontend/src/renderer/src/components/theme-provider/ThemeProvider.types.ts b/frontend/src/renderer/src/components/theme-provider/ThemeProvider.types.ts new file mode 100644 index 00000000..6186f91e --- /dev/null +++ b/frontend/src/renderer/src/components/theme-provider/ThemeProvider.types.ts @@ -0,0 +1,3 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export interface Props extends ComponentPropsWithoutRef<"div"> {} diff --git a/frontend/src/renderer/src/components/theme-provider/index.tsx b/frontend/src/renderer/src/components/theme-provider/index.tsx new file mode 100644 index 00000000..50b30fb2 --- /dev/null +++ b/frontend/src/renderer/src/components/theme-provider/index.tsx @@ -0,0 +1,6 @@ +import type { Props } from "./ThemeProvider.types"; + +export default function ThemeProvider({ children }: Props) { + // globalStyles(); + return
{children}
; +} diff --git a/frontend/src/renderer/src/components/title/index.ts b/frontend/src/renderer/src/components/title/index.ts new file mode 100644 index 00000000..668b6ec4 --- /dev/null +++ b/frontend/src/renderer/src/components/title/index.ts @@ -0,0 +1,39 @@ +import { styled } from "@/styles"; + +const Title = styled("h1", { + variants: { + size: { + default: { + fontSize: "$titleMd", + lineHeight: "$titleMd", + }, + main: { + fontSize: "$titleMain", + lineHeight: "$titleMain", + }, + }, + weight: { + default: { + fontWeight: "$default", + }, + strong: { + fontWeight: "$strong", + }, + heavy: { + fontWeight: "$heavy", + }, + }, + textColor: { + default: { + color: "$textDefault", + }, + }, + }, + defaultVariants: { + weight: "default", + size: "default", + textColor: "default", + }, +}); + +export default Title; diff --git a/frontend/src/renderer/src/components/ui/back-button.tsx b/frontend/src/renderer/src/components/ui/back-button.tsx new file mode 100644 index 00000000..c70c9070 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/back-button.tsx @@ -0,0 +1,17 @@ +import { css, cx } from "@/styled/css"; +import { createLink } from "@tanstack/react-router"; +import { ArrowLeft } from "phosphor-react"; + +const link = css({ + cursor: "pointer", +}); + +const BackButton = createLink( + ({ className, children: _, ref, ...props }: React.AnchorHTMLAttributes & { ref?: React.Ref }) => ( + + + + ), +); + +export default BackButton; diff --git a/frontend/src/renderer/src/components/ui/button.tsx b/frontend/src/renderer/src/components/ui/button.tsx new file mode 100644 index 00000000..5d015f81 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/button.tsx @@ -0,0 +1,150 @@ +import { type RecipeVariantProps, css, cva, cx } from "@/styled/css"; +import { CircleNotch } from "phosphor-react"; +import type { ButtonHTMLAttributes } from "react"; + +export const button = cva({ + base: { + display: "flex", + flexDir: "row", + gap: "1", // 4px + justifyContent: "center", + alignItems: "center", + + transitionProperty: "[background-color, color, box-shadow]", + transitionDuration: "slow", // 300ms + transitionTimingFunction: "default", + + border: "none", + + textStyle: "cta.md.strong", + + cursor: "pointer", + + "&:disabled": { + cursor: "not-allowed", + }, + }, + variants: { + variant: { + primary: { + bg: "action.default", + color: "text.onbutton-default", + + "&:hover:not(:disabled)": { + bg: "action.hover", + color: "text.onbutton-alternative", + }, + + "&:active:not(:disabled)": { + bg: "action.pressed", + color: "text.onbutton-alternative", + }, + + "&:focus:not(:disabled)": { + bg: "action.focus", + outline: "primary-alt", + outlineWidth: "[2px]", + boxShadow: "[0px 0px 10px rgba(17, 0, 65, 0.2)]", + }, + + "&:disabled": { + bg: "action.disabled", + color: "text.onbutton-default", + }, + }, + secondary: { + color: "text.onbutton-default", + bg: "bg.secondary", + + borderWidth: "[2px]", + borderStyle: "solid", + borderColor: "action.alt-default", + + "&:hover:not(:disabled)": { + color: "text.onbutton-default", + bg: "bg.secondary", + borderColor: "action.hover", + }, + + "&:active:not(:disabled)": { + color: "text.onbutton-alternative", + bg: "action.pressed", + borderColor: "action.pressed", + }, + + "&:focus:not(:disabled)": { + boxShadow: "[0px 0px 10px rgba(17, 0, 65, 0.2)]", + outline: "primary-alt", + outlineWidth: "[2px]", + bg: "bg.secondary", + }, + + "&:disabled": { + color: "text.onbutton-disabled", + bg: "bg.secondary", + borderColor: "action.disabled", + }, + }, + tertiary: { + // TODO: not implemented + }, + none: { + padding: "0", + bg: "[inherit]", + }, + }, + size: { + md: { height: "12", padding: "4", rounded: "sm" }, + sm: { height: "9", py: "2", px: "4", rounded: "sm" }, + "icon-md": { + height: "12", + width: "12", + padding: "3", + rounded: "md", + }, + "icon-sm": { height: "9", width: "9", padding: "2", rounded: "md" }, + }, + checked: { + true: { + bg: "action.pressed !important", + color: "text.onbutton-alternative !important", + }, + false: {}, + }, + }, + defaultVariants: { + size: "md", + variant: "primary", + checked: false, + }, +}); + +type ButtonProps = RecipeVariantProps & + ButtonHTMLAttributes & { + isLoading?: boolean; + }; + +export default function Button({ + size, + variant, + isLoading, + disabled, + children, + className, + checked, + ...props +}: ButtonProps) { + return ( + + ); +} diff --git a/frontend/src/renderer/src/components/ui/callout.tsx b/frontend/src/renderer/src/components/ui/callout.tsx new file mode 100644 index 00000000..d215abe2 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/callout.tsx @@ -0,0 +1,106 @@ +import { cva, cx } from "@/styled/css"; +import { styled } from "@/styled/jsx"; +import { hstack } from "@/styled/patterns"; +import type { Icon } from "phosphor-react"; +import { Bell, X } from "phosphor-react"; +import type { HTMLAttributes } from "react"; + +const calloutRecipe = cva({ + base: { + ...hstack.raw({ + alignItems: "center", + gap: "3", + }), + + px: "4", + py: "3", + + rounded: "sm", + borderWidth: "[1px]", + borderStyle: "solid", + + width: "full", + + textStyle: "paragraph.sm.default", + }, + variants: { + variant: { + error: { + bg: "system.error-secondary", + borderColor: "system.error", + color: "system.error", + }, + warning: { + bg: "system.warning-secondary", + borderColor: "system.warning", + color: "system.warning", + }, + success: { + bg: "system.success-secondary", + borderColor: "system.success", + color: "system.success", + }, + info: { + bg: "system.info-secondary", + borderColor: "system.info", + color: "text.default", + }, + }, + noBorder: { + true: { + borderWidth: "0", + }, + }, + }, + defaultVariants: { + variant: "info", + noBorder: false, + }, +}); + +export type CalloutVariant = "error" | "warning" | "success" | "info"; + +interface CalloutProps extends HTMLAttributes { + message: string; + variant?: CalloutVariant; + noBorder?: boolean; + onDismiss?: () => void; + icon?: Icon; +} + +function Callout({ + message, + variant = "info", + noBorder = false, + onDismiss, + icon: IconComponent = Bell, + className, + ...props +}: CalloutProps) { + return ( +
+
+ ); +} + +export default Callout; diff --git a/frontend/src/renderer/src/components/ui/card.tsx b/frontend/src/renderer/src/components/ui/card.tsx new file mode 100644 index 00000000..bc2b77e5 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import { cva, cx } from "@/styled/css"; + +const styles = cva({ + base: { + transitionProperty: "[border, box-shadow]", + transitionTimingFunction: "default", + transitionDuration: "normal", + }, + variants: { + size: { + lg: { + p: "8", + rounded: "sm", + }, + sm: { + rounded: "lg", + p: "4", + }, + }, + disabled: { + true: { + border: "primary", + bg: "bg.primary", + color: "text.lighter", + }, + false: { + border: "primary", + bg: "bg.secondary", + }, + }, + clickable: { + true: {}, + false: {}, + }, + }, + compoundVariants: [ + { + clickable: true, + disabled: false, + css: { + cursor: "pointer", + "&:hover": { + border: "primary-alt", + boxShadow: "[0px 0px 15px 0px #3F479D66]", + }, + }, + }, + { + clickable: true, + disabled: true, + css: { + cursor: "not-allowed", + }, + }, + ], + defaultVariants: { + disabled: false, + clickable: false, + size: "lg", + }, +}); + +interface CardProps { + children?: React.ReactNode; + disabled?: boolean; + size?: "lg" | "sm"; + clickable?: boolean; + className?: string; +} +export default function Card({ + disabled = false, + size = "lg", + clickable = false, + children, + className, +}: CardProps) { + const classes = styles({ size, disabled, clickable }); + return
{children}
; +} diff --git a/frontend/src/renderer/src/components/ui/dialog.tsx b/frontend/src/renderer/src/components/ui/dialog.tsx new file mode 100644 index 00000000..2cb6a475 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/dialog.tsx @@ -0,0 +1,115 @@ +import { css, cx } from "@/styled/css"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import type { ComponentPropsWithoutRef, HTMLAttributes } from "react"; + +const Dialog = DialogPrimitive.Root; +const DialogTrigger = DialogPrimitive.Trigger; +const DialogClose = DialogPrimitive.Close; +const DialogTitle = DialogPrimitive.Title; +const DialogDescription = DialogPrimitive.Description; + +const overlayStyles = css({ + position: "fixed", + inset: "[0]", + zIndex: 50, + bg: "[rgba(0, 0, 0, 0.5)]", + + "&[data-state='open']": { + animation: "fadeIn", + }, + "&[data-state='closed']": { + animation: "fadeOut", + }, +}); + +const contentStyles = css({ + position: "fixed", + inset: "[0]", + margin: "auto", + zIndex: 50, + + bg: "bg.secondary", + rounded: "sm", + p: "6", + boxShadow: "[0px 4px 8px rgba(0, 0, 0, 0.1)]", + + width: "[90vw]", + minWidth: "[300px]", + maxWidth: "[80%]", + h: "[fit-content]", + + "&[data-state='open']": { + animation: "fadeIn", + }, + "&[data-state='closed']": { + animation: "fadeOut", + }, +}); + +const headerStyles = css({ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + mb: "4", +}); + +const footerStyles = css({ + display: "flex", + gap: "2", + justifyContent: "flex-end", + alignItems: "center", + mt: "8", +}); + +function DialogOverlay({ + className, + ...props +}: ComponentPropsWithoutRef) { + return ( + + ); +} + +function DialogContent({ + className, + container, + children, + ...props +}: ComponentPropsWithoutRef & { + container?: HTMLElement; +}) { + return ( + + + + {children} + + + ); +} + +function DialogHeader({ className, ...props }: HTMLAttributes) { + return
; +} + +function DialogFooter({ className, ...props }: HTMLAttributes) { + return
; +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogTitle, + DialogTrigger, +}; diff --git a/frontend/src/renderer/src/components/ui/input.tsx b/frontend/src/renderer/src/components/ui/input.tsx new file mode 100644 index 00000000..674a1efd --- /dev/null +++ b/frontend/src/renderer/src/components/ui/input.tsx @@ -0,0 +1,207 @@ +import { WarningCircle } from "phosphor-react"; +import { useId } from "react"; + +import Suggestion from "@/components/ui/suggestion"; +import { cva, sva } from "@/styled/css"; +import { hstack, stack } from "@/styled/patterns"; + +const input = sva({ + slots: ["container", "inputBox", "input", "label", "errorMessage", "helper"], + base: { + container: stack.raw({ gap: "1", width: "full" }), + inputBox: { + ...hstack.raw({ alignItems: "center", gap: "1" }), + + rounded: "sm", + border: "primary", + + "&:focus-within": { + outline: "none", + boxShadow: "[0px 2px 2px rgba(0, 0, 0, 0.16)]", + }, + }, + input: { + textStyle: "label.md.default", + border: "none", + outline: "none", + flex: "[1]", + + "&::placeholder": { + color: "text.lighter", + }, + }, + label: { textStyle: "label.sm.default", color: "text.lighter" }, + errorMessage: { + ...hstack.raw({ gap: "1" }), + textStyle: "label.sm.default", + color: "system.error", + }, + helper: { + textStyle: "label.sm.default", + color: "text.lighter", + }, + }, + variants: { + size: { + md: { + input: { p: "3" }, + }, + sm: { + input: { px: "3", py: "1" }, + }, + }, + disabled: { + true: { + inputBox: { + bg: "bg.primary", + border: "primary", + color: "text.lighter", + cursor: "not-allowed", + }, + input: { + cursor: "not-allowed", + }, + }, + false: { + inputBox: { + bg: "white", + }, + }, + }, + error: { + true: { + container: { + color: "system.error", + }, + inputBox: { + border: "error", + }, + input: {}, + label: { color: "system.error" }, + }, + false: {}, + }, + }, + defaultVariants: { + error: false, + disabled: false, + size: "md", + }, +}); + +const affix = cva({ + base: { + ...hstack.raw({ alignItems: "center", gap: "1" }), + userSelect: "none", + textStyle: "label.md.default", + }, + variants: { + position: { + prefix: { pl: "3", mr: "-3" }, + suffix: { pr: "3", ml: "-3" }, + }, + }, +}); + +interface InputProps + extends Omit< + React.InputHTMLAttributes, + "prefix" | "suffix" | "size" + >, + React.RefAttributes { + // Rendering + label?: string; + placeholder?: string; + prefix?: string; + suffix?: string; + suggestion?: string; + helper?: string; + // Control + id?: string; + value: string | undefined; + onChange?: React.ChangeEventHandler; + disabled?: boolean; + error?: string | null; + type?: "text" | "number"; + size?: "md" | "sm"; +} +export default function Input({ + // Rendering + label, + placeholder, + prefix, + suffix, + suggestion, + helper, + // Control + id, + ref, + value, + onChange, + disabled = false, + error, + type, + size = "md", + ...props +}: InputProps) { + const randomId = useId(); + const inputId = id ?? randomId; + const errorMessageId = `${inputId}-error`; + + const classes = input({ disabled, error: !!error, size }); + + return ( +
+ {label && ( + + )} + +
+ {prefix && ( +
+ {prefix} + {/* In the designs this vertical bar is defined as a Text, but it's + not. Use a Label instead */} + | +
+ )} + + {suggestion && {suggestion}} + + + + {suffix && ( +
+ {/* In the designs this vertical bar is defined as a Text, but it's + not. Use a Label instead */} + | + {suffix} +
+ )} +
+ + {helper && !error &&

{helper}

} + {error && ( + + )} +
+ ); +} diff --git a/frontend/src/renderer/src/components/ui/link.tsx b/frontend/src/renderer/src/components/ui/link.tsx new file mode 100644 index 00000000..53042173 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/link.tsx @@ -0,0 +1,33 @@ +import { type RecipeVariantProps, css, cx } from "@/styled/css"; +import { Link as TanstackLink, type LinkProps as TanstackLinkProps } from "@tanstack/react-router"; +import { CircleNotch } from "phosphor-react"; +import { button } from "./button"; + +type LinkProps = RecipeVariantProps & + TanstackLinkProps & { + isLoading?: boolean; + className?: string; + }; + +export default function Link({ + size, + variant, + checked, + isLoading, + children, + className, + ...props +}: LinkProps) { + return ( + + {isLoading ? ( + + ) : ( + children + )} + + ); +} diff --git a/frontend/src/renderer/src/components/ui/popover.tsx b/frontend/src/renderer/src/components/ui/popover.tsx new file mode 100644 index 00000000..8be12b91 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/popover.tsx @@ -0,0 +1,53 @@ +import { css, cx } from "@/styled/css"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; +import type { ComponentPropsWithoutRef } from "react"; + +const Popover = PopoverPrimitive.Root; +const PopoverTrigger = PopoverPrimitive.Trigger; +const PopoverAnchor = PopoverPrimitive.Anchor; +const PopoverClose = PopoverPrimitive.Close; + +const contentStyles = css({ + zIndex: 50, + bg: "bg.primary", + rounded: "lg", + boxShadow: "[0px 0px 15px 0px #00000026]", + + "&[data-state='open']": { + animation: "fadeIn", + }, + "&[data-state='closed']": { + animation: "fadeOut", + }, +}); + +const arrowStyles = css({ + fill: "bg.secondary", +}); + +function PopoverContent({ + className, + sideOffset = 8, + showArrow = false, + container, + children, + ...props +}: ComponentPropsWithoutRef & { + showArrow?: boolean; + container?: HTMLElement; +}) { + return ( + + + {children} + {showArrow && } + + + ); +} + +export { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger }; diff --git a/frontend/src/renderer/src/components/ui/select.tsx b/frontend/src/renderer/src/components/ui/select.tsx new file mode 100644 index 00000000..b04110a1 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/select.tsx @@ -0,0 +1,306 @@ +import * as RadixSelect from "@radix-ui/react-select"; +import { CaretDown, CaretUp, Check } from "phosphor-react"; +import { type Ref, useId, useImperativeHandle } from "react"; + +import Suggestion from "@/components/ui/suggestion"; +import { sva } from "@/styled/css"; +import { styled } from "@/styled/jsx"; +import { stack } from "@/styled/patterns"; + +const Affix = styled("span", { + base: { + textStyle: "label.md.default", + color: "text.lighter", + flexShrink: "0", + }, +}); + +export type SelectOption = { id: string; text: string; shortText?: string }; +export type SelectSuggestion = { id: string; text?: string }; + +interface SelectProps { + options: SelectOption[]; + label?: string; + value?: string; + onChange?: (value: SelectOption) => void; + onOpenChange?: (open: boolean) => void; + prefix?: string; + suffix?: string; + suggestion?: SelectSuggestion; + priorityOrder?: string[]; + placeholder?: string; + disabled?: boolean; + size?: "md" | "sm"; + ref?: Ref<{ value: string | undefined }>; +} + +function orderByPriority(options: SelectOption[], priority: string[] = []) { + const filtered = options.filter(({ id }) => !priority.includes(id)); + const preferred = priority + .map((p) => options.find(({ id }) => p === id)) + .filter((o): o is SelectOption => !!o); + return [...preferred, ...filtered]; +} + +function secureSuggestion( + suggestion: SelectSuggestion | undefined, + options: SelectOption[], +): SelectOption | undefined { + if (!suggestion) return undefined; + if (suggestion.text) return { id: suggestion.id, text: suggestion.text }; + return options.find(({ id }) => id === suggestion.id); +} + +const select = sva({ + slots: [ + "container", + "trigger", + "value", + "caret", + "content", + "viewport", + "item", + "itemIndicator", + "scrollButton", + ], + base: { + container: { ...stack.raw({ gap: "1" }), width: "full" }, + trigger: { + display: "flex", + alignItems: "center", + gap: "2", + width: "full", + bg: "white", + border: "primary", + rounded: "sm", + cursor: "pointer", + textAlign: "left", + appearance: "none", + textStyle: "label.md.default", + color: "text.default", + + "&[data-state='open']": { + boxShadow: "[0px 2px 2px rgba(0, 0, 0, 0.16)]", + borderColor: "[#110041]", + }, + "&:focus-visible": { + outlineColor: "brand.primary", + outlineWidth: "0.5", + outlineStyle: "solid", + outlineOffset: "0.5", + }, + "&[data-disabled]": { + bg: "bg.primary", + cursor: "not-allowed", + color: "text.lighter", + }, + "&[data-placeholder]": { + color: "text.lighter", + }, + }, + value: { + flex: "[1]", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + caret: { + flexShrink: "0", + color: "text.default", + transition: "[transform 0.15s ease]", + + "[data-state='open'] > &": { + transform: "[rotate(180deg)]", + }, + }, + content: { + bg: "white", + rounded: "sm", + boxShadow: "[0px 8px 16px rgba(0, 0, 0, 0.08)]", + border: "secondary", + overflow: "hidden", + zIndex: "10", + minWidth: "[var(--radix-select-trigger-width)]", + }, + viewport: { + maxHeight: "[400px]", + overflowY: "auto", + }, + scrollButton: { + display: "flex", + alignItems: "center", + justifyContent: "center", + py: "1", + color: "text.lighter", + cursor: "default", + bg: "white", + }, + item: { + display: "flex", + alignItems: "center", + gap: "2", + textStyle: "label.md.default", + color: "text.default", + cursor: "pointer", + outline: "none", + userSelect: "none", + + "&[data-highlighted]": { + bg: "bg.primary-alternative", + }, + "&[data-state='checked']": { + bg: "bg.primary-alternative", + }, + "&[data-disabled]": { + cursor: "not-allowed", + color: "text.lighter", + }, + }, + itemIndicator: { + color: "brand.primary", + display: "flex", + alignItems: "center", + flexShrink: "0", + }, + }, + variants: { + size: { + md: { + trigger: { px: "3", py: "3" }, + item: { px: "3", py: "3" }, + }, + sm: { + trigger: { px: "3", py: "1" }, + item: { px: "3", py: "3" }, + }, + }, + }, + defaultVariants: { + size: "md", + }, +}); + +export default function Select({ + options, + label, + value, + onChange, + onOpenChange, + prefix, + suffix, + suggestion, + priorityOrder = [], + placeholder = "", + disabled = false, + size = "md", + ref, +}: SelectProps) { + const triggerId = useId(); + const classes = select({ size }); + + const orderedOptions = orderByPriority(options, priorityOrder); + const securedSuggestion = secureSuggestion(suggestion, options); + + useImperativeHandle(ref, () => ({ value }), [value]); + + const handleChange = (id: string) => { + const option = options.find((o) => o.id === id); + if (option) onChange?.(option); + }; + + const handleSuggestionClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (securedSuggestion) handleChange(securedSuggestion.id); + }; + + const handleSuggestionKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.stopPropagation(); + if (securedSuggestion) handleChange(securedSuggestion.id); + } + }; + + return ( +
+ {label && ( + + {label} + + )} + + + + {/* biome-ignore lint/a11y/useSemanticElements lint/a11y/useAriaPropsForRole: Radix merges role, aria-expanded, aria-controls, and tabIndex onto this div at runtime via asChild */} +
+ {prefix && } + + + + + + {!value && securedSuggestion ? ( + + ) : ( + + {value + ? (options.find((o) => o.id === value)?.shortText ?? + options.find((o) => o.id === value)?.text ?? + placeholder) + : undefined} + + )} + + + {suffix && } +
+
+ + + + + + + + {orderedOptions.map(({ id, text }) => ( + + + + + {text} + + ))} + + + + + + +
+
+ ); +} diff --git a/frontend/src/renderer/src/components/ui/suggestion.tsx b/frontend/src/renderer/src/components/ui/suggestion.tsx new file mode 100644 index 00000000..d41aa5a4 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/suggestion.tsx @@ -0,0 +1,25 @@ +import { styled } from "@/styled/jsx"; + +const Suggestion = styled("mark", { + base: { + bg: "bg.primary-alternative", + textStyle: "label.md.default", + display: "inline-flex", + }, + variants: { + clickable: { + true: { cursor: "pointer" }, + false: { cursor: "unset" }, + }, + rounded: { + true: { rounded: "md" }, + false: { rounded: "[0px]" }, + }, + }, + defaultVariants: { + clickable: false, + rounded: false, + }, +}); + +export default Suggestion; diff --git a/frontend/src/renderer/src/components/ui/switch.tsx b/frontend/src/renderer/src/components/ui/switch.tsx new file mode 100644 index 00000000..dfb2442b --- /dev/null +++ b/frontend/src/renderer/src/components/ui/switch.tsx @@ -0,0 +1,57 @@ +import * as RadixSwitch from "@radix-ui/react-switch"; +import { css } from "@/styled/css"; + +const rootStyle = css({ + width: "[44px]", + height: "[24px]", + borderRadius: "full", + border: "none", + cursor: "pointer", + position: "relative", + flexShrink: "0", + bg: "bg.primary-highlight", + + transitionProperty: "[background-color]", + transitionDuration: "slow", + transitionTimingFunction: "default", + + '&[data-state="checked"]': { + bg: "brand.primary", + }, + + "&:disabled": { + cursor: "not-allowed", + opacity: "0.4", + }, +}); + +const thumbStyle = css({ + display: "block", + width: "[20px]", + height: "[20px]", + borderRadius: "full", + bg: "white", + position: "absolute", + top: "[2px]", + left: "[2px]", + + transitionProperty: "[transform]", + transitionDuration: "slow", + transitionTimingFunction: "default", + + '&[data-state="checked"]': { + transform: "[translateX(20px)]", + }, +}); + +type SwitchProps = RadixSwitch.SwitchProps; + +function Switch(props: SwitchProps) { + return ( + + + + ); +} + +export default Switch; diff --git a/frontend/src/renderer/src/components/ui/toast.tsx b/frontend/src/renderer/src/components/ui/toast.tsx new file mode 100644 index 00000000..48f935a3 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/toast.tsx @@ -0,0 +1,35 @@ +import type { Icon } from "phosphor-react"; +import { type Toast as HotToast, toast } from "react-hot-toast"; +import Callout, { type CalloutVariant } from "./callout"; + +export type ToastVariant = CalloutVariant; + +interface ToastProps { + t: HotToast; + message: string; + variant?: ToastVariant; + icon?: Icon; +} + +const ASSERTIVE_VARIANTS: ToastVariant[] = ["error", "warning"]; + +function Toast({ t, message, variant = "info", icon }: ToastProps) { + const isAssertive = ASSERTIVE_VARIANTS.includes(variant); + + return ( +
+ toast.remove(t.id)} + role={isAssertive ? "alert" : "status"} + aria-live={isAssertive ? "assertive" : "polite"} + aria-atomic="true" + noBorder + /> +
+ ); +} + +export default Toast; diff --git a/frontend/src/renderer/src/components/ui/tooltip.tsx b/frontend/src/renderer/src/components/ui/tooltip.tsx new file mode 100644 index 00000000..537cf185 --- /dev/null +++ b/frontend/src/renderer/src/components/ui/tooltip.tsx @@ -0,0 +1,50 @@ +import { css, cx } from "@/styled/css"; +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import type { ComponentPropsWithoutRef } from "react"; + +const TooltipProvider = TooltipPrimitive.Provider; +const Tooltip = TooltipPrimitive.Root; +const TooltipTrigger = TooltipPrimitive.Trigger; + +const contentStyles = css({ + zIndex: 50, + bg: "bg.primary", + rounded: "md", + boxShadow: "[0px 0px 15px 0px #00000026]", + + "&[data-state='delayed-open']": { + animation: "fadeIn", + }, + "&[data-state='closed']": { + animation: "fadeOut", + }, +}); + +const arrowStyles = css({ + fill: "bg.primary", +}); + +function TooltipContent({ + className, + sideOffset = 6, + showArrow = true, + children, + ...props +}: ComponentPropsWithoutRef & { + showArrow?: boolean; +}) { + return ( + + + {children} + {showArrow && } + + + ); +} + +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; diff --git a/frontend/src/renderer/src/components/uncontrolled-input/UncontrolledInput.styles.ts b/frontend/src/renderer/src/components/uncontrolled-input/UncontrolledInput.styles.ts new file mode 100644 index 00000000..991ff6f9 --- /dev/null +++ b/frontend/src/renderer/src/components/uncontrolled-input/UncontrolledInput.styles.ts @@ -0,0 +1,43 @@ +import { styled } from "@/styles"; + +export const Container = styled("label", { + display: "flex", + flexDirection: "column", + gap: "$xs", + + fontWeight: 400, + fontSize: "$labelSm", + lineHeight: "$labelSm", + + color: "$textDefault", +}); + +export const InputContainer = styled("div", { + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: "$s", + flex: 1, + + p: 12, + + bg: "$bgSecondary", + b: "1px solid $borderPrimary", + borderRadius: "$xs", + + "&:focus": { + outline: "none", + b: "1px solid $borderPrimaryAlt", + boxShadow: "0px 2px 2px rgba(0, 0, 0, 0.16)", + }, +}); +export const Input = styled("input", { + fontWeight: 400, + fontSize: "$labelMd", + lineHeight: "$labelMd", + + b: "none", + outline: "none", + + flex: 1, +}); diff --git a/frontend/src/renderer/src/components/uncontrolled-input/index.tsx b/frontend/src/renderer/src/components/uncontrolled-input/index.tsx new file mode 100644 index 00000000..0e777e7b --- /dev/null +++ b/frontend/src/renderer/src/components/uncontrolled-input/index.tsx @@ -0,0 +1,141 @@ +import { + type ChangeEventHandler, + type KeyboardEvent, + type ReactNode, + useImperativeHandle, + useState, +} from "react"; + +import Label from "@/components/label"; +import Text from "@/components/text"; +import Suggestion from "@/components/ui/suggestion"; +import type { CSS } from "@/styles"; +import type { NativeComponent } from "@/types/component"; +import { forwardRef } from "react"; +import { + Container, + InputContainer, + Input as StyledInput, +} from "./UncontrolledInput.styles"; + +export type InputRefValue = { value: string }; +interface Props + extends NativeComponent<"input", "prefix" | "type" | "value" | "onChange"> { + label?: string; + suggestion?: string; + helper?: string; + sufix?: ReactNode; + prefix?: ReactNode; + defaultValue?: string; + onChange?: (value: string) => void; + type?: "text" | "number"; + css?: CSS; + specialCharacters?: string; +} +export default forwardRef<{ value: string }, Props>(function UncontrolledInput( + { + label, + helper, + suggestion, + prefix, + sufix, + defaultValue, + onChange, + type = "text", + specialCharacters = "", + ...props + }, + ref, +) { + const [value, setValue] = useState(defaultValue ?? ""); + + // Only exposes `selected` object to the parent component + useImperativeHandle( + ref, + () => { + return { + value, + }; + }, + [value], + ); + + const isValueEmpty = !value || value === ""; + + const updateValue = (newValue: string) => { + if (type === "number") { + const regex = new RegExp(`^[\\d${specialCharacters}.]+$`); + if (regex.test(newValue) || !newValue) { + setValue(newValue); + onChange?.(newValue); + return; + } + return; + } + + setValue(newValue); + + onChange?.(newValue); + }; + + const handleClickSuggestion = () => { + updateValue(suggestion as string); // We are sure is a string because the button is enabled only in case suggestion = string + }; + const handleKeySuggestion = (e: KeyboardEvent) => { + e.preventDefault(); + + if (e.code === "Space" || e.code === "Enter") + updateValue(suggestion as string); // We are sure is a string because the button is enabled only in case suggestion = string + }; + + const handleChange: ChangeEventHandler = (e) => { + updateValue(e.target.value); + }; + + return ( + + {/* LABEL */} + {label} + + {/* INPUT CONTAINER */} + + {/* PREFIX */} + {prefix && ( + <> + + | + + )} + + {/* INPUT */} + + + {/* SUGGESTION */} + {suggestion && isValueEmpty && ( + <> + | + + {suggestion} + + + )} + + {/* SUFIX */} + {sufix} + + + {/* HELPER */} + {helper && } + + ); +}); diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.styles.ts b/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.styles.ts new file mode 100644 index 00000000..60f516d8 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.styles.ts @@ -0,0 +1,9 @@ +import { styled } from "@/styles"; + +const Container = styled("div", { + display: "flex", + flexDirection: "column", + gap: 64, +}); + +export default Container; diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.types.ts b/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.types.ts new file mode 100644 index 00000000..47fea702 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/FormGroup.types.ts @@ -0,0 +1,17 @@ +import type { FormEventHandler } from "react"; + +import type { FormValue, RegisterFunction } from "@/hooks/useForm"; +import type { LabelDecisiones } from "@/types/aymurai"; +import type Suggester from "@/utils/predictions/suggestions"; + +export interface FormProps { + decision?: number; + register: RegisterFunction; + onSubmit: FormEventHandler; + onCheck: (checked: boolean) => void; + suggester: Suggester; +} + +export interface FormDecisionProps extends FormProps { + getDecisionValue: (n: number) => (field: LabelDecisiones) => FormValue; +} diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/forms/DatosAcusado.tsx b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/DatosAcusado.tsx new file mode 100644 index 00000000..e8c8a696 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/DatosAcusado.tsx @@ -0,0 +1,52 @@ +import { Input, ValidationForm } from "@/components"; +import Select from "@/components/ui/select"; +import { LabelType } from "@/types/aymurai"; +import type { FormProps } from "../FormGroup.types"; +import json from "./options.json"; + +export default function DatosAcusado({ + onSubmit, + onCheck, + register, + suggester, +}: FormProps) { + return ( + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoGral.tsx b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoGral.tsx new file mode 100644 index 00000000..e2548216 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoGral.tsx @@ -0,0 +1,55 @@ +import { Input, ValidationForm } from "@/components"; +import { LabelType } from "@/types/aymurai"; +import type { FormProps } from "../FormGroup.types"; + +export default function InfoGral({ + onSubmit, + onCheck, + register, + suggester, +}: FormProps) { + return ( + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoHecho.tsx b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoHecho.tsx new file mode 100644 index 00000000..fcb71365 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/InfoHecho.tsx @@ -0,0 +1,246 @@ +import { Plus } from "phosphor-react"; +import { useState } from "react"; + +import { + Button, + Checkbox, + CheckboxGroup, + Input, + Radio, + RadioGroup, + Stack, + ValidationForm, +} from "@/components"; +import Select from "@/components/ui/select"; +import { css } from "@/styled/css"; +import { LabelDecisiones } from "@/types/aymurai"; +import nArray from "@/utils/nArray"; +import type { FormDecisionProps } from "../FormGroup.types"; +import json from "./options.json"; + +export default function DatosDenunciante({ + decision, + onSubmit, + onCheck, + register, + suggester, + getDecisionValue, +}: FormDecisionProps) { + const [frasesAgresion, setFrasesAgresion] = useState(1); + + const newFraseAgresion = () => setFrasesAgresion(frasesAgresion + 1); + + const frasesArray = nArray(frasesAgresion, undefined).map((_, i) => i); + const fraseName = (i: number) => `${LabelDecisiones.FRASES_AGRESION}${i}`; + const defaultValue = getDecisionValue(decision ?? 0); + const prop = (label: LabelDecisiones) => register(label, decision); + + return ( + + + + + + {frasesArray.map((i) => ( + 1 ? i + 1 : undefined} + ref={prop(fraseName(i) as LabelDecisiones)} // Treat this dynamic Label the same way as any other + label="Frases de la agresión" + {...suggester.text(fraseName(i) as LabelDecisiones)} + /> + ))} + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/forms/index.ts b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/index.ts new file mode 100644 index 00000000..0acd781a --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/index.ts @@ -0,0 +1,7 @@ +import DatosAcusado from "./DatosAcusado"; +import DatosDenunciante from "./DatosDenunciante"; +import Decision from "./Decision"; +import InfoGral from "./InfoGral"; +import InfoHecho from "./InfoHecho"; + +export { InfoGral, DatosDenunciante, DatosAcusado, InfoHecho, Decision }; diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/forms/options.json b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/options.json new file mode 100644 index 00000000..a35142a8 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/forms/options.json @@ -0,0 +1,1937 @@ +{ + "GENERO": [ + { "id": "varon_cis", "text": "Varón cis" }, + { "id": "mujer_cis", "text": "Mujer cis" }, + { "id": "varon_trans", "text": "Varón trans" }, + { "id": "mujer_trans", "text": "Mujer trans" }, + { "id": "travesti", "text": "Travesti" }, + { "id": "no_binaria", "text": "No binaria" }, + { "id": "sd", "text": "S/D" }, + { "id": "no_corresponde", "text": "No corresponde" } + ], + "ZONA_DEL_HECHO": [ + { "id": "zona_sur", "text": "Zona Sur" }, + { "id": "zona_este", "text": "Zona Este" }, + { "id": "zona_oeste", "text": "Zona Oeste" }, + { "id": "zona_norte", "text": "Zona Norte" }, + { "id": "provincia_de_buenos_aires", "text": "Provincia de Buenos Aires" }, + { "id": "no_corresponde", "text": "No corresponde" }, + { "id": "mas_de_una_zona", "text": "Más de una zona" }, + { "id": "s/d", "text": "S/D" } + ], + "LUGAR_DEL_HECHO": [ + { "id": "en_auto_particular", "text": "En auto particular" }, + { "id": "en_comisaria", "text": "En comisaría" }, + { + "id": "en_complejo_penitenciario", + "text": "En complejo penitenciario" + }, + { "id": "en_domicilio_laboral", "text": "En domicilio laboral" }, + { + "id": "en_domicilio_laboral_y_mediante_medios_tecnologicos", + "text": "En domicilio laboral y mediante medios tecnológicos" + }, + { "id": "en_domicilio_particular", "text": "En domicilio particular" }, + { + "id": "en_domicilio_particular_y_en_puerta_de_domicilio_particular", + "text": "En domicilio particular y en puerta de domicilio particular" + }, + { + "id": "en_domicilio_particular_y_mediante_medios_tecnologicos", + "text": "En domicilio particular y mediante medios tecnológicos" + }, + { "id": "en_estadio_de_futbol", "text": "En estadio de fútbol" }, + { "id": "en_geriatrico", "text": "En geriátrico" }, + { "id": "en_hospital", "text": "En hospital" }, + { "id": "en_institucion_privada", "text": "En institución privada" }, + { "id": "en_lugar_privado", "text": "En lugar privado" }, + { "id": "en_lugar_publico", "text": "En lugar público" }, + { + "id": "en_puerta_de_domicilio_particular_y_mediante_medios_tecnológicos", + "text": "En puerta de domicilio particular y mediante medios tecnológicos" + }, + { "id": "en_transporte_publico", "text": "En transporte público" }, + { + "id": "en_una_institucion_educativa", + "text": "En una institución educativa" + }, + { + "id": "en_via_publica_y_mediante_medios_tecnologicos", + "text": "En vía pública y mediante medios tecnológicos" + }, + { + "id": "mediante_medios_tecnologicos", + "text": "Mediante medios tecnológicos" + }, + { "id": "pasillo_de_domicilio", "text": "Pasillo de domicilio" }, + { + "id": "puerta_de_domicilio_particular_y_en_domicilio_laboral", + "text": "Puerta de domicilio particular y en domicilio laboral" + }, + { + "id": "puerta_de_domicilio_particular", + "text": "Puerta de domicilio particular" + }, + { + "id": "puerta_de_domicilio_particular_y_en_puerta_de_domicilio_laboral", + "text": "Puerta de domicilio particular y en puerta de domicilio laboral" + }, + { "id": "via_publica", "text": "Vía pública" }, + { + "id": "via_publica_y_en_domicilio_particular", + "text": "Via pública y en domicilio particular" + }, + { + "id": "via_publica_y_en_domicilio_particular_y_mediante_medios_tecnologicos", + "text": "Vía pública y en domicilio particular y mediante medios tecnológicos" + } + ], + "FRECUENCIA_EPISODIOS": [ + { "id": "primera_vez", "text": "Primera vez" }, + { "id": "esporadico", "text": "Esporádico" }, + { "id": "diario", "text": "Diario" }, + { "id": "habitual", "text": "Habitual" }, + { "id": "eventual", "text": "Eventual" }, + { "id": "sd", "text": "S/D" }, + { "id": "no_corresponde", "text": "No corresponde" } + ], + "NIVEL_INSTRUCCION": [ + { "id": "sin_instruccion", "text": "Sin instruccion" }, + { "id": "sin_escolarizar", "text": "Sin escolarizar" }, + { "id": "primario_incompleto", "text": "Primario incompleto" }, + { "id": "primario_completo", "text": "Primario completo" }, + { "id": "primario_en_curso", "text": "Primario en curso" }, + { "id": "secundario_incompleto", "text": "Secundario incompleto" }, + { "id": "secundario_completo", "text": "Secundario completo" }, + { "id": "secundario_en_curso", "text": "Secundario en curso" }, + { "id": "terciario_incompleto", "text": "Terciario incompleto" }, + { "id": "terciario_completo", "text": "Terciario completo" }, + { "id": "terciario_en_curso", "text": "Terciario en curso" }, + { + "id": "universitario_incompleto", + "text": "Universitario incompleto" + }, + { "id": "universitario_completo", "text": "Universitario completo" }, + { "id": "universitario_en_curso", "text": "Universitario en curso" }, + { "id": "sd", "text": "S/D" }, + { "id": "no_corresponde", "text": "No corresponde" } + ], + "MODALIDAD_DE_LA_VIOLENCIA": [ + { "id": "domestica", "text": "Doméstica" }, + { "id": "en_espacio_publico", "text": "En espacio público" }, + { "id": "institucional", "text": "Institucional" }, + { "id": "laboral", "text": "Laboral" }, + { "id": "libertad_reproductiva", "text": "Libertad reproductiva" }, + { "id": "mediatica", "text": "Mediática" }, + { "id": "no_corresponde", "text": "No corresponde" }, + { "id": "obstetrica", "text": "Obstétrica" }, + { "id": "publica_politica", "text": "Pública política" } + ], + "TIPO_DE_RESOLUCION": [ + { "id": "definitiva", "text": "Definitiva" }, + { "id": "interlocutoria", "text": "Interlocutoria" } + ], + "DECISION": [ + { "id": "hace_lugar", "text": "Hace lugar" }, + { "id": "no_hace_lugar", "text": "No hace lugar" } + ], + "CONDUCTA": [ + { + "id": "abandonar_animal_domestico", + "text": "Abandonar animal doméstico" + }, + { "id": "abandono_de_personas", "text": "Abandono de personas" }, + { "id": "abuso_de_armas", "text": "Abuso de armas" }, + { + "id": "abuso_de_autoridad_e_incumplimiento_deberes_funcionario_publico", + "text": "Abuso de autoridad e incumplimiento deberes funcionario público" + }, + { "id": "abuso_sexual", "text": "Abuso sexual" }, + { + "id": "acceder_lugares_distintos_segun_entrada", + "text": "Acceder lugares distintos según entrada" + }, + { + "id": "acceder_a_lugares_distintos_segun_entrada", + "text": "Acceder a lugares distintos según entrada" + }, + { + "id": "acceso_a_sistema_restringido", + "text": "Acceso a sistema restringido" + }, + { "id": "acoso_sexual_callejero", "text": "Acoso sexual callejero" }, + { + "id": "actividades_lucrativas_sin_autorizacion", + "text": "Actividades lucrativas sin autorización" + }, + { + "id": "actividades_lucrativas_sin_habilitacion", + "text": "Actividades lucrativas sin habilitación" + }, + { + "id": "actos_contenido_sexual_con_menores", + "text": "Actos contenido sexual con menores" + }, + { + "id": "administracion_fraudulenta", + "text": "Administración fraudulenta" + }, + { + "id": "afectar_desarrollo_espectaculo", + "text": "Afectar desarrollo espectáculo" + }, + { + "id": "afectar_el_desarrollo_del_espectaculo", + "text": "Afectar el desarrollo del espectáculo" + }, + { + "id": "afectar_funcionamiento_servicios_publicos", + "text": "Afectar funcionamiento servicios públicos" + }, + { + "id": "afectar_servicios_de_emergencia_o_seguridad", + "text": "Afectar servicios de emergencia o seguridad" + }, + { + "id": "afectar_servicios_emergencia", + "text": "Afectar servicios emergencia" + }, + { "id": "afectar_señalizacion", "text": "Afectar señalización" }, + { "id": "allanamiento_autonomo", "text": "Allanamiento autónomo" }, + { "id": "alterar_programa", "text": "Alterar programa" }, + { "id": "alterar_sepulturas", "text": "Alterar sepulturas" }, + { "id": "amenazas", "text": "Amenazas" }, + { "id": "amparo", "text": "Amparo" }, + { "id": "apariencia_falsa", "text": "Apariencia falsa" }, + { + "id": "apariencia_falsa_para_entrar_a_domicilio_o_lugar_privado", + "text": "Apariencia falsa para entrar a domicilio o lugar privado" + }, + { + "id": "apropiacion_indebida_de_tributos", + "text": "Apropiacion indebida de tributos" + }, + { + "id": "arrojar_cosas_que_puedan_causar_lesiones", + "text": "Arrojar cosas que puedan causar lesiones" + }, + { "id": "arrojar_cosas_sustancias", "text": "Arrojar cosas sustancias" }, + { + "id": "arrojar_sustancias_insalubres_en_lugares_publicos", + "text": "Arrojar sustancias insalubres en lugares públicos" + }, + { "id": "asociacion_ilicita", "text": "Asociacion ilícita" }, + { + "id": "asuncion_falsa_de_contravencion", + "text": "Asunción falsa de contravención" + }, + { + "id": "atentado_contra_la_autoridad", + "text": "Atentado contra la autoridad" + }, + { "id": "ausencia_de_habilitacion", "text": "Ausencia de habilitación" }, + { "id": "banderas", "text": "Banderas" }, + { "id": "carteles_afiches_volantes", "text": "Carteles afiches volantes" }, + { "id": "cierre_defectuoso", "text": "Cierre defectuoso" }, + { + "id": "circular_por_carriles_exclusivos", + "text": "Circular por carriles exclusivos" + }, + { + "id": "circular_por_lugar_prohibido", + "text": "Circular por lugar prohibido" + }, + { "id": "cohecho_activo", "text": "Cohecho activo" }, + { "id": "cohecho_pasivo", "text": "Cohecho pasivo" }, + { "id": "coimas", "text": "Coimas" }, + { "id": "competencia_desleal", "text": "Competencia desleal" }, + { + "id": "conducir_con_mayor_cantidad_de_alcohol_en_sangre_del_permitido", + "text": "Conducir con mayor cantidad de alcohol en sangre del permitido" + }, + { + "id": "conducir_con_mayor_grado_de_alcohol_o_bajo_efectos_de_estupefacientes", + "text": "Conducir con mayor grado de alcohol o bajo efectos de estupefacientes" + }, + { + "id": "conducir_sin_licencia_que_lo_habilite_por_categoria_de_vehiculo", + "text": "Conducir sin licencia que lo habilite por categoría de vehiculo" + }, + { + "id": "conducir_usando_aparato_electronico", + "text": "Conducir usando aparato electrónico" + }, + { "id": "connivencia_policial", "text": "Connivencia policial" }, + { + "id": "contactar_menor_por_medio_de_tecnologias_para_cometer_delitos_contra_su_integridad_sexual", + "text": "Contactar menor por medio de tecnologías para cometer delitos contra su integridad sexual" + }, + { "id": "cruce_de_semaforo_en_rojo", "text": "Cruce de semaforo en rojo" }, + { "id": "cuida_coche", "text": "Cuida coche" }, + { + "id": "cuidar_coches_sin_autorizacion", + "text": "Cuidar coches sin autorización" + }, + { "id": "daños", "text": "Daños" }, + { "id": "daños_informaticos", "text": "Daños informáticos" }, + { "id": "defraudacion", "text": "Defraudación" }, + { + "id": "defraudacion_contra_la_administracion_publica", + "text": "Defraudación contra la administración pública" + }, + { + "id": "delito_contra_seguridad_transito", + "text": "Delito contra seguridad tránsito" + }, + { "id": "denegacion_de_justicia", "text": "Denegación de justicia" }, + { "id": "derecho_admision", "text": "Derecho admisión" }, + { "id": "desarmado_automotor", "text": "Desarmado automotor" }, + { + "id": "desinfeccion_y_desratizacion", + "text": "Desinfección y desratización" + }, + { + "id": "desobediencia_a_cargas_procesales", + "text": "Desobediencia a cargas procesales" + }, + { + "id": "desobediencia_a_la_autoridad", + "text": "Desobediencia a la autoridad" + }, + { + "id": "difusion_no_autorizada_de_imagenes", + "text": "Difusión no autorizada de imágenes" + }, + { + "id": "difusion_no_autorizada_de_imagenes_intimas", + "text": "Difusion no autorizada de imágenes íntimas" + }, + { "id": "discriminar", "text": "Discriminar" }, + { "id": "documentacion_sanitaria", "text": "Documentación sanitaria" }, + { "id": "ejecucion_de_multa", "text": "Ejecución de multa" }, + { + "id": "ejercer_ilegitimamente_actividad", + "text": "Ejercer ilegítimamente actividad" + }, + { + "id": "ejercicio_ilegal_de_la_medicina", + "text": "Ejercicio ilegal de la medicina" + }, + { + "id": "elementos_de_prevencion_contra_incendio", + "text": "Elementos de prevención contra incendio" + }, + { "id": "encubrimiento", "text": "Encubrimiento" }, + { + "id": "encubrimiento_actividades_baile", + "text": "Encubrimiento actividades baile" + }, + { "id": "ensuciar_bienes", "text": "Ensuciar bienes" }, + { + "id": "entregar_indebidamente_armas_explosivos", + "text": "Entregar indebidamente armas explosivos" + }, + { "id": "espantar_animales", "text": "Espantar animales" }, + { "id": "espejos_retrovisores", "text": "Espejos retrovisores" }, + { "id": "estacionamiento_prohibido", "text": "Estacionamiento prohibido" }, + { + "id": "estacionamiento_sobre_senda_peatonal", + "text": "Estacionamiento sobre senda peatonal" + }, + { "id": "estafa", "text": "Estafa" }, + { "id": "evasion_tributaria", "text": "Evasión tributaria" }, + { "id": "estupefacientes", "text": "Estupefacientes" }, + { "id": "exceso_capacidad_ingreso", "text": "Exceso capacidad ingreso" }, + { "id": "exceso_de_velocidad", "text": "Exceso de velocidad" }, + { + "id": "exhibicion_de_documentacion_obligatoria", + "text": "Exhibición de documentacion obligatoria" + }, + { "id": "exhibiciones_obscenas", "text": "Exhibiciones obscenas" }, + { + "id": "exhortos_de_otras_jurisdicciones", + "text": "Exhortos de otras jurisdicciones" + }, + { + "id": "explotacion_trabajo_infantil", + "text": "Explotación trabajo infantil" + }, + { "id": "extorsion", "text": "Extorsión" }, + { + "id": "fabricar_transportar_artefactos_pirotecnicos", + "text": "Fabricar transportar artefactos pirotécnicos" + }, + { "id": "falsa_denuncia", "text": "Falsa denuncia" }, + { + "id": "falsedad_documental_de_certificado_de_aptitud_ambiental", + "text": "Falsedad documental de certificado de aptitud ambiental" + }, + { + "id": "falsificacion_certificado_medico", + "text": "Falsificación certificado médico" + }, + { + "id": "falsificacion_de_documento", + "text": "Falsificación de documento" + }, + { + "id": "falsificacion_de_marcas_firmas_señas_oficiales", + "text": "Falsificación de marcas firmas señas oficiales" + }, + { + "id": "falsificacion_de_numeracion_de_objeto_registrada_de_acuerdo_a_ley", + "text": "Falsificación de numeración de objeto registrada de acuerdo a ley" + }, + { + "id": "falta_de_poliza_de_seguros", + "text": "Falta de póliza de seguros" + }, + { + "id": "favorecimiento_evasion_persona_detenida", + "text": "Favorecimiento evasión persona detenida" + }, + { "id": "frustrar_subasta_publica", "text": "Frustrar subasta pública" }, + { + "id": "guardar_artefactos_pirotecnicos", + "text": "Guardar artefactos pirotécnicos" + }, + { + "id": "guardar_elementos_aptos_violencia", + "text": "Guardar elementos aptos violencia" + }, + { "id": "habeas_corpus", "text": "Habeas corpus" }, + { + "id": "habilitacion_en_infraccion", + "text": "Habilitación en infracción" + }, + { "id": "homicidio", "text": "Homicidio" }, + { "id": "hostigamiento", "text": "Hostigamiento" }, + { "id": "hostigamiento_digital", "text": "Hostigamiento digital" }, + { "id": "hurto", "text": "Hurto" }, + { + "id": "impedimento_de_contacto_de_menor_con_padre_no_conviviente", + "text": "Impedimento de contacto de menor con padre no conviviente" + }, + { + "id": "incendio_explosion_inundacion_con_peligro_para_bienes", + "text": "Incendio explosión inundación con peligro para bienes" + }, + { + "id": "incendios_y_otros_estragos", + "text": "Incendios y otros estragos" + }, + { "id": "incitar_desorden", "text": "Incitar desorden" }, + { "id": "incitar_al_desorden", "text": "Incitar al desorden" }, + { "id": "incumplimiento_de_plazos", "text": "Incumplimiento de plazos" }, + { + "id": "incumplimiento_deberes_familiares", + "text": "Incumplimiento deberes familiares" + }, + { + "id": "incumplimiento_deberes_funcionario_publico", + "text": "Incumplimiento deberes funcionario público" + }, + { + "id": "incumplimiento_medidas_prevencion_sanidad", + "text": "Incumplimiento medidas prevención sanidad" + }, + { + "id": "incumplimiento_perimetro_profundidad", + "text": "Incumplimiento perímetro profundidad" + }, + { "id": "incumplir_clausura", "text": "Incumplir clausura" }, + { + "id": "incumplir_obligaciones_legales", + "text": "Incumplir obligaciones legales" + }, + { "id": "inducir_menor_a_mendigar", "text": "Inducir menor a mendigar" }, + { + "id": "infraccion_reglamentos_seguridad", + "text": "Infracción reglamentos seguridad" + }, + { + "id": "ingresar_artefactos_pirotecnicos", + "text": "Ingresar artefactos pirotécnicos" + }, + { + "id": "ingresar_consumir_bebidas_alcoholicas", + "text": "Ingresar consumir bebidas alcohólicas" + }, + { + "id": "ingresar_contra_derecho_admision", + "text": "Ingresar contra derecho admisión" + }, + { + "id": "ingresar_sin_autorizacion_lugares_reservados", + "text": "Ingresar sin autorización lugares reservados" + }, + { "id": "ingresar_sin_entrada", "text": "Ingresar sin entrada" }, + { + "id": "ingresar_sin_entrada_autorizacion", + "text": "Ingresar sin entrada autorización" + }, + { + "id": "ingreso_a_domicilio_sin_autorizacion", + "text": "Ingreso a domicilio sin autorización" + }, + { "id": "inhumar_exhumar_profanar", "text": "Inhumar exhumar profanar" }, + { "id": "intimidacion", "text": "Intimidación" }, + { "id": "juego_sin_autorizacion", "text": "Juego sin autorización" }, + { + "id": "juegos_de_azar_sin_autorizacion", + "text": "Juegos de azar sin autorización" + }, + { "id": "lesiones", "text": "Lesiones" }, + { "id": "ley_451", "text": "Ley 451" }, + { "id": "licencia_vencida", "text": "Licencia vencida" }, + { "id": "maltrato", "text": "Maltrato" }, + { + "id": "mantener_animal_domestico_espacios_inadecuados", + "text": "Mantener animal doméstico espacios inadecuados" + }, + { + "id": "mantenimiento_de_cercas_y_aceras", + "text": "Mantenimiento de cercas y aceras" + }, + { + "id": "menoscabar_integridad_animal_domestico", + "text": "Menoscabar integridad animal doméstico" + }, + { "id": "mesnna_masnna", "text": "Mesnna masnna" }, + { + "id": "no_realizar_grabado_de_autopartes", + "text": "No realizar grabado de autopartes" + }, + { "id": "no_respetar_carriles", "text": "No respetar carriles" }, + { + "id": "no_respetar_senda_peatonal", + "text": "No respetar senda peatonal" + }, + { + "id": "obligacion_de_exhibir_cartel", + "text": "Obligación de exhibir cartel" + }, + { + "id": "obligacion_de_informar_prohibicion_de_fumar", + "text": "Obligación de informar prohibición de fumar" + }, + { "id": "obligacion_de_conservar", "text": "Obligación de conservar" }, + { + "id": "obstaculizar_ingreso_o_salida", + "text": "Obstaculizar ingreso o salida" + }, + { "id": "obstruccion_de_inspeccion", "text": "Obstrucción de inspección" }, + { "id": "obstruccion_de_via", "text": "Obstrucción de vía" }, + { + "id": "obstruccion_de_via_publica", + "text": "Obstruccion de vía pública" + }, + { + "id": "obstruccion_via_publica", + "text": "Obstruccion vía pública" + }, + { "id": "obstruir_salida", "text": "Obstruir salida" }, + { "id": "ocupar_via_publica", "text": "Ocupar vía pública" }, + { + "id": "oferta_demanda_sexo_espacio_publico", + "text": "Oferta demanda sexo espacio público" + }, + { + "id": "omitir_cuidados_animal_domestico", + "text": "Omitir cuidados animal doméstico" + }, + { + "id": "omitir_recaudos_de_cuidado_animal_domestico", + "text": "Omitir recaudos de cuidado animal doméstico" + }, + { + "id": "omitir_recaudos_de_organizacion", + "text": "Omitir recaudos de organización" + }, + { + "id": "omitir_recaudos_espectaculo_masivo", + "text": "Omitir recaudos espectáculo masivo" + }, + { + "id": "omitir_recaudos_organizacion_seguridad", + "text": "Omitir recaudos organización seguridad" + }, + { "id": "organizar_explotar_juego", "text": "Organizar explotar juego" }, + { + "id": "organizar_sin_autorizacion_juego", + "text": "Organizar sin autorización juego" + }, + { + "id": "participar_competencias_velocidad_via_publica", + "text": "Participar competencias velocidad via pública" + }, + { "id": "pelear_en_lugar_publico", "text": "Pelear en lugar público" }, + { "id": "permiso_y_planos_de_obra", "text": "Permiso y planos de obra" }, + { "id": "persona_no_habilitada", "text": "Persona no habilitada" }, + { + "id": "perturbar_ceremonias_religiosas_funebres", + "text": "Perturbar ceremonias religiosas fúnebres" + }, + { + "id": "perturbar_filas_ingreso_no_respetar_vallado", + "text": "Perturbar filas ingreso no respetar vallado" + }, + { "id": "placas_de_dominio", "text": "Placas de dominio" }, + { "id": "pornografia_infantil", "text": "Pornografía infantil" }, + { "id": "portacion_de_arma", "text": "Portación de arma" }, + { + "id": "portar_armas_no_convencionales", + "text": "Portar armas no convencionales" + }, + { + "id": "presencia_menores_lugar_no_autorizado", + "text": "Presencia menores lugar no autorizado" + }, + { "id": "presunta_comision_delito", "text": "Presunta comisión delito" }, + { "id": "presunta_contravencion", "text": "Presunta contravención" }, + { + "id": "privacion_ilegitima_de_la_libertad", + "text": "Privacion ilegítima de la libertad" + }, + { "id": "producir_avalanchas", "text": "Producir avalanchas" }, + { + "id": "promocion_o_facilitacion_de_prostitucion_de_personas", + "text": "Promoción o facilitación de prostitución de personas" + }, + { + "id": "promover_comerciar_ofertar_juego", + "text": "Promover comerciar ofertar juego" + }, + { + "id": "propaganda_discriminatoria", + "text": "Propaganda discriminatoria" + }, + { "id": "proteccion_animal", "text": "Protección animal" }, + { "id": "provocar_parcialidad", "text": "Provocar parcialidad" }, + { + "id": "publicidad_en_lugares_no_habilitados_via_publica", + "text": "Publicidad en lugares no habilitados via pública" + }, + { + "id": "regentear_o_administrar_casas_de_tolerancia", + "text": "Regentear o administrar casas de tolerancia" + }, + { "id": "residuos_peligrosos", "text": "Residuos peligrosos" }, + { "id": "retencion_indebida", "text": "Retención indebida" }, + { "id": "revender_entradas", "text": "Revender entradas" }, + { "id": "robo", "text": "Robo" }, + { "id": "ruidos_molestos", "text": "Ruidos molestos" }, + { "id": "ruidos_y_vibraciones", "text": "Ruidos y vibraciones" }, + { "id": "sancion_generica", "text": "Sanción generica" }, + { + "id": "servicios_de_seguridad_prohibiciones_incumplidas", + "text": "Servicios de seguridad prohibiciones incumplidas" + }, + { + "id": "servicios_de_seguridad_requisitos_incumplidos", + "text": "Servicios de seguridad requisitos incumplidos" + }, + { "id": "suministrar_alcohol_menor", "text": "Suministrar alcohol menor" }, + { + "id": "suministrar_bebidas_alcoholicas", + "text": "Suministrar bebidas alcohólicas" + }, + { + "id": "suministrar_elementos_aptos_agresion", + "text": "Suministrar elementos aptos agresión" + }, + { + "id": "suministrar_material_pornografico", + "text": "Suministrar material pornográfico" + }, + { + "id": "suministrar_objetos_peligrosos", + "text": "Suministrar objetos peligrosos" + }, + { + "id": "suministrar_productos_farmaceuticos", + "text": "Suministrar productos farmacéuticos" + }, + { + "id": "suministro_de_medicamentos", + "text": "Suministro de medicamentos" + }, + { "id": "suplantacion_identidad", "text": "Suplantación identidad" }, + { + "id": "suplantacion_digital_de_identidad", + "text": "Suplantación digital de identidad" + }, + + { + "id": "taxis_transportes_escolares_remises_sin_autorizacion", + "text": "Taxis transportes escolares remises sin autorización" + }, + { "id": "tenencia_de_arma", "text": "Tenencia de arma" }, + { "id": "transporte_de_pasajeros", "text": "Transporte de pasajeros" }, + { + "id": "transporte_de_pasajeros_sin_habilitacion", + "text": "Transporte de pasajeros sin habilitación" + }, + { + "id": "transporte_de_pasajeros_sin_habilitacion_o_autorizacion", + "text": "Transporte de pasajeros sin habilitación o autorización" + }, + { + "id": "usar_indebidamente_credencial", + "text": "Usar indebidamente credencial" + }, + { + "id": "usar_indebidamente_espacio_publico", + "text": "Usar indebidamente espacio público" + }, + { "id": "usar_indebidamente_armas", "text": "Usar indebidamente armas" }, + { + "id": "uso_de_documento_falso_o_adulterado", + "text": "Uso de documento falso o adulterado" + }, + { + "id": "uso_indebido_del_espacio_publico", + "text": "Uso indebido del espacio público" + }, + { + "id": "uso_o_exhibicion_de_franquicias", + "text": "Uso o exhibición de franquicias" + }, + { "id": "usurpacion", "text": "Usurpación" }, + { "id": "vehiculo_abandonado", "text": "Vehiculo abandonado" }, + { + "id": "vender_entradas_o_permitir_ingreso_exceso", + "text": "Vender entradas o permitir ingreso exceso" + }, + { + "id": "vender_sustancias_medicinales_sin_receta", + "text": "Vender sustancias medicinales sin receta" + }, + { + "id": "venta_o_consumo_de_bebidas_alcoholicas_fuera_del_horario", + "text": "Venta o consumo de bebidas alcohólicas fuera del horario" + }, + { "id": "verificacion_tecnica", "text": "Verificación técnica" }, + { "id": "violacion_de_peaje", "text": "Violación de peaje" }, + { "id": "violacion_de_domicilio", "text": "Violación de domicilio" }, + { + "id": "violacion_de_secretos_y_de_la_privacidad", + "text": "Violación de secretos y de la privacidad" + }, + { "id": "violar_clausura", "text": "Violar clausura" }, + { + "id": "violar_inhabilitacion_para_conducir", + "text": "Violar inhabilitación para conducir" + }, + { + "id": "violar_reglamentacion_juego", + "text": "Violar reglamentación juego" + }, + { + "id": "zanjas_y_pozos_en_via_publica", + "text": "Zanjas y pozos en via pública" + } + ], + "CONDUCTA_DESCRIPCION": [ + { "id": "agravada", "text": "Agravada" }, + { "id": "agravadas_por_edad", "text": "Agravadas por edad" }, + { + "id": "agravadas_por_el_uso_de_armas", + "text": "Agravadas por el uso de armas" + }, + { + "id": "agravadas_por_uso_de_armas", + "text": "Agravadas por uso de armas" + }, + { + "id": "agravadas_por_uso_de_armas_impropia", + "text": "Agravadas por uso de armas impropia" + }, + { "id": "agravado", "text": "Agravado" }, + { "id": "agravado_abuso_funcion", "text": "Agravado abuso función" }, + { "id": "agravado_alevosia", "text": "Agravado alevosía" }, + { + "id": "agravado_causar_sufrimiento_otra_persona", + "text": "Agravado causar sufrimiento otra persona" + }, + { "id": "agravado_concurso", "text": "Agravado concurso" }, + { "id": "agravado_familiar", "text": "Agravado familiar" }, + { + "id": "agravado_insertar_datos_en_datos_personales", + "text": "Agravado insertar datos en datos personales" + }, + { "id": "agravado_lugar_publico", "text": "Agravado lugar público" }, + { "id": "agravado_mas_personas", "text": "Agravado más personas" }, + { "id": "agravado_medio_idoneo", "text": "Agravado medio idóneo" }, + { "id": "agravado_menor_de_edad", "text": "Agravado menor de edad" }, + { "id": "agravado_miembro_fuerzas", "text": "Agravado miembro fuerzas" }, + { "id": "agravado_monumentos", "text": "Agravado monumentos" }, + { "id": "agravado_otro_delito", "text": "Agravado otro delito" }, + { + "id": "agravado_peligro_de_muerte", + "text": "Agravado peligro de muerte" + }, + { "id": "agravado_por_el_vinculo", "text": "Agravado por el vínculo" }, + { "id": "agravado_por_espacio", "text": "Agravado por espacio" }, + { + "id": "agravado_por_ser_fuerza_de_seguridad", + "text": "Agravado por ser fuerza de seguridad" + }, + { "id": "agravado_precio", "text": "Agravado precio" }, + { + "id": "agravado_relacion_de_pareja", + "text": "Agravado relación de pareja" + }, + { "id": "agravado_superior_militar", "text": "Agravado superior militar" }, + { + "id": "agravado_violar_informacion_banco_de_datos", + "text": "Agravado violar información banco de datos" + }, + { + "id": "agravado_violar_sistemas_seguridad", + "text": "Agravado violar sistemas seguridad" + }, + { + "id": "agravado_violencia_de_genero", + "text": "Agravado violencia de género" + }, + { "id": "agravado_violencia_mujer", "text": "Agravado violencia mujer" }, + { "id": "agravados", "text": "Agravados" }, + { "id": "coactivas", "text": "Coactivas" }, + { "id": "coactivas_agravadas", "text": "Coactivas agravadas" }, + { + "id": "coactivas_agravadas_por_uso_de_armas", + "text": "Coactivas agravadas por uso de armas" + }, + { + "id": "comercio_de_plantas_para_producir_estupefacientes", + "text": "Comercio de plantas para producir estupefacientes" + }, + { "id": "con_escalamiento", "text": "Con escalamiento" }, + { "id": "conduccion_imprudente", "text": "Conducción imprudente" }, + { "id": "culposas", "text": "Culposas" }, + { "id": "culposo", "text": "Culposo" }, + { "id": "de_fuego_uso_civil", "text": "De fuego uso civil" }, + { "id": "de_guerra", "text": "De guerra" }, + { + "id": "destinado_a_acreditar_identidad_de_personas_habilitacion_o_titularidad", + "text": "Destinado a acreditar identidad de personas habilitación o titularidad" + }, + { "id": "digital", "text": "Digital" }, + { "id": "digital_agravado_familiar", "text": "Digital agravado familiar" }, + { "id": "digital_agravado_jefe", "text": "Digital agravado jefe" }, + { + "id": "digital_agravado_mas_de_una_persona", + "text": "Digital agravado más de una persona" + }, + { + "id": "digital_agravado_menor_de_edad", + "text": "Digital agravado menor de edad" + }, + { + "id": "digital_agravado_relacion_de_pareja", + "text": "Digital agravado relación de pareja" + }, + { + "id": "durante_espectaculo_deportivo", + "text": "Durante espectáculo deportivo" + }, + { + "id": "en_grandes_parques_o_espectaculos_masivos", + "text": "En grandes parques o espectáculos masivos" + }, + { "id": "en_riña", "text": "En riña" }, + { + "id": "entrega_suministro_facilitacion", + "text": "Entrega suministro facilitación" + }, + { "id": "funcionario_publico", "text": "Funcionario público" }, + { "id": "ganzua_llave", "text": "Ganzúa llave" }, + { "id": "graves", "text": "Graves" }, + { "id": "graves_tentativa", "text": "Graves tentativa" }, + { "id": "informatica", "text": "Informática" }, + { + "id": "ingreso_a_domicilio_sin_autorizacion", + "text": "Ingreso a domicilio sin autorización" + }, + { + "id": "intimidacion_o_invocando_orden_superior", + "text": "Intimidación o invocando orden superior" + }, + { "id": "leves", "text": "Leves" }, + { "id": "leves_agravadas", "text": "Leves agravadas" }, + { "id": "leves_tentativa", "text": "Leves tentativa" }, + { "id": "muebles_transportables", "text": "Muebles transportables" }, + { "id": "no_denuncia_delito", "text": "No denuncia delito" }, + { "id": "no_corresponde", "text": "No corresponde" }, + { "id": "ocasion_de_incendio", "text": "Ocasión de incendio" }, + { "id": "organismo_publico", "text": "Organismo público" }, + { + "id": "para_produccion_o_tenencia_con_fines_de_comercializacion", + "text": "Para producción o tenencia con fines de comercialización" + }, + { "id": "por_despojo", "text": "Por despojo" }, + { + "id": "por_obligacion_de_devolver", + "text": "Por obligación de devolver" + }, + { + "id": "por_turbacion_de_la_posesion", + "text": "Por turbación de la posesión" + }, + { "id": "productos_separados_suelo", "text": "Productos separados suelo" }, + { "id": "publico_o_privado", "text": "Público o privado" }, + { "id": "seguido_de_muerte", "text": "Seguido de muerte" }, + { "id": "simple", "text": "Simple" }, + { "id": "simples", "text": "Simples" }, + { "id": "sin_autorizacion", "text": "Sin autorización" }, + { "id": "tarjeta_credito_debito", "text": "Tarjeta crédito débito" }, + { "id": "tenencia", "text": "Tenencia" }, + { "id": "tentativa", "text": "Tentativa" }, + { + "id": "tentativa_agravado_alevosia", + "text": "Tentativa agravado alevosía" + }, + { + "id": "transporte_de_pasajeros_sin_habilitacion_o_autorizacion", + "text": "Transporte de pasajeros sin habilitación o autorización" + }, + { "id": "uso_de_tarjeta_o_datos", "text": "Uso de tarjeta o datos" }, + { "id": "vehiculos", "text": "Vehículos" } + ], + "DETALLE": [ + { + "id": "audiencia_victima_ley_26484", + "text": "Audiencia víctima Ley 26484" + }, + { + "id": "audiencia_victima_11_bis_ley_24660", + "text": "Audiencia víctima 11 bis Ley 24660" + }, + { + "id": "audiencia_victima_ley_26485", + "text": "Audiencia victima Ley 26485" + }, + { + "id": "audiencia_acusado_ley_26485", + "text": "Audiencia acusado Ley 26485" + }, + { "id": "29_dias", "text": "29 días" }, + { "id": "30_dias", "text": "30 dias" }, + { "id": "44_dias", "text": "44 días" }, + { "id": "45_dias", "text": "45 dias" }, + { "id": "59_dias", "text": "59 días" }, + { "id": "60_dias", "text": "60 dias" }, + { "id": "89_dias", "text": "89 días" }, + { "id": "90_dias", "text": "90 dias" }, + { "id": "179_dias", "text": "179 días" }, + { "id": "180_dias", "text": "180 dias" }, + { "id": "abandono_vehiculo", "text": "Abandono vehículo" }, + { "id": "absolucion", "text": "Absolución" }, + { + "id": "absolucion_desistimiento_fiscal", + "text": "Absolución desistimiento fiscal" + }, + { "id": "accesorias_legales", "text": "Accesorias legales" }, + { + "id": "acepta_competencia_inhibitoria", + "text": "Acepta competencia inhibitoria" + }, + { "id": "acepta_cuestion_turno", "text": "Acepta cuestión turno" }, + { "id": "aclaratoria", "text": "Aclaratoria" }, + { "id": "acta_de_comprobacion", "text": "Acta de comprobación" }, + { "id": "acta_de_intimacion", "text": "Acta de intimación" }, + { + "id": "acuerdo_de_pago_levanta_embargo", + "text": "Acuerdo de pago levanta embargo" + }, + { + "id": "acumulacion_por_conexidad_y_ampliacion", + "text": "Acumulación por conexidad y ampliación" + }, + { "id": "admisibilidad", "text": "Admisibilidad" }, + { "id": "agente_encubierto", "text": "Agente encubierto" }, + { "id": "agente_revelador", "text": "Agente revelador" }, + { "id": "agotamiento_de_la_pena", "text": "Agotamiento de la pena" }, + { "id": "alimentos_provisorios", "text": "Alimentos provisorios" }, + { "id": "allanamiento", "text": "Allanamiento" }, + { "id": "apelacion", "text": "Apelación" }, + { + "id": "apertura_extraccion_examen_datos", + "text": "Apertura extracción examen datos" + }, + { "id": "aprehension", "text": "Aprehensión" }, + { "id": "arma", "text": "Arma" }, + { "id": "armas", "text": "Armas" }, + { + "id": "arresto_domiciliario_deja_sin_efecto", + "text": "Arresto domiciliario deja sin efecto" + }, + { "id": "asesor_tutelar", "text": "Asesor tutelar" }, + { "id": "atipicidad_sobreseimiento", "text": "Atipicidad sobreseimiento" }, + { + "id": "audiencia_acusado_ley_26484", + "text": "Audiencia acusado Ley 26484" + }, + { + "id": "audiencia_acusado_difiere_sentencia", + "text": "Audiencia acusado difiere sentencia" + }, + { "id": "audiencia_preliminar", "text": "Audiencia preliminar" }, + { + "id": "audiencia_victima_10_bis_ley_24660", + "text": "Audiencia víctima 10 bis Ley 24660" + }, + { "id": "bloqueo_acceso_al_dominio", "text": "Bloqueo acceso al dominio" }, + { + "id": "cadena_de_custodia_de_elementos_secuestrados", + "text": "Cadena de custodia de elementos secuestrados" + }, + { "id": "camara_gesell", "text": "Cámara Gesell" }, + { + "id": "cambio_domicilio_prision_domiciliaria", + "text": "Cambio domicilio prisión domiciliaria" + }, + { "id": "captura", "text": "Captura" }, + { "id": "captura_deja_sin_efecto", "text": "Captura deja sin efecto" }, + { "id": "caucion_real", "text": "Caución real" }, + { "id": "cedula_de_notificacion", "text": "Cédula de notificación" }, + { + "id": "cese_de_actos_de_perturbacion_o_intimidacion", + "text": "Cese de actos de perturbación o intimidación" + }, + { "id": "cese_de_medidas", "text": "Cese de medidas" }, + { "id": "cese_de_medidas_parcial", "text": "Cese de medidas parcial" }, + { "id": "cierre_de_instancia", "text": "Cierre de instancia" }, + { "id": "clausura", "text": "Clausura" }, + { "id": "clausura_deja_sin_efecto", "text": "Clausura deja sin efecto" }, + { "id": "competencia_penal_juvenil", "text": "Competencia penal juvenil" }, + { "id": "concede", "text": "Concede" }, + { "id": "concede_parcialmente", "text": "Concede parcialmente" }, + { "id": "conocimiento_personal", "text": "Conocimiento personal" }, + { "id": "consigna_policial", "text": "Consigna policial" }, + { "id": "contienda_negativa", "text": "Contienda negativa" }, + { "id": "control", "text": "Control" }, + { "id": "convalida_fallecimiento", "text": "Convalida fallecimiento" }, + { "id": "convalida_inimputabilidad", "text": "Convalida inimputabilidad" }, + { + "id": "convalidacion_otras_causales", + "text": "Convalidación otras causales" + }, + { "id": "copia_forense_celular", "text": "Copia forense celular" }, + { + "id": "copias_de_capturas_de_pantalla_mensajes_y_llamados", + "text": "Copias de capturas de pantalla mensajes y llamados" + }, + { + "id": "cosa_juzgada_sobreseimiento", + "text": "Cosa juzgada sobreseimiento" + }, + { "id": "cuarto_intermedio", "text": "Cuarto intermedio" }, + { + "id": "cuarto_intermedio_para_resolver", + "text": "Cuarto intermedio para resolver" + }, + { + "id": "cumplimiento_de_regla_de_conducta_en_complejo_penitenciario", + "text": "Cumplimiento de regla de conducta en complejo penitenciario" + }, + { + "id": "cumplimiento_de_reglas_de_conducta", + "text": "Cumplimiento de reglas de conducta" + }, + { "id": "declaracion_testimonial", "text": "Declaracion testimonial" }, + { "id": "declina_competencia", "text": "Declina competencia" }, + { "id": "defensa", "text": "Defensa" }, + { "id": "defensa_particular", "text": "Defensa particular" }, + { "id": "defensa_querella", "text": "Defensa querella" }, + { + "id": "deja_sin_efecto_inhabilitacion_y_devuelve_licencia", + "text": "Deja sin efecto inhabilitación y devuelve licencia" + }, + { + "id": "deja_sin_efecto_revocacion", + "text": "Deja sin efecto revocación" + }, + { "id": "delitos_de_accion_publica", "text": "Delitos de acción publica" }, + { + "id": "desistimiento_accion_instancia_privada_sobreseimiento", + "text": "Desistimiento accion instancia privada sobreseimiento" + }, + { "id": "desistimiento_fiscal", "text": "Desistimiento fiscal" }, + { "id": "destruccion_de_arma", "text": "Destrucción de arma" }, + { "id": "detencion", "text": "Detención" }, + { + "id": "detencion_captura_y/o_traslado", + "text": "Detención captura y/o traslado" + }, + { + "id": "detencion_captura_y/o_traslado_deja_sin_efecto", + "text": "Detención captura y/o traslado deja sin efecto" + }, + { + "id": "detencion_secuestro_y_requisa", + "text": "Detención secuestro y requisa" + }, + { + "id": "devolucion_acarreo_y_estadia", + "text": "Devolución acarreo y estadía" + }, + { "id": "dictamen_fiscal", "text": "Dictamen fiscal" }, + { "id": "difiere_decision", "text": "Difiere decisión" }, + { "id": "difiere_fundamentos", "text": "Difiere fundamentos" }, + { "id": "difiere_resolucion", "text": "Difiere resolución" }, + { + "id": "dispositivo_de_geoposicionamiento_mantiene_hasta_juicio", + "text": "Dispositivo de geoposicionamiento mantiene hasta juicio" + }, + { + "id": "domiciliaria_con_vigilancia_electronica", + "text": "Domiciliaria con vigilancia electrónica" + }, + { "id": "egreso_del_pais", "text": "Egreso del país" }, + { "id": "ejecucion_de_honorarios", "text": "Ejecución de honorarios" }, + { "id": "embargo", "text": "Embargo" }, + { "id": "embargo_deja_sin_efecto", "text": "Embargo deja sin efecto" }, + { "id": "estimulo_educativo", "text": "Estímulo educativo" }, + { "id": "estupefacientes", "text": "Estupefacientes" }, + { "id": "estupefacientes_dinero", "text": "Estupefacientes dinero" }, + { + "id": "estupefacientes_telefono_dinero", + "text": "Estupefacientes teléfono dinero" + }, + { + "id": "estupefacientes_telefono_dinero_arma", + "text": "Estupefacientes teléfono dinero arma" + }, + { + "id": "estupefacientes_telefono_vehiculo", + "text": "Estupefacientes teléfono vehículo" + }, + { + "id": "estupefacientes_vehiculo_dinero", + "text": "Estupefacientes vehículo dinero" + }, + { "id": "excarcelacion", "text": "Excarcelación" }, + { "id": "exclusion_del_hogar", "text": "Exclusión del hogar" }, + { "id": "exhorto", "text": "Exhorto" }, + { "id": "exhorto_diplomatico", "text": "Exhorto diplomático" }, + { "id": "exime_pago_multa", "text": "Exime pago multa" }, + { + "id": "exime_reparacion_del_daño_y_pago_multa", + "text": "Exime reparación del daño y pago multa" + }, + { "id": "eximicion_pago", "text": "Eximición pago" }, + { + "id": "eximicion_pauta_de_conducta", + "text": "Eximición pauta de conducta" + }, + { + "id": "expulsion_art_63_a_ley_25871", + "text": "Expulsión art. 63 a Ley 25871" + }, + { + "id": "expulsion_art_64_a_ley_25871", + "text": "Expulsion art. 64 a Ley 25871" + }, + { + "id": "extincion_accion_por_autocomposicion", + "text": "Extinción acción por autocomposición" + }, + { + "id": "extincion_accion_por_cumplimiento", + "text": "Extinción acción por cumplimiento" + }, + { + "id": "extincion_accion_sobreseimiento", + "text": "Extinción acción sobreseimiento" + }, + { + "id": "extincion_accion_sobreseimiento_por_fallecimiento", + "text": "Extinción acción sobreseimiento por fallecimiento" + }, + { "id": "extincion_de_la_pena", "text": "Extinción de la pena" }, + { + "id": "extincion_sancion_por_cumplimiento", + "text": "Extinción sanción por cumplimiento" + }, + { "id": "extrañamiento", "text": "Extrañamiento" }, + { + "id": "falta_de_accion_sobreseimiento", + "text": "Falta de acción sobreseimiento" + }, + { + "id": "falta_de_legitimacion_pasiva", + "text": "Falta de legitimación pasiva" + }, + { "id": "falta_de_participacion", "text": "Falta de participación" }, + { "id": "falta_de_presentacion", "text": "Falta de presentación" }, + { + "id": "falta_de_requisitos_procedencia", + "text": "Falta de requisitos procedencia" + }, + { "id": "fija_nueva_audiencia", "text": "Fija nueva audiencia" }, + { "id": "fondo_de_reserva", "text": "Fondo de reserva" }, + { "id": "habeas_corpus", "text": "Habeas corpus" }, + { + "id": "hasta_agotar_investigacion", + "text": "Hasta agotar investigación" + }, + { "id": "identidad_objeto_y_sujeto", "text": "Identidad objeto y sujeto" }, + { "id": "imagenes_videos", "text": "Imagenes videos" }, + { "id": "incompetencia_material", "text": "Incompetencia material" }, + { + "id": "incompetencia_por_conexidad", + "text": "Incompetencia por conexidad" + }, + { + "id": "incompetencia_por_conexidad_subjetiva", + "text": "Incompetencia por conexidad subjetiva" + }, + { "id": "incompetencia_territorial", "text": "Incompetencia territorial" }, + { + "id": "indeterminacion_imputacion", + "text": "Indeterminacion imputación" + }, + { + "id": "informacion_y_apertura_de_antenas_llamadas", + "text": "Información y apertura de antenas llamadas" + }, + { "id": "informe_cij", "text": "Informe cij" }, + { "id": "informe_mercado_libre", "text": "Informe mercado libre" }, + { "id": "informe_ncmec", "text": "Informe ncmec" }, + { "id": "informe_previo", "text": "Informe previo" }, + { + "id": "informes_llamadas_y_deteccion_de_celdas", + "text": "Informes llamadas y detección de celdas" + }, + { + "id": "inhabilitacion_para_conducir", + "text": "Inhabilitación para conducir" + }, + { + "id": "inhabilitacion_para_conducir_deja_sin_efecto", + "text": "Inhabilitación para conducir deja sin efecto" + }, + { "id": "inmovilizacion", "text": "Inmovilización" }, + { "id": "interes_en_el_caso", "text": "Interés en el caso" }, + { "id": "internacion_involuntaria", "text": "Internación involuntaria" }, + { "id": "interprete", "text": "Intérprete" }, + { "id": "interprete_y_defensa", "text": "Intérprete y defensa" }, + { "id": "interrogatorio_policial", "text": "Interrogatorio policial" }, + { + "id": "intervencion_equipo_especializado", + "text": "Intervención equipo especializado" + }, + { "id": "intervencion_previa", "text": "Intervención previa" }, + { "id": "intervencion_red_social", "text": "Intervención red social" }, + { "id": "intervencion_telefonica", "text": "Intervención telefónica" }, + { + "id": "intervencion_telefonica_con_ubicacion_de_celda_de_apertura", + "text": "Intervención telefónica con ubicación de celda de apertura" + }, + { + "id": "levantamiento_clausura_administrativa", + "text": "Levantamiento clausura administrativa" + }, + { + "id": "levantamiento_secreto_bancario", + "text": "Levantamiento secreto bancario" + }, + { + "id": "levantamiento_secreto_fiscal", + "text": "Levantamiento secreto fiscal" + }, + { "id": "liberacion_aves", "text": "Liberación aves" }, + { + "id": "libertad_asistida_incorporacion", + "text": "Libertad asistida incorporación" + }, + { + "id": "libertad_condicional_otorga", + "text": "Libertad condicional otorga" + }, + { + "id": "libertad_condicional_revoca", + "text": "Libertad condicional revoca" + }, + { "id": "litispendencia", "text": "Litispendencia" }, + { "id": "mantiene", "text": "Mantiene" }, + { "id": "mediacion", "text": "Mediación" }, + { "id": "medidas_restrictivas", "text": "Medidas restrictivas" }, + { + "id": "modifica_pauta_de_conducta", + "text": "Modifica pauta de conducta" + }, + { "id": "morigeracion_domiciliaria", "text": "Morigeración domiciliaria" }, + { "id": "no_convalida", "text": "No convalida" }, + { "id": "no_homologa_absolucion", "text": "No homologa absolución" }, + { "id": "notificacion_via_mail", "text": "Notificacion vía mail" }, + { + "id": "nulidad_incorporacion_prueba", + "text": "Nulidad incorporación prueba" + }, + { "id": "otorga_plazo", "text": "Otorga plazo" }, + { "id": "otras_vias_idoneas", "text": "Otras vías idóneas" }, + { "id": "pago_de_multa_en_cuotas", "text": "Pago de multa en cuotas" }, + { + "id": "pago_minimo_multa_sobreseimiento", + "text": "Pago mínimo multa sobreseimiento" + }, + { + "id": "pago_minimo_multa_y_reparacion_daño_63cp", + "text": "Pago mínimo multa y reparación daño 63 CP" + }, + { + "id": "pago_minimo_multa_y_reparacion_daño_64cp", + "text": "Pago minimo multa y reparacion daño 64 CP" + }, + { "id": "paradero", "text": "Paradero" }, + { "id": "paradero_deja_sin_efecto", "text": "Paradero deja sin efecto" }, + { + "id": "parcial_requerimiento_de_juicio", + "text": "Parcial requerimiento de juicio" + }, + { + "id": "parcial_requerimiento_de_juicio_falta_de_circunscripcion_temporal", + "text": "Parcial requerimiento de juicio falta de circunscripción temporal" + }, + { + "id": "parcial_requerimiento_de_juicio_falta_de_fundamentacion", + "text": "Parcial requerimiento de juicio falta de fundamentación" + }, + { + "id": "parcial_requerimiento_de_juicio_por_introduccion_de_agravante", + "text": "Parcial requerimiento de juicio por introducción de agravante" + }, + { "id": "pedido_de_informe_banco", "text": "Pedido de informe banco" }, + { + "id": "pedido_de_informe_facebook", + "text": "Pedido de informe Facebook" + }, + { + "id": "pedido_de_informe_facebook_google", + "text": "Pedido de informe Facebook Google" + }, + { + "id": "pedido_de_informe_facebook_microsoft", + "text": "Pedido de informe Facebook Microsoft" + }, + { "id": "pedido_de_informe_google", "text": "Pedido de informe Google" }, + { + "id": "pedido_de_informe_google_microsoft", + "text": "Pedido de informe Google Microsoft" + }, + { + "id": "pedido_de_informe_imgur_llc", + "text": "Pedido de informe Imgur LLC" + }, + { + "id": "pedido_de_informe_instagram", + "text": "Pedido de informe Instagram" + }, + { + "id": "pedido_de_informe_mercado_libre", + "text": "Pedido de informe Mercado Libre" + }, + { + "id": "pedido_de_informe_microsoft", + "text": "Pedido de informe Microsoft" + }, + { "id": "pedido_de_informe_skype", "text": "Pedido de informe Skype" }, + { + "id": "pedido_de_informe_telefonia", + "text": "Pedido de informe telefonía" + }, + { + "id": "pedido_de_informe_whatsapp", + "text": "Pedido de informe Whatsapp" + }, + { "id": "pedido_informe_tiktok", "text": "Pedido informe Tiktok" }, + + { + "id": "pedido_prision_domiciliaria", + "text": "Pedido prisión domiciliaria" + }, + { + "id": "pena_de_arresto_efectivo_cumplimiento", + "text": "Pena de arresto efectivo cumplimiento" + }, + { + "id": "pena_de_arresto_efectivo_cumplimiento_compurgada", + "text": "Pena de arresto efectivo cumplimiento compurgada" + }, + { + "id": "pena_de_arresto_en_suspenso", + "text": "Pena de arresto en suspenso" + }, + { + "id": "pena_de_inhabilitacion_especial", + "text": "Pena de inhabilitación especial" + }, + { + "id": "pena_de_multa_efectivo_cumplimiento", + "text": "Pena de multa efectivo cumplimiento" + }, + { "id": "pena_de_multa_en_suspenso", "text": "Pena de multa en suspenso" }, + { + "id": "pena_de_multa_sustituida_por_amonestacion", + "text": "Pena de multa sustituida por amonestación" + }, + { + "id": "pena_de_multa_sustituida_por_clausura_se_tiene_por_cumplida", + "text": "Pena de multa sustituida por clausura se tiene por cumplida" + }, + { + "id": "pena_de_multa_sustituida_por_trabajos_comunitarios", + "text": "Pena de multa sustituida por trabajos comunitarios" + }, + { + "id": "pena_de_prision_domiciliaria", + "text": "Pena de prisión domiciliaria" + }, + { + "id": "pena_de_prision_efectivo_cumplimiento", + "text": "Pena de prisión efectivo cumplimiento" + }, + { + "id": "pena_de_prision_efectivo_cumplimiento_compurgada", + "text": "Pena de prisión efectivo cumplimiento compurgada" + }, + { + "id": "pena_de_prision_efectivo_cumplimiento_domiciliaria", + "text": "Pena de prisión efectivo cumplimiento domiciliaria" + }, + { + "id": "pena_de_prision_en_suspenso", + "text": "Pena de prisión en suspenso" + }, + { + "id": "pena_de_tareas_comunitarias", + "text": "Pena de tareas comunitarias" + }, + { + "id": "pena_de_tareas_de_utilidad_publica", + "text": "Pena de tareas de utilidad pública" + }, + { "id": "pericia", "text": "Pericia" }, + { "id": "pericia_arma", "text": "Pericia arma" }, + { + "id": "pericia_examen_veterinario", + "text": "Pericia examen veterinario" + }, + { "id": "peritaje_de_telefono", "text": "Peritaje de teléfono" }, + { + "id": "peritaje_ordenadores_y_o_telefonos", + "text": "Peritaje ordenadores y o teléfonos" + }, + { "id": "peritaje_telefono", "text": "Peritaje teléfono" }, + { "id": "perito", "text": "Perito" }, + { "id": "permiso_de_viaje", "text": "Permiso de viaje" }, + { + "id": "plazo_razonable_sobreseimiento", + "text": "Plazo razonable sobreseimiento" + }, + { + "id": "plazo_razonable_sobresimiento", + "text": "Plazo razonable sobresimiento" + }, + { "id": "pleito_con_una_parte", "text": "Pleito con una parte" }, + { + "id": "por_quince_dias_hasta_tomar_contacto_con_victima", + "text": "Por quince dias hasta tomar contacto con víctima" + }, + { + "id": "prescripcion_sobreseimiento", + "text": "Prescripción sobreseimiento" + }, + { + "id": "prision_domiciliaria_mantiene", + "text": "Prisión domiciliaria mantiene" + }, + { + "id": "prision_domiciliaria_prorroga", + "text": "Prisión domiciliaria prórroga" + }, + { "id": "procedimiento", "text": "Procedimiento" }, + { + "id": "procedimiento_administrativo_pendiente", + "text": "Procedimiento administrativo pendiente" + }, + { + "id": "prohibicion_compra_tenencia_registracion_de_armas", + "text": "Prohibición compra tenencia registración de armas" + }, + { + "id": "prohibicion_de_acercamiento", + "text": "Prohibición de acercamiento" + }, + { + "id": "prohibicion_de_acercamiento_y_contacto", + "text": "Prohibición de acercamiento y contacto" + }, + { + "id": "prohibicion_de_publicar_en_redes", + "text": "Prohibición de publicar en redes" + }, + { + "id": "prohibicion_de_publicar_en_redes_sobre", + "text": "Prohibición de publicar en redes sobre" + }, + { + "id": "promocion_a_periodo_de_prueba_art_6_ley_24660", + "text": "Promoción a período de prueba art 6 Ley 24660" + }, + { + "id": "promocion_a_periodo_de_prueba_art_7_ley_24660", + "text": "Promoción a período de prueba art 7 Ley 24660" + }, + { "id": "prorroga", "text": "Prórroga" }, + { "id": "prueba_ofrecida", "text": "Prueba ofrecida" }, + { "id": "publicacion_de_edictos", "text": "Publicación de edictos" }, + { "id": "queja", "text": "Queja" }, + { "id": "querella", "text": "Querella" }, + { + "id": "quita_puntos_de_licencia_de_conducir", + "text": "Quita puntos de licencia de conducir" + }, + { "id": "realizacion", "text": "Realización" }, + { "id": "rebeldia", "text": "Rebeldía" }, + { "id": "rebeldia_deja_sin_efecto", "text": "Rebeldía deja sin efecto" }, + { "id": "rebeldia_y_captura", "text": "Rebeldía y captura" }, + { "id": "rebeldia_y_paradero", "text": "Rebeldía y paradero" }, + { "id": "rechaza_acuerdo", "text": "Rechaza acuerdo" }, + { "id": "rechaza_competencia", "text": "Rechaza competencia" }, + { + "id": "regimen_de_comunicacion_provisoria", + "text": "Regimen de comunicación provisoria" + }, + { "id": "reincidencia", "text": "Reincidencia" }, + { + "id": "reintegra_puntos_de_licencia_de_conducir", + "text": "Reintegra puntos de licencia de conducir" + }, + { "id": "remate_vehiculo", "text": "Remate vehículo" }, + { "id": "remision_a_justicia_civil", "text": "Remisión a justicia civil" }, + { + "id": "remision_camara_para_sorteo", + "text": "Remisión cámara para sorteo" + }, + { + "id": "reparacion_integral_del_daño_sobreseimiento", + "text": "Reparación integral del daño sobreseimiento" + }, + { + "id": "replica_de_arma_de_plastico", + "text": "Réplica de arma de plástico" + }, + { "id": "reposicion", "text": "Reposición" }, + { + "id": "reposicion_apelacion_subsidio", + "text": "Reposición apelación subsidio" + }, + { "id": "reprogramacion", "text": "Reprogramación" }, + { "id": "requerido_por_infractor", "text": "Requerido por infractor" }, + { "id": "requerimiento_de_juicio", "text": "Requerimiento de juicio" }, + { + "id": "requerimiento_de_juicio_falta_de_fundamentacion", + "text": "Requerimiento de juicio falta de fundamentación" + }, + { "id": "requisa", "text": "Requisa" }, + { "id": "restitucion_de_inmueble", "text": "Restitución de inmueble" }, + { + "id": "revision_psicofisica_capacidad", + "text": "Revisión psicofísica capacidad" + }, + { + "id": "revision_psiquiatrica_y_psicologica", + "text": "Revision psiquiátrica y psicológica" + }, + { "id": "revoca", "text": "Revoca" }, + { "id": "revoca_condicionalidad", "text": "Revoca condicionalidad" }, + { "id": "revocacion", "text": "Revocación" }, + { "id": "rueda_de_reconocimiento", "text": "Rueda de reconocimiento" }, + { "id": "salidas_extraordinarias", "text": "Salidas extraordinarias" }, + { "id": "salidas_transitorias", "text": "Salidas transitorias" }, + { + "id": "sancion_sustitutiva_prorroga", + "text": "Sanción sustitutiva prórroga" + }, + { "id": "secuestro", "text": "Secuestro" }, + { "id": "sin_indicacion_plazo", "text": "Sin indicación plazo" }, + { "id": "sobreseimiento", "text": "Sobreseimiento" }, + { "id": "solicita", "text": "Solicita" }, + { + "id": "solicitud_destruccion_de_estupefacientes", + "text": "Solicitud destrucción de estupefacientes" + }, + { "id": "tareas_de_investigacion", "text": "Tareas de investigación" }, + { "id": "telefono", "text": "Teléfono" }, + { + "id": "temor_parcialidad_generico", + "text": "Temor parcialidad genérico" + }, + { "id": "tiene_por_desistido", "text": "Tiene por desistido" }, + { + "id": "tiene_por_no_computado_plazo", + "text": "Tiene por no computado plazo" + }, + { "id": "tiene_presente", "text": "Tiene presente" }, + { "id": "tiene_presente_acuerdo", "text": "Tiene presente acuerdo" }, + { "id": "tobillera_refractaria", "text": "Tobillera refractaria" }, + { + "id": "traslado_por_fuerza_publica", + "text": "Traslado por fuerza pública" + }, + { + "id": "unifica_solicita_inhibitoria", + "text": "Unifica solicita inhibitoria" + }, + { "id": "unificacion_de_pena", "text": "Unificación de pena" }, + { "id": "vehiculo", "text": "Vehículo" }, + { "id": "vencimiento_plazo_ipp", "text": "Vencimiento plazo IPP" } + ], + "OBJETO_DE_LA_RESOLUCION": [ + { "id": "acumulacion_de_caso", "text": "Acumulación de caso" }, + { "id": "admisibilidad_amparo", "text": "Admisibilidad amparo" }, + { + "id": "admisibilidad_habeas_corpus", + "text": "Admisibilidad habeas corpus" + }, + { "id": "admisibilidad_prueba", "text": "Admisibilidad prueba" }, + { "id": "apartamiento", "text": "Apartamiento" }, + { "id": "archivo_fiscal", "text": "Archivo fiscal" }, + { "id": "audiencia_de_conciliacion", "text": "Audiencia de conciliación" }, + { + "id": "beneficio_litigar_sin_gastos", + "text": "Beneficio litigar sin gastos" + }, + { "id": "caducidad_de_instancia", "text": "Caducidad de instancia" }, + { "id": "cuestion_de_competencia", "text": "Cuestión de competencia" }, + { "id": "decomiso", "text": "Decomiso" }, + { "id": "desistimiento_faltas", "text": "Desistimiento faltas" }, + { "id": "ejecucion_de_la_pena", "text": "Ejecución de la pena" }, + { "id": "excepcion", "text": "Excepción" }, + { "id": "excusacion", "text": "Excusación" }, + { "id": "extraccion_testimonios", "text": "Extracción testimonios" }, + { "id": "juicio_abreviado", "text": "Juicio abreviado" }, + { "id": "juicio_oral", "text": "Juicio oral" }, + { "id": "juzgamiento_faltas", "text": "Juzgamiento faltas" }, + { "id": "mediacion", "text": "Mediación" }, + { "id": "medida_cautelar", "text": "Medida cautelar" }, + { "id": "medida_probatoria", "text": "Medida probatoria" }, + { "id": "mesa_de_dialogo", "text": "Mesa de diálogo" }, + { "id": "nulidad", "text": "Nulidad" }, + { "id": "prision_preventiva", "text": "Prisión preventiva" }, + { + "id": "prorroga_investigacion_penal_preparatoria", + "text": "Prórroga investigación penal preparatoria" + }, + { "id": "recurso", "text": "Recurso" }, + { "id": "recusacion", "text": "Recusación" }, + { "id": "regulacion_de_honorarios", "text": "Regulación de honorarios" }, + { "id": "remision_controladora", "text": "Remisión controladora" }, + { + "id": "restablecimiento_de_contacto", + "text": "Restablecimiento de contacto" + }, + { "id": "restitucion_de_efectos", "text": "Restitución de efectos" }, + { "id": "restitucion_de_inmueble", "text": "Restitución de inmueble" }, + { "id": "restitucion_efectos", "text": "Restitución efectos" }, + { + "id": "suspension_del_proceso_a_prueba", + "text": "Suspensión del proceso a prueba" + } + ], + "ART_INFRINGIDO": [ + { "id": "00", "text": "00" }, + { "id": "1", "text": "1" }, + { "id": "1.1.6", "text": "1.1.6" }, + { "id": "1.3.20", "text": "1.3.20" }, + { "id": "1.3.22", "text": "1.3.22" }, + { "id": "1.3.3", "text": "1.3.3" }, + { "id": "1.3.31", "text": "1.3.31" }, + { "id": "1.3.6.1", "text": "1.3.6.1" }, + { "id": "2.1.1", "text": "2.1.1" }, + { "id": "2.1.13", "text": "2.1.13" }, + { "id": "2.1.15", "text": "2.1.15" }, + { "id": "2.1.16", "text": "2.1.16" }, + { "id": "2.1.17", "text": "2.1.17" }, + { "id": "2.1.19", "text": "2.1.19" }, + { "id": "2.1.21", "text": "2.1.21" }, + { "id": "2.1.3", "text": "2.1.3" }, + { "id": "2.2.1", "text": "2.2.1" }, + { "id": "2.2.14", "text": "2.2.14" }, + { "id": "3", "text": "3" }, + { "id": "3.1.1", "text": "3.1.1" }, + { "id": "4.1.1", "text": "4.1.1" }, + { "id": "4.1.1.2", "text": "4.1.1.2" }, + { "id": "4.1.17", "text": "4.1.17" }, + { "id": "4.1.22", "text": "4.1.22" }, + { "id": "4.1.24", "text": "4.1.24" }, + { "id": "4.1.7", "text": "4.1.7" }, + { "id": "5c", "text": "5c" }, + { "id": "5d", "text": "5d" }, + { "id": "5e", "text": "5e" }, + { "id": "6", "text": "6" }, + { "id": "6.1.12", "text": "6.1.12" }, + { "id": "6.1.12.1", "text": "6.1.12.1" }, + { "id": "6.1.2", "text": "6.1.2" }, + { "id": "6.1.24", "text": "6.1.24" }, + { "id": "6.1.26", "text": "6.1.26" }, + { "id": "6.1.28", "text": "6.1.28" }, + { "id": "6.1.31", "text": "6.1.31" }, + { "id": "6.1.37", "text": "6.1.37" }, + { "id": "6.1.4", "text": "6.1.4" }, + { "id": "6.1.40", "text": "6.1.40" }, + { "id": "6.1.41", "text": "6.1.41" }, + { "id": "6.1.42", "text": "6.1.42" }, + { "id": "6.1.44", "text": "6.1.44" }, + { "id": "6.1.47", "text": "6.1.47" }, + { "id": "6.1.49", "text": "6.1.49" }, + { "id": "6.1.52", "text": "6.1.52" }, + { "id": "6.1.54", "text": "6.1.54" }, + { "id": "6.1.63", "text": "6.1.63" }, + { "id": "6.1.67", "text": "6.1.67" }, + { "id": "6.1.8", "text": "6.1.8" }, + { "id": "6.1.9", "text": "6.1.9" }, + { "id": "6.1.94", "text": "6.1.94" }, + { "id": "9.1.1", "text": "9.1.1" }, + { "id": "10.1.5", "text": "10.1.5" }, + { "id": "11.1.10", "text": "11.1.10" }, + { "id": "11.1.2", "text": "11.1.2" }, + { "id": "11.1.4", "text": "11.1.4" }, + { "id": "13", "text": "13" }, + { "id": "14", "text": "14" }, + { "id": "17", "text": "17" }, + { "id": "23", "text": "23" }, + { "id": "51", "text": "51" }, + { "id": "52", "text": "52" }, + { "id": "53_bis", "text": "53 bis" }, + { "id": "53_bis_inc3", "text": "53 bis inc. 3" }, + { "id": "53_bis_inc5", "text": "53 bis inc. 5" }, + { "id": "54", "text": "54" }, + { "id": "56", "text": "56" }, + { "id": "57", "text": "57" }, + { "id": "58", "text": "58" }, + { "id": "59", "text": "59" }, + { "id": "67_bis", "text": "67 bis" }, + { "id": "71_quinquies", "text": "71 quinquies" }, + { "id": "71_ter", "text": "71 ter" }, + { "id": "72", "text": "72" }, + { "id": "73", "text": "73" }, + { "id": "74", "text": "74" }, + { "id": "75", "text": "75" }, + { "id": "76", "text": "76" }, + { "id": "77", "text": "77" }, + { "id": "79", "text": "79" }, + { "id": "80", "text": "80" }, + { "id": "80_inc11", "text": "80 inc. 11" }, + { "id": "80_inc2", "text": "80 inc. 2" }, + { "id": "81", "text": "81" }, + { "id": "82", "text": "82" }, + { "id": "83", "text": "83" }, + { "id": "84", "text": "84" }, + { "id": "85", "text": "85" }, + { "id": "87", "text": "87" }, + { "id": "88", "text": "88" }, + { "id": "89", "text": "89" }, + { "id": "90", "text": "90" }, + { "id": "92", "text": "92" }, + { "id": "93", "text": "93" }, + { "id": "94", "text": "94" }, + { "id": "94_bis", "text": "94 bis" }, + { "id": "95", "text": "95" }, + { "id": "96", "text": "96" }, + { "id": "97", "text": "97" }, + { "id": "100", "text": "100" }, + { "id": "101", "text": "101" }, + { "id": "104", "text": "104" }, + { "id": "105", "text": "105" }, + { "id": "106", "text": "106" }, + { "id": "107", "text": "107" }, + { "id": "109", "text": "109" }, + { "id": "111", "text": "111" }, + { "id": "113", "text": "113" }, + { "id": "116", "text": "116" }, + { "id": "118", "text": "118" }, + { "id": "119", "text": "119" }, + { "id": "120", "text": "120" }, + { "id": "122", "text": "122" }, + { "id": "125_bis", "text": "125 bis" }, + { "id": "128", "text": "128" }, + { "id": "129", "text": "129" }, + { "id": "129_parr_2", "text": "129 párr. 2" }, + { "id": "131", "text": "131" }, + { "id": "135", "text": "135" }, + { "id": "141", "text": "141" }, + { "id": "148_bis", "text": "148 bis" }, + { "id": "149_bis", "text": "149 bis" }, + { "id": "149_bis_parr_2", "text": "149 bis párr. 2" }, + { "id": "149ter_inc2b", "text": "149 ter inc. 2b)" }, + { "id": "150", "text": "150" }, + { "id": "153", "text": "153" }, + { "id": "153_bis", "text": "153 bis" }, + { "id": "153_bis_parr_2", "text": "153 bis párr. 2" }, + { "id": "157_bis", "text": "157 bis" }, + { "id": "162", "text": "162" }, + { "id": "163", "text": "163" }, + { "id": "163_inc4", "text": "163 inc. 4" }, + { "id": "163_bis", "text": "163 bis" }, + { "id": "164", "text": "164" }, + { "id": "166", "text": "166" }, + { "id": "168", "text": "168" }, + { "id": "172", "text": "172" }, + { "id": "173_inc15", "text": "173 inc. 15" }, + { "id": "173_inc16", "text": "173 inc. 16" }, + { "id": "173_inc2", "text": "173 inc. 2" }, + { "id": "181_inc1", "text": "181 inc. 1" }, + { "id": "181_inc3", "text": "181 inc. 3" }, + { "id": "183", "text": "183" }, + { "id": "183_parr_2", "text": "183 párr. 2" }, + { "id": "184_inc5", "text": "184 inc. 5" }, + { "id": "186", "text": "186" }, + { "id": "186_inc1", "text": "186 inc. 1" }, + { "id": "186_inc4", "text": "186 inc. 4" }, + { "id": "186_inc5", "text": "186 inc. 5" }, + { "id": "189_bis", "text": "189 bis" }, + { "id": "189_bis_inc4", "text": "189 bis inc. 4" }, + { "id": "193_bis", "text": "193 bis" }, + { "id": "204_quinquies", "text": "204 quinquies" }, + { "id": "205", "text": "205" }, + { "id": "208", "text": "208" }, + { "id": "210", "text": "210" }, + { "id": "237", "text": "237" }, + { "id": "238", "text": "238" }, + { "id": "239", "text": "239" }, + { "id": "243", "text": "243" }, + { "id": "245", "text": "245" }, + { "id": "248", "text": "248" }, + { "id": "249", "text": "249" }, + { "id": "258", "text": "258" }, + { "id": "266", "text": "266" }, + { "id": "267", "text": "267" }, + { "id": "273", "text": "273" }, + { "id": "277_inc_d", "text": "277 inc. d" }, + { "id": "281", "text": "281" }, + { "id": "289_inc1", "text": "289 inc. 1" }, + { "id": "289_inc3", "text": "289 inc. 3" }, + { "id": "292_parr_1", "text": "292 párr. 1" }, + { "id": "292_parr_2", "text": "292 párr. 2" }, + { "id": "295", "text": "295" }, + { "id": "296", "text": "296" }, + { "id": "301_bis", "text": "301 bis" } + ], + "CODIGO_O_LEY": [ + { "id": "codigo_contravencional", "text": "Código contravencional" }, + { "id": "codigo_penal_de_la_nacion", "text": "Código penal de la Nación" }, + { "id": "ley_1217", "text": "Ley 1217" }, + { "id": "ley_12331", "text": "Ley 12331" }, + { "id": "ley_13944", "text": "Ley 13944" }, + { "id": "ley_14346", "text": "Ley 14346" }, + { "id": "ley_23098", "text": "Ley 23098" }, + { "id": "ley_23592", "text": "Ley 23592" }, + { "id": "ley_23737", "text": "Ley 23737" }, + { "id": "ley_24192", "text": "Ley 24192" }, + { "id": "ley_24270", "text": "Ley 24270" }, + { "id": "ley_24769", "text": "Ley 24769" }, + { "id": "ley_25761", "text": "Ley 25761" }, + { "id": "ley_26735", "text": "Ley 26735" }, + { "id": "ley_451", "text": "Ley 451" } + ], + "NACIONALIDAD": [ + { "id": "argentina", "text": "Argentina" }, + { "id": "armenia", "text": "Armenia" }, + { "id": "australiana", "text": "Australiana" }, + { "id": "belga", "text": "Belga" }, + { "id": "boliviana", "text": "Boliviana" }, + { "id": "brasilera", "text": "Brasilera" }, + { "id": "chilena", "text": "Chilena" }, + { "id": "china", "text": "China" }, + { "id": "colombiana", "text": "Colombiana" }, + { "id": "cubana", "text": "Cubana" }, + { "id": "dominicana", "text": "Dominicana" }, + { "id": "ecuatoriana", "text": "Ecuatoriana" }, + { "id": "española", "text": "Española" }, + { "id": "estadounidense", "text": "Estadounidense" }, + { "id": "estonia", "text": "Estonia" }, + { "id": "francesa", "text": "Francesa" }, + { "id": "italiana", "text": "Italiana" }, + { "id": "kazaka", "text": "Kazaka" }, + { "id": "paraguaya", "text": "Paraguaya" }, + { "id": "peruana", "text": "Peruana" }, + { "id": "rusa", "text": "Rusa" }, + { "id": "senegalesa", "text": "Senegalesa" }, + { "id": "surcoreana", "text": "Surcoreana" }, + { "id": "ucraniana", "text": "Ucraniana" }, + { "id": "uruguaya", "text": "Uruguaya" }, + { "id": "venezolana", "text": "Venezolana" }, + { "id": "no_corresponde", "text": "No corresponde" }, + { "id": "sd", "text": "s/d" } + ], + "PERSONA_ACUSADA_NO_DETERMINADA": [ + { "id": "manifestantes", "text": "Manifestantes" }, + { "id": "organo_jurisdiccional", "text": "Órgano jurisdiccional" }, + { "id": "pagina_web", "text": "Página web" }, + { "id": "persona_juridica", "text": "Persona jurídica" }, + { "id": "personal_policial", "text": "Personal policial" }, + { "id": "usuario_de_chatstep", "text": "Usuario de Chatstep" }, + { + "id": "usuario_de_cuenta_de_google", + "text": "Usuario de cuenta de Google" + }, + { "id": "usuario_de_facebook", "text": "Usuario de Facebook" }, + { "id": "usuario_de_imgur", "text": "Usuario de Imgur" }, + { "id": "usuario_de_instagram", "text": "Usuario de Instagram" }, + { "id": "usuario_de_mercado_libre", "text": "Usuario de Mercado Libre" }, + { "id": "usuario_de_outlook", "text": "Usuario de Outlook" }, + { "id": "usuario_de_skout", "text": "Usuario de Skout" }, + { "id": "usuario_de_skype", "text": "Usuario de Skype" }, + { "id": "usuario_de_tiktok", "text": "Usuario de Tiktok" }, + { "id": "usuario_de_twitter", "text": "Usuario de Twitter" }, + { "id": "usuario_de_whatsapp", "text": "Usuario de WhatsApp" }, + { "id": "usuario_de_youtube", "text": "Usuario de Youtube" }, + { "id": "usuario_microsoft", "text": "Usuario Microsoft" }, + + { "id": "no_corresponde", "text": "No corresponde" }, + { "id": "sd", "text": "s/d" } + ], + "HIJOS_HIJAS_EN_COMUN": [ + { "id": "si", "text": "Si" }, + { "id": "no", "text": "No" } + ], + + "RELACION_Y_TIPO_ENTRE_ACUSADO_Y_DENUNCIANTE": [ + { "id": "familiar", "text": "Familiar" }, + { "id": "pareja", "text": "Pareja" }, + { "id": "ex_pareja", "text": "Ex pareja" }, + { "id": "jefe_empleada", "text": "Jefe empleada" }, + { "id": "familiar_de_ex_pareja", "text": "Familiar de ex pareja" }, + { "id": "pareja_de_la_madre", "text": "Pareja de la madre" }, + { "id": "ninguna", "text": "Ninguna" }, + { "id": "vecino", "text": "Vecino/a" }, + { "id": "profesor", "text": "Profesor" }, + { + "id": "propietario_del_inmueble_de_residencia", + "text": "Propietario del inmueble de residencia" + }, + { "id": "compañero_de_trabajo", "text": "Compañero/a de trabajo" }, + { "id": "amigo", "text": "Amigo/a" }, + { "id": "familiar_de_pareja", "text": "Familiar de pareja" }, + { "id": "s/d", "text": "S/D" }, + { "id": "no_corresponde", "text": "No corresponde" } + ], + "MEDIDAS_DE_PROTECCION_VIGENTES_AL_MOMENTO_DEL_HECHO": [ + { "id": "si", "text": "Si" }, + { "id": "no", "text": "No" } + ], + "MATERIA": [ + { "id": "allanamiento_autonomo", "text": "Allanamiento autónomo" }, + { "id": "habeas_corpus", "text": "Habeas corpus" }, + { "id": "ejecucion_de_multa", "text": "Ejecución de multa" }, + { "id": "amparo", "text": "Amparo" }, + { "id": "contravencional", "text": "Contravencional" }, + { "id": "faltas", "text": "Faltas" }, + { "id": "penal", "text": "Penal" } + ] +} diff --git a/frontend/src/renderer/src/components/validate-dataset/form-group/index.tsx b/frontend/src/renderer/src/components/validate-dataset/form-group/index.tsx new file mode 100644 index 00000000..e2009660 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/form-group/index.tsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from "react"; + +import { DecisionTabs } from "@/components"; +import { useFileDispatch, useForm } from "@/hooks"; +import { appendValidation } from "@/reducers/file/actions"; +import type { DocFile } from "@/types/file"; +import { Suggester, countDecisiones } from "@/utils/predictions"; +import Container from "./FormGroup.styles"; +import { + DatosAcusado, + DatosDenunciante, + Decision, + InfoGral, + InfoHecho, +} from "./forms"; + +interface Props { + file: DocFile; + onCheck: (checked: boolean) => void; +} +export default function FormGroup({ file, onCheck }: Props) { + const [decision, setDecision] = useState(0); + + const [checkedInfoGral, setCheckedInfoGral] = useState(false); + const [checkedInfoHecho, setCheckedInfoHecho] = useState(false); + const [checkedDecision, setCheckedDecision] = useState(false); + const [checkedDatosDenunciante, setCheckedDatosDenunciante] = useState(false); + const [checkedDatosAcusado, setCheckedDatosAcusado] = useState(false); + + const { register, submit, addDecision, decisionAmount, getDecisionValue } = + useForm(countDecisiones(file.predictions)); + const dispatch = useFileDispatch(); + + const createDecision = () => { + addDecision(); + }; + const selectDecision = (n: number) => setDecision(n); + + const handleSubmit = submit((data) => { + console.log({ data }); + dispatch(appendValidation(file.data.name, data)); + }); + + const props = { + register, + onSubmit: handleSubmit, + suggester: new Suggester(file.predictions), + }; + const decisionProps = { + ...props, + getDecisionValue, + }; + + useEffect(() => { + onCheck( + checkedInfoGral && + checkedInfoHecho && + checkedDecision && + checkedDatosDenunciante && + checkedDatosAcusado, + ); + }, [ + onCheck, + checkedInfoGral, + checkedInfoHecho, + checkedDecision, + checkedDatosDenunciante, + checkedDatosAcusado, + ]); + + return ( + + + + + + + + + + + + + ); +} diff --git a/frontend/src/renderer/src/components/validate-dataset/index.tsx b/frontend/src/renderer/src/components/validate-dataset/index.tsx new file mode 100644 index 00000000..c5b0f124 --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/index.tsx @@ -0,0 +1,126 @@ +import { useState } from "react"; + +import { Button, FileAnnotator, FileStepper, Grid } from "@/components"; +import Footer from "@/components/layout/footer"; +import { useFileDispatch, useFiles } from "@/hooks"; +import { SectionTitle } from "@/layout/section-title"; +import { validate } from "@/reducers/file/actions"; +import { css } from "@/styled/css"; +import { HStack, Stack } from "@/styled/jsx"; +import { isFileValidated, isValidationCompleted } from "@/utils/file"; +import { getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import { Navigate, useNavigate, useParams } from "@tanstack/react-router"; +import FormGroup from "./form-group"; +import { moveNext, movePrevious } from "./utils"; + +export function ValidateDataset() { + // HOOKS + const { feature: featureSlug } = useParams({ from: "/$feature/validation" }); + const feature = parseFeatureRouteSlug(featureSlug); + + if (!feature) return ; + const files = useFiles(); + const [checked, setChecked] = useState(false); + const [selected, setSelected] = useState(0); + const dispatch = useFileDispatch(); + const navigate = useNavigate(); + + // FIELDS + const hasStepper = files.length > 1; + + const selectedFile = files[selected]; + // Check if the validation was completed on all the files + const canContinue = isValidationCompleted(files); + const canValidate = isFileValidated(selectedFile); + + // HANDLERS + const moveIndex = (newIndex: number | undefined) => { + if (newIndex !== undefined) setSelected(newIndex); + }; + const nextFile = () => moveIndex(moveNext(selected, files)); + const previousFile = () => moveIndex(movePrevious(selected, files)); + + const handleContinue = () => { + navigate({ + to: "/$feature/finish", + params: { feature: getFeatureRouteSlug(feature) }, + }); + }; + + const handleValidate = async () => { + // Set validated = true so the file is no longer accesible through the FileStepper component + dispatch(validate(selectedFile.data.name)); + + if (canContinue || !hasStepper) { + handleContinue(); + } else { + nextFile(); + } + }; + + const handleCheck = (checked: boolean) => { + setChecked(checked); + }; + + return ( + + + + + + 3. Validación de datos + + + + +
+ + {hasStepper && ( + + )} + + {canContinue ? ( + + ) : ( + + )} + +
+
+ ); +} diff --git a/frontend/src/renderer/src/components/validate-dataset/utils.ts b/frontend/src/renderer/src/components/validate-dataset/utils.ts new file mode 100644 index 00000000..6cc7ca6d --- /dev/null +++ b/frontend/src/renderer/src/components/validate-dataset/utils.ts @@ -0,0 +1,37 @@ +import type { DocFile } from "@/types/file"; + +/** + * Finds the next available index and returns it + * @param cur Current `selected` index + * @param state State array of files + * @returns The next available index + */ +export function movePrevious(cur: number, state: DocFile[]) { + let index = cur; + + do { + index--; + + if (index < 0) return undefined; + } while (state[index].validated); + + return index; +} + +/** + * Finds the previous available index and returns it + * @param cur Current `selected` index + * @param state State array of files + * @returns The previous available index + */ +export function moveNext(cur: number, state: DocFile[]) { + let index = cur; + + do { + index++; + + if (index === state.length) return undefined; + } while (state[index].validated); + + return index; +} diff --git a/frontend/src/renderer/src/components/validation-form/ValidationForm.styles.ts b/frontend/src/renderer/src/components/validation-form/ValidationForm.styles.ts new file mode 100644 index 00000000..36adf124 --- /dev/null +++ b/frontend/src/renderer/src/components/validation-form/ValidationForm.styles.ts @@ -0,0 +1,7 @@ +import { styled } from "@/styles"; + +export const Form = styled("form", { + display: "flex", + flexDirection: "column", + gap: "$l", +}); diff --git a/frontend/src/renderer/src/components/validation-form/index.tsx b/frontend/src/renderer/src/components/validation-form/index.tsx new file mode 100644 index 00000000..3d15eecf --- /dev/null +++ b/frontend/src/renderer/src/components/validation-form/index.tsx @@ -0,0 +1,76 @@ +import { CheckCircle } from "phosphor-react"; +import { + Children, + type FormEvent, + type FormEventHandler, + type ReactElement, + type ReactNode, + cloneElement, + isValidElement, + useEffect, + useState, +} from "react"; + +import { Button } from "@/components"; +import type { NativeComponent } from "@/types/component"; + +import { css } from "@/styled/css"; +import { styled } from "@/styled/jsx"; +import { Form } from "./ValidationForm.styles"; + +interface Props extends NativeComponent<"form"> { + children: ReactNode; + title: string; + onSubmit: FormEventHandler; + onCheck: (checked: boolean) => void; +} +export default function ValidationForm({ + children, + title, + onSubmit, + onCheck, + ...props +}: Props) { + const [checked, setChecked] = useState(false); + + const handleClick = () => setChecked(true); + + const onChange = () => { + if (checked) setChecked(false); + }; + + // Add the onChange handler to every children + const childrenWithHandler = Children.map(children, (child) => { + if (isValidElement(child)) { + return cloneElement(child as ReactElement<{ onChange: () => void }>, { onChange }); + } + return child; + }); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + onSubmit(e); + }; + + useEffect(() => { + onCheck(checked); + }, [checked, onCheck]); + + return ( +
+ {title} + {childrenWithHandler} + +
+ ); +} diff --git a/frontend/src/renderer/src/constants/anonymizer-categories.ts b/frontend/src/renderer/src/constants/anonymizer-categories.ts new file mode 100644 index 00000000..1300ddc0 --- /dev/null +++ b/frontend/src/renderer/src/constants/anonymizer-categories.ts @@ -0,0 +1,82 @@ +import { type AnonymizerLabels, anonymizerLabels } from "@/types/aymurai"; + +export const FALLBACK_ANONYMIZER_CATEGORY = "Otros datos sensibles"; + +export const ANONYMIZER_CATEGORY_LABEL_IDS = { + Roles: [ + "PER", + "DENUNCIANTE", + "ACUSADO/A", + "TESTIGO/A", + "NIÑO/A_ADOSLECENTE", + ], + Lugares: ["LOC", "DIRECCION"], + "Datos personales": ["DNI", "EDAD", "NACIONALIDAD", "ESTUDIOS"], + "Contacto e identificadores digitales": [ + "TELEFONO", + "CORREO_ELECTRONICO", + "USUARIX", + "IP", + "LINK", + "NOMBRE_ARCHIVO", + ], + "Datos judiciales": ["NUM_EXPEDIENTE", "NUM_ACTUACION", "CAUSA", "CUIJ"], + "Datos administrativos y registrales": [ + "CUIT_CUIL", + "AFILIADO", + "NUM_MATRICULA", + "PATENTE_DOMINIO", + ], + "Datos bancarios": ["BANCO", "CBU", "NUM_CAJA_AHORRO"], + Fechas: ["FECHA"], + [FALLBACK_ANONYMIZER_CATEGORY]: [ + "MARCA_AUTOMOVIL", + "INSTITUCION", + "TEXTO_ANONIMIZAR", + ], +} as const satisfies Record; + +export const ANONYMIZER_CATEGORY_NAMES = Object.keys( + ANONYMIZER_CATEGORY_LABEL_IDS, +); + +const labelById = new Map(anonymizerLabels.map((label) => [label.id, label])); + +export const ANONYMIZER_LABEL_CATEGORY = Object.fromEntries( + Object.entries(ANONYMIZER_CATEGORY_LABEL_IDS).flatMap( + ([category, labelIds]) => labelIds.map((labelId) => [labelId, category]), + ), +) as Partial>; + +export function getAnonymizerCategoryForLabel(labelId: string): string { + return ( + ANONYMIZER_LABEL_CATEGORY[labelId as AnonymizerLabels] ?? + FALLBACK_ANONYMIZER_CATEGORY + ); +} + +export function getAnonymizerLabelsForCategory(category: string) { + const configuredIds = ANONYMIZER_CATEGORY_LABEL_IDS[category] ?? []; + const configuredLabels = configuredIds + .map((labelId) => labelById.get(labelId)) + .filter((label) => label !== undefined); + + if (category !== FALLBACK_ANONYMIZER_CATEGORY) return configuredLabels; + + const configuredIdSet = new Set( + Object.values(ANONYMIZER_CATEGORY_LABEL_IDS).flat(), + ); + const uncategorizedLabels = anonymizerLabels.filter( + (label) => !configuredIdSet.has(label.id), + ); + + return [...configuredLabels, ...uncategorizedLabels]; +} + +export function getLabelsPrioritizingCategory(category: string) { + const categoryLabels = getAnonymizerLabelsForCategory(category); + const categoryIds = new Set(categoryLabels.map((label) => label.id)); + const rest = anonymizerLabels.filter((label) => !categoryIds.has(label.id)); + + return [...categoryLabels, ...rest]; +} diff --git a/frontend/src/renderer/src/constants/config.ts b/frontend/src/renderer/src/constants/config.ts new file mode 100644 index 00000000..f8968453 --- /dev/null +++ b/frontend/src/renderer/src/constants/config.ts @@ -0,0 +1,6 @@ +export const DATAGENERO_URL = "https://www.datagenero.org/"; + +/** + * Only allow these extensions to be analyzed + */ +export const WHITELISTED_EXTENSIONS = ["docx", "pdf"]; diff --git a/frontend/src/renderer/src/constants/excluded-tags.ts b/frontend/src/renderer/src/constants/excluded-tags.ts new file mode 100644 index 00000000..3d423f8c --- /dev/null +++ b/frontend/src/renderer/src/constants/excluded-tags.ts @@ -0,0 +1,8 @@ +import { type AnonymizerLabels, anonymizerLabels } from "@/types/aymurai"; + +const entries = anonymizerLabels.map((label) => [label.id, true] as const); + +export const EXCLUDED_TAGS = Object.fromEntries(entries) as Record< + AnonymizerLabels, + boolean +>; diff --git a/frontend/src/renderer/src/constants/i18n/index.ts b/frontend/src/renderer/src/constants/i18n/index.ts new file mode 100644 index 00000000..06b788ca --- /dev/null +++ b/frontend/src/renderer/src/constants/i18n/index.ts @@ -0,0 +1,27 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import es from "./locales/es"; + +// --- TypeScript type augmentation --- +// Tells i18next the exact shape of every namespace, +// so t('key') calls are fully typed and autocompleted. +declare module "i18next" { + interface CustomTypeOptions { + defaultNS: "common"; + resources: typeof es; + } +} + +i18n.use(initReactI18next).init({ + lng: "es", + fallbackLng: "es", + defaultNS: "common", + resources: { + es, + }, + interpolation: { + escapeValue: false, // React already escapes values + }, +}); + +export default i18n; diff --git a/frontend/src/renderer/src/constants/i18n/locales/es/anonymizer.ts b/frontend/src/renderer/src/constants/i18n/locales/es/anonymizer.ts new file mode 100644 index 00000000..5fbaada6 --- /dev/null +++ b/frontend/src/renderer/src/constants/i18n/locales/es/anonymizer.ts @@ -0,0 +1,73 @@ +import { WHITELISTED_EXTENSIONS } from "@/constants/config"; + +const validExtensions = WHITELISTED_EXTENSIONS.map((e) => `.${e}`).join(", "); + +const anonymizer = { + title: "Anonimizador", + subtitle: "Anonimiza resoluciones judiciales de manera automática y editable", + onboarding: { + sectionTitle: "1. Selección de Archivo", + validFormats: `Formatos válidos: ${validExtensions}`, + loadDocuments: "Cargar documentos", + dropAreaTitle: "Selecciona el archivo para anonimizar", + dropAreaFormats: `Formatos válidos: ${validExtensions}`, + }, + preview: { + sectionTitle: "1. Selección de Archivo", + filesLabel: "Archivos seleccionados", + validFormats: `Formatos válidos: ${validExtensions}`, + loadMore: "Cargar más documentos", + continue: "Continuar", + }, + howItWorks: { + step1: { + alt: "Interfaz web con selector y cursor", + title: "Selecciona la resolución judicial", + subtitle: "Sube el documento que quieres anonimizar.", + }, + step2: { + alt: "Barra de búsqueda con cursor", + title: "La inteligencia artificial analiza el documento", + subtitle: + "Reconoce automáticamente la información a anonimizar.", + }, + step3: { + alt: "Visor de documentos con controles de revisión", + title: "Revisión y validación humana", + subtitle: + "Es importante que verifiques que los datos sean correctos antes de exportar el archivo.", + }, + step4: { + alt: "Binoculares con globo terráqueo", + title: "Generación del documento anonimizado", + subtitle: + "Proceso terminado. El documento está listo para ser exportado.", + }, + }, + process: { + sectionTitle: "2. Procesamiento del archivo", + processingTitle: "AymurAI está extrayendo los datos del archivo", + processingSubtitle: "Este proceso puede tardar algunos minutos.", + finishText: "Se finalizó el análisis del documento.", + }, + validation: { + toastSingleSuccess: + 'Se aplicó con éxito una etiqueta de "{{label}}" en una ocurrencia.', + toastAllSuccess: + 'Se aplicó con éxito una etiqueta de "{{label}}" en todas las ocurrencias.', + }, + result: { sectionTitle: "" }, + finish: { + sectionTitle: "4. Finalización", + description: + "Los datos encontrados por AymurAI y posteriormente validados ya han sido anonimizados correctamente.", + subtitle: "Archivo procesado", + restart: "Cargar un nuevo documento", + viewResult: "Descargar ODT", + viewResultPDF: "Descargar PDF", + downloadError: + "Ocurrió un error al generar el archivo. Por favor, intente nuevamente.", + }, +}; + +export default anonymizer; diff --git a/frontend/src/renderer/src/constants/i18n/locales/es/common.ts b/frontend/src/renderer/src/constants/i18n/locales/es/common.ts new file mode 100644 index 00000000..9d4bfac6 --- /dev/null +++ b/frontend/src/renderer/src/constants/i18n/locales/es/common.ts @@ -0,0 +1,39 @@ +const common = { + back: "Volver", + howItWorks: "¿Cómo funciona?", + platformBuiltBy: "Plataforma hecha por", + settings: "Configuración", + home: { + features: { + greeting: "¡Hola! Selecciona la herramienta a utilizar", + }, + host: { + howToConnect: "¿Cómo deseas conectarte a Aymurai?", + optionLocal: "Local", + optionServer: "Servidor", + optionOr: "o", + connectServerExplanation: + "Ingresa la dirección del servidor al que deseas conectarte", + connectServerLabel: "Dirección del servidor", + connectServerSubmit: "Guardar y conectar", + errors: { + network: "No se pudo conectar al servidor", + connection: "Error de conexión", + invalidResponse: "El servidor no respondió correctamente", + invalidUrl: "El formato de la URL es incorrecto.", + unknown: "Error desconocido", + }, + }, + }, + stepper: { + selection: "Selección", + extraction: "Extracción", + validation: "Validación", + finalization: "Finalización", + }, + filePreview: { + loadError: "No se pudo cargar el archivo", + }, +}; + +export default common; diff --git a/frontend/src/renderer/src/constants/i18n/locales/es/dataset.ts b/frontend/src/renderer/src/constants/i18n/locales/es/dataset.ts new file mode 100644 index 00000000..de7fa05f --- /dev/null +++ b/frontend/src/renderer/src/constants/i18n/locales/es/dataset.ts @@ -0,0 +1,64 @@ +import { WHITELISTED_EXTENSIONS } from "@/constants/config"; + +const validExtensions = WHITELISTED_EXTENSIONS.map((e) => `.${e}`).join(", "); + +const dataset = { + title: "Set de datos", + subtitle: "Convierte resoluciones judiciales en set de datos estructurados", + onboarding: { + sectionTitle: "1. Selección de Archivos", + validFormats: `Formatos válidos: ${validExtensions}`, + loadDocuments: "Cargar documentos", + dropAreaTitle: "Selecciona el archivo para\nagregar a la base de datos", + dropAreaFormats: `Formatos válidos: ${validExtensions}`, + }, + preview: { + sectionTitle: "1. Selección de Archivos", + filesLabel: "Archivos seleccionados", + validFormats: `Formatos válidos: ${validExtensions}`, + loadMore: "Cargar más documentos", + continue: "Continuar", + }, + howItWorks: { + step1: { + alt: "Interfaz web con selector y cursor", + title: "Selecciona las resoluciones judiciales", + subtitle: "Sube los documentos que quieres incorporar al set de datos.", + }, + step2: { + alt: "Barra de búsqueda con cursor", + title: "La inteligencia artificial analiza los documentos", + subtitle: + "Extrae automáticamente la información relevante de cada documento.", + }, + step3: { + alt: "Visor de documentos con controles de revisión", + title: "Revisión y validación humana", + subtitle: + "Es importante que verifiques que los datos sean correctos antes de exportar el archivo.", + }, + step4: { + alt: "Binoculares con globo terráqueo", + title: "Generación del set de datos", + subtitle: + "Los documentos pasan a formar parte del set de datos abiertos.", + }, + }, + process: { + sectionTitle: "2. Extracción de datos", + processingTitle: "AymurAI está extrayendo los datos de los archivos", + processingSubtitle: "Este proceso puede tardar algunos minutos.", + finishText: "Se finalizó el análisis de tus documentos.", + }, + result: { sectionTitle: "" }, + finish: { + sectionTitle: "4. Finalización", + description: + "Los datos encontrados por AymurAI y posteriormente validados ya son parte del set de datos abiertos con perspectiva de género.", + subtitle: "Archivos procesados", + restart: "Cargar más documentos", + viewResult: "Ver set de datos", + }, +}; + +export default dataset; diff --git a/frontend/src/renderer/src/constants/i18n/locales/es/index.ts b/frontend/src/renderer/src/constants/i18n/locales/es/index.ts new file mode 100644 index 00000000..d3cf487c --- /dev/null +++ b/frontend/src/renderer/src/constants/i18n/locales/es/index.ts @@ -0,0 +1,11 @@ +import anonymizer from "./anonymizer"; +import common from "./common"; +import dataset from "./dataset"; + +const es = { + common, + dataset, + anonymizer, +}; + +export default es; diff --git a/frontend/src/renderer/src/context/Annotation/index.tsx b/frontend/src/renderer/src/context/Annotation/index.tsx new file mode 100644 index 00000000..4404e057 --- /dev/null +++ b/frontend/src/renderer/src/context/Annotation/index.tsx @@ -0,0 +1,792 @@ +import type { + LabelAnnotation, + SearchAnnotation, +} from "@/components/file-annotator/types"; +import ManualEntityResolutionDialog, { + type ManualEntityResolutionRequest, +} from "@/components/file/manual-entity-resolution-dialog"; +import { getAnonymizerCategoryForLabel } from "@/constants/anonymizer-categories"; +import { EXCLUDED_TAGS } from "@/constants/excluded-tags"; +import { showToast } from "@/features/showToast"; +import { useEntityGroups, useFileDispatch, useFiles } from "@/hooks"; +import { + appendPrediction, + removePrediction, + removePredictionsByText, + updatePredictionLabel, + updatePredictionsByCanonicalId, + updatePredictionsByText, +} from "@/reducers/file/actions"; +import { useHoverState } from "@/store/useHoverState"; +import { + useExcludedTagsConfig, + useGroupOrder, + useGroupOrderActions, +} from "@/store/useLocal"; +import type { + AllLabels, + AllLabelsWithSufix, + AnonymizerLabels, + PredictLabel, +} from "@/types/aymurai"; +import { anonymizerLabels } from "@/types/aymurai"; +import type { DocFile, Paragraph } from "@/types/file"; +import { + type ManualEntityResolution, + findSimilarEntityGroups, + normalizeEntityText, + stripEntityLabelSuffix, +} from "@/utils/anonymizer/entity-similarity"; +import { findNormalizedOccurrenceRanges } from "@/utils/anonymizer/occurrences"; +import { filterActivePredictions } from "@/utils/anonymizer/predictions"; +import { Check, WarningCircle } from "phosphor-react"; +import { + type ReactNode, + createContext, + useCallback, + useContext, + useMemo, + useState, +} from "react"; +import { findSearchIndexes, getSelectionAnnotationRange } from "./utils"; + +interface AnnotationContextValues { + isAnnotable: boolean; + label: AllLabels | null; + add: (prediction: PredictLabel) => void; + remove: (prediction: PredictLabel) => void; + removeByText: (prediction: PredictLabel) => void; + updateLabel: ( + prediction: PredictLabel, + newLabel: AllLabels | AllLabelsWithSufix, + ) => void; + updateByText: ( + prediction: PredictLabel, + newLabel: AllLabels | AllLabelsWithSufix, + ) => void; + updateByCanonicalId: ( + canonicalId: string, + newLabel: AllLabels | AllLabelsWithSufix, + ) => void; + addBySearch: (search: string, label: AllLabels) => void; +} + +/** + * Provides the token received through OAuth2 login to the whole app + */ +export const AnnotationContext = createContext({ + isAnnotable: false, + label: null, + add: () => {}, + remove: () => {}, + removeByText: () => {}, + updateLabel: () => {}, + updateByText: () => {}, + updateByCanonicalId: () => {}, + addBySearch: () => {}, +}); +AnnotationContext.displayName = "AnnotationContext"; + +interface Props { + children?: ReactNode; + file: DocFile; + isAnnotable?: boolean; + label: AllLabels | null; +} + +type PendingManualResolution = ManualEntityResolutionRequest & { + predictions: PredictLabel[]; + selectedLabel: AllLabels; +}; + +const ANONYMIZER_LABEL_IDS = new Set( + anonymizerLabels.map((item) => item.id), +); + +function toBaseLabel(label: AllLabels | AllLabelsWithSufix): AllLabels { + return stripEntityLabelSuffix(String(label)) as AllLabels; +} + +function isKnownAnonymizerLabel(label: string): label is AnonymizerLabels { + return ANONYMIZER_LABEL_IDS.has(label); +} + +function isLabelEnabled( + label: AllLabels, + excludedTags: Record | null, +): boolean { + const baseLabel = stripEntityLabelSuffix(String(label)); + if (!isKnownAnonymizerLabel(baseLabel)) return true; + return (excludedTags ?? EXCLUDED_TAGS)[baseLabel] !== false; +} + +function isExcludedMention(text: string, excludedWords: string[]): boolean { + const normalizedText = normalizeEntityText(text); + return excludedWords.some( + (word) => normalizeEntityText(word) === normalizedText, + ); +} + +function withCanonicalEntity( + prediction: PredictLabel, + canonicalId: string, + label: AllLabels | AllLabelsWithSufix, +): PredictLabel { + return { + ...prediction, + attrs: { + ...prediction.attrs, + aymurai_label: label, + canonical_entity_id: canonicalId, + }, + }; +} + +function compareAppearance( + a: { paragraphIndex: number; start_char: number }, + b: { paragraphIndex: number; start_char: number }, +) { + if (a.paragraphIndex !== b.paragraphIndex) { + return a.paragraphIndex - b.paragraphIndex; + } + return a.start_char - b.start_char; +} + +function rangesOverlap( + a: { start_char: number; end_char: number }, + b: { start_char: number; end_char: number }, +) { + return a.start_char < b.end_char && b.start_char < a.end_char; +} + +function hasInternalOverlap(predictions: PredictLabel[]) { + const byParagraph = new Map(); + for (const prediction of predictions) { + const paragraphPredictions = byParagraph.get(prediction.paragraphId) ?? []; + paragraphPredictions.push(prediction); + byParagraph.set(prediction.paragraphId, paragraphPredictions); + } + + for (const paragraphPredictions of byParagraph.values()) { + const sorted = [...paragraphPredictions].sort( + (a, b) => a.start_char - b.start_char, + ); + for (let i = 1; i < sorted.length; i += 1) { + const previous = sorted[i - 1]; + const current = sorted[i]; + if (previous && current && rangesOverlap(previous, current)) return true; + } + } + + return false; +} + +export default function AnnotationProvider({ + children, + file, + isAnnotable = false, + label, +}: Props) { + const dispatch = useFileDispatch(); + const files = useFiles(); + const { tags, words } = useExcludedTagsConfig(); + const { groupOrder } = useGroupOrder(); + const { setGroupOrder } = useGroupOrderActions(); + const setLastEditedCanonicalId = useHoverState( + (s) => s.setLastEditedCanonicalId, + ); + const [pendingResolution, setPendingResolution] = + useState(null); + + const activeFiles = useMemo( + () => + files.map((item) => ({ + ...item, + predictions: filterActivePredictions(item.predictions, tags, words), + })), + [files, tags, words], + ); + const groups = useEntityGroups(activeFiles); + const activePredictionsByParagraph = useMemo(() => { + const result = new Map(); + const activePredictions = filterActivePredictions( + file.predictions, + tags, + words, + ); + + for (const prediction of activePredictions) { + const paragraphPredictions = result.get(prediction.paragraphId) ?? []; + paragraphPredictions.push(prediction); + result.set(prediction.paragraphId, paragraphPredictions); + } + + return result; + }, [file.predictions, tags, words]); + + const paragraphIndexById = useMemo(() => { + const result = new Map(); + for (const currentFile of files) { + for (const [index, paragraph] of ( + currentFile.paragraphs ?? [] + ).entries()) { + if (!result.has(paragraph.id)) result.set(paragraph.id, index); + } + } + return result; + }, [files]); + + const getFirstAppearance = useCallback( + (predictions: PredictLabel[]) => { + return predictions.reduce( + (earliest, prediction) => { + const current = { + paragraphIndex: paragraphIndexById.get(prediction.paragraphId) ?? 0, + start_char: prediction.start_char, + }; + return compareAppearance(current, earliest) < 0 ? current : earliest; + }, + { + paragraphIndex: Number.POSITIVE_INFINITY, + start_char: Number.POSITIVE_INFINITY, + }, + ); + }, + [paragraphIndexById], + ); + + const updateGroupOrderForNewGroup = useCallback( + ( + canonicalId: string, + selectedLabel: AllLabels, + predictions: PredictLabel[], + ) => { + const category = getAnonymizerCategoryForLabel(String(selectedLabel)); + const existingOrder = groupOrder?.[category]; + if (!existingOrder) return; + + const firstAppearance = getFirstAppearance(predictions); + const categoryGroups = groups + .filter( + (group) => + getAnonymizerCategoryForLabel(group.renderBase) === category, + ) + .sort((a, b) => + compareAppearance(a.firstAppearance, b.firstAppearance), + ); + const nextOrder = existingOrder.filter((id) => id !== canonicalId); + const insertBefore = categoryGroups.find( + (group) => + compareAppearance(firstAppearance, group.firstAppearance) < 0, + ); + + if (!insertBefore) { + nextOrder.push(canonicalId); + } else { + const index = nextOrder.indexOf(insertBefore.canonicalId); + if (index === -1) nextOrder.push(canonicalId); + else nextOrder.splice(index, 0, canonicalId); + } + + setGroupOrder({ ...groupOrder, [category]: nextOrder }); + }, + [getFirstAppearance, groupOrder, groups, setGroupOrder], + ); + + const commitPredictions = useCallback( + ( + predictions: PredictLabel[], + canonicalId: string, + resolvedLabel: AllLabels | AllLabelsWithSufix, + ) => { + for (const prediction of predictions) { + dispatch( + appendPrediction( + file.data.name, + withCanonicalEntity(prediction, canonicalId, resolvedLabel), + ), + ); + } + setLastEditedCanonicalId(canonicalId); + showToast( + predictions.length > 1 + ? "Se agregaron las etiquetas al grupo de entidad." + : "Se agregó la etiqueta al grupo de entidad.", + "success", + Check, + ); + }, + [dispatch, file.data.name, setLastEditedCanonicalId], + ); + + const commitNewGroup = useCallback( + (predictions: PredictLabel[], selectedLabel: AllLabels) => { + const canonicalId = crypto.randomUUID(); + commitPredictions(predictions, canonicalId, selectedLabel); + updateGroupOrderForNewGroup(canonicalId, selectedLabel, predictions); + }, + [commitPredictions, updateGroupOrderForNewGroup], + ); + + const queueManualPredictions = useCallback( + ( + predictions: PredictLabel[], + selectedLabel: AllLabels | AllLabelsWithSufix, + ) => { + if (predictions.length === 0) return; + + const baseLabel = toBaseLabel(selectedLabel); + const text = predictions[0]?.text ?? ""; + if (!normalizeEntityText(text)) return; + + if (hasInternalOverlap(predictions)) { + showToast( + "No se pueden agregar ocurrencias solapadas entre sí.", + "error", + WarningCircle, + ); + return; + } + + const overlappingPrediction = predictions.find((prediction) => + (activePredictionsByParagraph.get(prediction.paragraphId) ?? []).some( + (activePrediction) => rangesOverlap(prediction, activePrediction), + ), + ); + if (overlappingPrediction) { + showToast( + "La selección se solapa con una entidad activa. Eliminá o editá la entidad existente antes de crear otra.", + "error", + WarningCircle, + ); + return; + } + + if (!isLabelEnabled(baseLabel, tags)) { + showToast( + "La categoría seleccionada está desactivada en Configuración.", + "error", + WarningCircle, + ); + return; + } + + if (isExcludedMention(text, words)) { + showToast( + "El término seleccionado está en la lista de términos excluidos.", + "error", + WarningCircle, + ); + return; + } + + const candidates = findSimilarEntityGroups( + { + text, + normalizedText: normalizeEntityText(text), + label: String(baseLabel), + paragraphId: predictions[0]?.paragraphId, + start_char: predictions[0]?.start_char, + end_char: predictions[0]?.end_char, + }, + groups, + ); + + if (candidates.length === 0) { + commitNewGroup(predictions, baseLabel); + return; + } + + setPendingResolution({ + text, + label: String(baseLabel), + count: predictions.length, + candidates, + predictions, + selectedLabel: baseLabel, + }); + }, + [activePredictionsByParagraph, commitNewGroup, groups, tags, words], + ); + + const add = useCallback( + (prediction: PredictLabel) => { + queueManualPredictions([prediction], prediction.attrs.aymurai_label); + }, + [queueManualPredictions], + ); + + const remove = useCallback( + (prediction: PredictLabel) => { + dispatch(removePrediction(file.data.name, prediction)); + }, + [dispatch, file.data.name], + ); + + const removeByText = useCallback( + (prediction: PredictLabel) => { + dispatch(removePredictionsByText(file.data.name, prediction.text)); + }, + [dispatch, file.data.name], + ); + + const updateLabel = useCallback( + (prediction: PredictLabel, newLabel: AllLabels | AllLabelsWithSufix) => { + dispatch(updatePredictionLabel(file.data.name, prediction, newLabel)); + }, + [dispatch, file.data.name], + ); + + const updateByText = useCallback( + (prediction: PredictLabel, newLabel: AllLabels | AllLabelsWithSufix) => { + const normalizedText = normalizeEntityText(prediction.text); + if (!normalizedText) return; + + const sourceBaseLabel = toBaseLabel(prediction.attrs.aymurai_label); + const nextBaseLabel = toBaseLabel(newLabel); + const sourceCanonicalId = prediction.attrs.canonical_entity_id ?? null; + const targetCanonicalId = + sourceCanonicalId && sourceBaseLabel === nextBaseLabel + ? sourceCanonicalId + : crypto.randomUUID(); + const missingPredictions: PredictLabel[] = []; + let skippedOverlaps = 0; + + file.paragraphs?.forEach((paragraph: Paragraph) => { + const ranges = findNormalizedOccurrenceRanges( + paragraph.value, + prediction.text, + ); + + ranges.forEach((range) => { + const draftRange = { + start_char: range.start, + end_char: range.end, + }; + const activeOverlaps = ( + activePredictionsByParagraph.get(paragraph.id) ?? [] + ).filter((activePrediction) => + rangesOverlap(draftRange, activePrediction), + ); + const hasEquivalentActiveOverlap = activeOverlaps.some( + (activePrediction) => + normalizeEntityText(activePrediction.text) === normalizedText, + ); + + if (hasEquivalentActiveOverlap) return; + + if (activeOverlaps.length > 0) { + skippedOverlaps += 1; + return; + } + + missingPredictions.push({ + mentionId: crypto.randomUUID(), + start_char: range.start, + end_char: range.end, + paragraphId: paragraph.id, + text: range.text, + attrs: { + aymurai_label: newLabel, + aymurai_label_subclass: null, + aymurai_alt_text: null, + aymurai_alt_start_char: range.start, + aymurai_alt_end_char: range.end, + }, + }); + }); + }); + + dispatch( + updatePredictionsByText( + file.data.name, + prediction.text, + newLabel, + targetCanonicalId, + ), + ); + + for (const missingPrediction of missingPredictions) { + dispatch( + appendPrediction( + file.data.name, + withCanonicalEntity(missingPrediction, targetCanonicalId, newLabel), + ), + ); + } + + const createdNewGroup = + !sourceCanonicalId || sourceBaseLabel !== nextBaseLabel; + if (createdNewGroup) { + updateGroupOrderForNewGroup( + targetCanonicalId, + nextBaseLabel, + missingPredictions.length > 0 ? missingPredictions : [prediction], + ); + } + + setLastEditedCanonicalId(targetCanonicalId); + + if (skippedOverlaps > 0) { + showToast( + "Algunas ocurrencias no se agregaron porque se solapan con entidades activas.", + "error", + WarningCircle, + ); + } + }, + [ + activePredictionsByParagraph, + dispatch, + file.data.name, + file.paragraphs, + setLastEditedCanonicalId, + updateGroupOrderForNewGroup, + ], + ); + + const updateByCanonicalId = useCallback( + (canonicalId: string, newLabel: AllLabels | AllLabelsWithSufix) => { + dispatch(updatePredictionsByCanonicalId(canonicalId, newLabel)); + }, + [dispatch], + ); + + const addBySearch = useCallback( + (search: string, label: AllLabels) => { + if (!search || search.length < 3) return; + + const normalizedSearch = normalizeEntityText(search); + const predictions: PredictLabel[] = []; + let skippedOverlaps = 0; + const reusableCanonicalId = [...activePredictionsByParagraph.values()] + .flat() + .find( + (prediction) => + normalizeEntityText(prediction.text) === normalizedSearch && + prediction.attrs.canonical_entity_id, + )?.attrs.canonical_entity_id; + + file.paragraphs?.forEach((paragraph: Paragraph) => { + const normalizedRanges = findNormalizedOccurrenceRanges( + paragraph.value, + search, + ); + const ranges = + normalizedRanges.length > 0 + ? normalizedRanges + : findSearchIndexes(paragraph.value, search).map((start) => ({ + start, + end: start + search.length, + text: paragraph.value.slice(start, start + search.length), + normalizedText: normalizeEntityText(search), + })); + ranges.forEach((range) => { + const draftRange = { + start_char: range.start, + end_char: range.end, + }; + const activeOverlaps = ( + activePredictionsByParagraph.get(paragraph.id) ?? [] + ).filter((activePrediction) => + rangesOverlap(draftRange, activePrediction), + ); + const hasEquivalentActiveOverlap = activeOverlaps.some( + (activePrediction) => + normalizeEntityText(activePrediction.text) === normalizedSearch, + ); + + if (hasEquivalentActiveOverlap) return; + + if (activeOverlaps.length > 0) { + skippedOverlaps += 1; + return; + } + + const prediction: PredictLabel = { + mentionId: crypto.randomUUID(), + start_char: range.start, + end_char: range.end, + paragraphId: paragraph.id, + text: range.text, + attrs: { + aymurai_label: label, + aymurai_label_subclass: null, + aymurai_alt_text: null, + aymurai_alt_start_char: range.start, + aymurai_alt_end_char: range.end, + }, + }; + predictions.push(prediction); + }); + }); + + if (predictions.length === 0 && skippedOverlaps > 0) { + showToast( + "No se agregaron ocurrencias porque todas se solapan con entidades activas.", + "error", + WarningCircle, + ); + return; + } + + if (reusableCanonicalId) { + dispatch( + updatePredictionsByText( + file.data.name, + search, + label, + reusableCanonicalId, + ), + ); + + for (const prediction of predictions) { + dispatch( + appendPrediction( + file.data.name, + withCanonicalEntity(prediction, reusableCanonicalId, label), + ), + ); + } + + setLastEditedCanonicalId(reusableCanonicalId); + if (skippedOverlaps > 0) { + showToast( + "Algunas ocurrencias no se agregaron porque se solapan con entidades activas.", + "error", + WarningCircle, + ); + } + return; + } + + queueManualPredictions(predictions, label); + }, + [ + activePredictionsByParagraph, + dispatch, + file.data.name, + file.paragraphs, + queueManualPredictions, + setLastEditedCanonicalId, + ], + ); + + const selectHandler = () => { + // If the user hasn't selected any tag to search, do nothing + if (!label) return; + + const selection = window.getSelection(); + if (!selection) return; + const selectedRange = getSelectionAnnotationRange(selection); + if (!selectedRange) return; + + add({ + mentionId: crypto.randomUUID(), + start_char: selectedRange.start, + end_char: selectedRange.end, + paragraphId: selectedRange.paragraphId, + text: selectedRange.text, + attrs: { + aymurai_label: label, + aymurai_label_subclass: null, + aymurai_alt_text: null, + aymurai_alt_start_char: selectedRange.start, + aymurai_alt_end_char: selectedRange.end, + }, + }); + }; + + const handleResolveManualEntity = (resolution: ManualEntityResolution) => { + if (!pendingResolution) return; + + if (resolution.type === "existing") { + commitPredictions( + pendingResolution.predictions, + resolution.candidate.group.canonicalId, + resolution.candidate.group.renderBase as AllLabels | AllLabelsWithSufix, + ); + } else { + commitNewGroup( + pendingResolution.predictions, + pendingResolution.selectedLabel, + ); + } + + setPendingResolution(null); + }; + + return ( + +
{children}
+ setPendingResolution(null)} + onResolve={handleResolveManualEntity} + /> +
+ ); +} + +export const useAnnotation = () => { + const { + add, + remove, + removeByText, + isAnnotable, + label, + updateLabel, + updateByText, + updateByCanonicalId, + addBySearch, + } = useContext(AnnotationContext); + + const createAnnotationData = ( + text: string, + annotation: LabelAnnotation | SearchAnnotation, + labelOverride?: AllLabels | AllLabelsWithSufix, + ) => { + const { start, end, paragraphId, tag } = annotation; + const resolvedTag = labelOverride ?? tag; + if (!resolvedTag) return null; + return { + mentionId: crypto.randomUUID(), + text, + start_char: start, + end_char: end, + paragraphId: paragraphId, + attrs: { + aymurai_label: resolvedTag, + aymurai_label_subclass: null, + aymurai_alt_text: null, + aymurai_alt_start_char: start, + aymurai_alt_end_char: end, + }, + } satisfies PredictLabel; + }; + + return { + add, + remove, + removeByText, + isAnnotable, + label, + updateLabel, + updateByText, + updateByCanonicalId, + addBySearch, + createAnnotationData, + }; +}; diff --git a/frontend/src/renderer/src/context/Annotation/utils.ts b/frontend/src/renderer/src/context/Annotation/utils.ts new file mode 100644 index 00000000..00ddaea4 --- /dev/null +++ b/frontend/src/renderer/src/context/Annotation/utils.ts @@ -0,0 +1,136 @@ +/** + * Check if the selection contains another Node. + * @param selection The current selection. + * @returns `true` if the selection contains a Node, `false` otherwise. + */ +export const selectionHasNodes = (selection: Selection | null): boolean => { + if (!selection) return false; + + const container = document.createElement("div"); + + for (let i = 0; i < selection.rangeCount; ++i) { + container.appendChild(selection.getRangeAt(i).cloneContents()); + } + + return new RegExp(/<.*?>/g).test(container.innerHTML); +}; + +/** + * Checks if the node is can be annotated. It checks the type of Node and its parentElement (if it's a span). + * @param node Node from the selection + * @returns `true` if the node is a valid node, `false` otherwise. + */ +export const isValidNode = (node: Node | null): node is Node => + !!node && + node instanceof Text && + node.parentElement instanceof HTMLSpanElement; + +/** + * Get the start and end boundaries of the selection. Reverses the boundaries if the selection is made from right to left. + * @param selection Selection object + * @returns The start and end boundaries of the selection + */ +export const getBoundaries = (selection: Selection): [number, number] => { + const start = selection.anchorOffset; + const end = selection.focusOffset; + return [start, end].sort((a, b) => a - b) as [number, number]; +}; + +/** + * Get the id of the paragraph where the selection is made. + * @param selection Selection object + * @returns The id of the paragraph where the selection is made + */ +export const paragraphIdFromSelection = (selection: Selection) => { + // This is evaluated after the isValidNode check, so we can safely assume that the parentElement is a span + const span = selection.anchorNode?.parentElement; + const paragraph = span?.parentElement as HTMLParagraphElement; + + return paragraph.id; +}; + +function closestOffsetElement(node: Node | null): HTMLElement | null { + const element = node instanceof HTMLElement ? node : node?.parentElement; + return element?.closest("[data-start]") ?? null; +} + +function offsetWithinElement( + element: HTMLElement, + container: Node, + offset: number, +): number | null { + const range = document.createRange(); + range.setStart(element, 0); + + try { + range.setEnd(container, offset); + } catch { + return null; + } + + return range.toString().length; +} + +export function getSelectionAnnotationRange(selection: Selection): { + paragraphId: string; + start: number; + end: number; + text: string; +} | null { + if (selection.rangeCount === 0 || selection.type !== "Range") return null; + + const range = selection.getRangeAt(0); + const text = range.toString(); + if (!text) return null; + + const startElement = closestOffsetElement(range.startContainer); + const endElement = closestOffsetElement(range.endContainer); + if (!startElement || !endElement) return null; + + const startParagraph = startElement.closest("p[id]"); + const endParagraph = endElement.closest("p[id]"); + if (!startParagraph || !endParagraph || startParagraph.id !== endParagraph.id) + return null; + + const startBase = Number(startElement.dataset.start ?? 0); + const endBase = Number(endElement.dataset.start ?? 0); + const startOffset = offsetWithinElement( + startElement, + range.startContainer, + range.startOffset, + ); + const endOffset = offsetWithinElement( + endElement, + range.endContainer, + range.endOffset, + ); + + if (startOffset === null || endOffset === null) return null; + + return { + paragraphId: startParagraph.id, + start: startBase + startOffset, + end: endBase + endOffset, + text, + }; +} + +/** + * Find all indexes where a search string appears in a text. + * @param text The text to search in + * @param search The string to search for + * @returns An array of indexes where the search string appears + */ +export const findSearchIndexes = (text: string, search: string): number[] => { + const indexes: number[] = []; + const lowerText = text.toLowerCase(); + const lowerSearch = search.toLowerCase(); + let index = lowerText.indexOf(lowerSearch); + + while (index !== -1) { + indexes.push(index); + index = lowerText.indexOf(lowerSearch, index + 1); + } + + return indexes; +}; diff --git a/frontend/src/renderer/src/context/File.tsx b/frontend/src/renderer/src/context/File.tsx new file mode 100644 index 00000000..27d2724d --- /dev/null +++ b/frontend/src/renderer/src/context/File.tsx @@ -0,0 +1,35 @@ +import { + type Dispatch, + type ReactNode, + createContext, + useReducer, +} from "react"; + +import reducer, { type Action } from "@/reducers/file"; +import type { DocFile } from "@/types/file"; + +/** + * Context used to provide files that have to be processed + */ +export const FileContext = createContext([]); +FileContext.displayName = "FileContext"; + +/** + * Context used to provide the dispatch function + */ +export const FileDispatchContext = createContext>(() => {}); +FileDispatchContext.displayName = "FileDispatchContext"; + +interface Props { + children: ReactNode; +} +export default function FileProvider({ children }: Props) { + const [state, dispatch] = useReducer(reducer, []); + return ( + + + {children} + + + ); +} diff --git a/frontend/src/renderer/src/features/APIProtected.tsx b/frontend/src/renderer/src/features/APIProtected.tsx new file mode 100644 index 00000000..0ccd1a76 --- /dev/null +++ b/frontend/src/renderer/src/features/APIProtected.tsx @@ -0,0 +1,12 @@ +import api from "@/services/api"; +import { Navigate } from "@tanstack/react-router"; +import type { ReactNode } from "react"; + +interface Props { + children: ReactNode; +} + +export default function APIProtected({ children }: Props) { + if (!api.defaults.baseURL) return ; + return children; +} diff --git a/frontend/src/renderer/src/features/ReactQueryProvider.tsx b/frontend/src/renderer/src/features/ReactQueryProvider.tsx new file mode 100644 index 00000000..33c23f18 --- /dev/null +++ b/frontend/src/renderer/src/features/ReactQueryProvider.tsx @@ -0,0 +1,55 @@ +import { + MutationCache, + QueryClient, + QueryClientProvider, + matchQuery, +} from "@tanstack/react-query"; + +export function getContext() { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: false, + }, + }, + mutationCache: new MutationCache({ + onSettled: async (_data, _err, _vars, _ctx, mutation) => { + if (!mutation.meta) return; + const { invalidates, awaits } = mutation.meta; + + const promise = queryClient.invalidateQueries({ + predicate: (query) => { + if (typeof invalidates === "boolean") return invalidates; + + return ( + invalidates?.some((queryKey) => + matchQuery({ queryKey }, query), + ) ?? false + ); + }, + }); + + if (awaits) { + await promise; + } + }, + }), + }); + + return { + queryClient, + }; +} + +export function Provider({ + children, + queryClient, +}: { + children: React.ReactNode; + queryClient: QueryClient; +}) { + return ( + {children} + ); +} diff --git a/frontend/src/renderer/src/features/RequireFile.tsx b/frontend/src/renderer/src/features/RequireFile.tsx new file mode 100644 index 00000000..4e9c518f --- /dev/null +++ b/frontend/src/renderer/src/features/RequireFile.tsx @@ -0,0 +1,22 @@ +import { useFiles } from "@/hooks"; +import { getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import { Navigate, useParams } from "@tanstack/react-router"; +import type { ReactNode } from "react"; + +interface Props { + children: ReactNode; +} + +export default function RequireFile({ children }: Props) { + const files = useFiles(); + const { feature: featureSlug } = useParams({ strict: false }) as { feature?: string }; + const feature = featureSlug ? parseFeatureRouteSlug(featureSlug) : null; + + if (!feature) return ; + + if (!files.length) { + return ; + } + + return children; +} diff --git a/frontend/src/renderer/src/features/showToast.tsx b/frontend/src/renderer/src/features/showToast.tsx new file mode 100644 index 00000000..0199ac4a --- /dev/null +++ b/frontend/src/renderer/src/features/showToast.tsx @@ -0,0 +1,7 @@ +import Toast, { type ToastVariant } from "@/components/ui/toast"; +import type { Icon } from "phosphor-react"; +import { toast } from "react-hot-toast"; + +export function showToast(message: string, variant: ToastVariant = "info", icon?: Icon) { + toast.custom((t) => ); +} diff --git a/frontend/src/renderer/src/hooks/index.ts b/frontend/src/renderer/src/hooks/index.ts new file mode 100644 index 00000000..a5b268f4 --- /dev/null +++ b/frontend/src/renderer/src/hooks/index.ts @@ -0,0 +1,8 @@ +import { useFileDispatch, useFiles } from "./useFiles"; +import useForm from "./useForm"; + +export { useEntityGroups } from "./useEntityGroups"; +export type { EntityGroup } from "./useEntityGroups"; +export { usePredict } from "./usePredict"; + +export { useFileDispatch, useFiles, useForm }; diff --git a/frontend/src/renderer/src/hooks/useDisambiguate.ts b/frontend/src/renderer/src/hooks/useDisambiguate.ts new file mode 100644 index 00000000..b4ce9704 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useDisambiguate.ts @@ -0,0 +1,81 @@ +import { useEffect, useRef } from "react"; + +import { useFileDispatch } from "@/hooks"; +import { addPredictions, removePredictions } from "@/reducers/file/actions"; +import { disambiguate } from "@/services/aymurai/queries"; +import type { DocFile } from "@/types/file"; +import { useQueries } from "@tanstack/react-query"; + +import type { PredictStatus } from "./usePredict"; + +type FileDisambiguate = { status: PredictStatus }; + +/** + * Runs disambiguation for each file **after all its predictions have arrived**. + * When `enabled` is false (e.g. datapublic flow) every file resolves to + * `"completed"` immediately and no requests are made. + * + * Aligned index-wise with `files`: `queries[i]` corresponds to `files[i]`. + */ +export function useDisambiguate( + files: DocFile[], + predictStatuses: Record< + string, + { status: PredictStatus; fromValidation?: boolean } + >, + enabled: boolean, +): Record { + const dispatch = useFileDispatch(); + // Track which files have already had their disambiguated predictions dispatched + const dispatchedRef = useRef(new Set()); + + const queries = useQueries({ + queries: files.map((file) => ({ + ...disambiguate(file), + // Fire only when this step is enabled, predict is fully done, AND the + // predictions were not loaded from stored DB validation (which is already + // post-disambiguation data and must not be overwritten). + enabled: + enabled && + predictStatuses[file.data.name]?.status === "completed" && + !predictStatuses[file.data.name]?.fromValidation, + })), + }); + + // Dispatch disambiguated predictions exactly once per file on success + useEffect(() => { + queries.forEach((query, i) => { + const fileName = files[i].data.name; + if (!query.isSuccess || dispatchedRef.current.has(fileName)) return; + + dispatchedRef.current.add(fileName); + // Replace raw predictions with the disambiguated ones + dispatch(removePredictions(fileName)); + dispatch(addPredictions(fileName, query.data)); + }); + }); + + const result: Record = {}; + + files.forEach((file, i) => { + const fileName = file.data.name; + const query = queries[i]; + const predictDone = predictStatuses[fileName]?.status === "completed"; + + result[fileName] = { + status: !enabled + ? "completed" // this step is skipped for non-anonymizer flows + : predictStatuses[fileName]?.fromValidation + ? "completed" // predictions came from DB validation; disambiguation not needed + : !predictDone + ? "processing" // still waiting on predict + : query.isError + ? "error" + : query.isSuccess + ? "completed" + : "processing", + }; + }); + + return result; +} diff --git a/frontend/src/renderer/src/hooks/useEntityGroups.ts b/frontend/src/renderer/src/hooks/useEntityGroups.ts new file mode 100644 index 00000000..fcdf99b8 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useEntityGroups.ts @@ -0,0 +1,269 @@ +import { useMemo } from "react"; + +import type { PredictLabel } from "@/types/aymurai"; +import type { DocFile } from "@/types/file"; +import { + normalizeEntityText, + stripEntityLabelSuffix, +} from "@/utils/anonymizer/entity-similarity"; + +// ─── Public types ───────────────────────────────────────────────────────────── + +export interface EntityGroup { + /** The canonical_entity_id that identifies this group. */ + canonicalId: string; + /** aymurai_label with numeric suffix stripped (e.g. "PER", "DNI"). */ + renderBase: string; + /** 1-indexed position among all groups sharing the same renderBase, ordered + * by first appearance (paragraphIndex then start_char). */ + suffixIndex: number; + /** Human-readable render token shown to the user (e.g. "PER_1"). */ + renderToken: string; + /** All individual mention predictions belonging to this group. */ + mentions: PredictLabel[]; + /** Deduplicated normalised texts, sorted alphabetically. Used internally for matching. */ + uniqueTexts: string[]; + /** Verbatim texts corresponding 1-to-1 with {@link uniqueTexts}. Each inner array holds all distinct + * surface forms that share the same normalised text (e.g. ["PUERTO BELGRANO", "Puerto Belgrano"]). + * Use for display; use {@link uniqueTexts} for matching/removal. */ + displayTexts: string[][]; + /** Location of the earliest mention, for ordering and UX. */ + firstAppearance: { paragraphIndex: number; start_char: number }; + /** Whether the group should be anonymised (majority vote from mentions). */ + anonymize: boolean; + /** True when another group has the same renderBase and uniqueTexts set. */ + isDuplicate: boolean; + /** canonicalId of the earliest group that is identical to this one. */ + duplicateOf: string | null; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +/** Strip a numeric suffix like `_1`, `_12` from a label string. */ +export function stripSuffix(label: string): string { + return stripEntityLabelSuffix(label); +} + +// ─── Main hook ──────────────────────────────────────────────────────────────── + +/** + * Derives a flat array of {@link EntityGroup} objects from all predictions + * across the provided files. + * + * Groups are identified by `canonical_entity_id`. Mentions without a + * canonical id are silently skipped (they are raw predict results that haven't + * been through the disambiguate step yet). + * + * Suffix indices are assigned by first-appearance order per `renderBase`. + * Duplicate detection compares `(renderBase, uniqueTexts)`. + */ +export function useEntityGroups(files: DocFile[]): EntityGroup[] { + return useMemo(() => { + // Build a paragraphId → index map across all files so we can order + // mentions by paragraph position. + const paragraphIndexByParagraphId = new Map(); + for (const file of files) { + for (const [i, p] of (file.paragraphs ?? []).entries()) { + if (!paragraphIndexByParagraphId.has(p.id)) { + paragraphIndexByParagraphId.set(p.id, i); + } + } + } + + // Accumulate mentions per canonicalId, plus track renderBase and first appearance. + const byCanonicalId = new Map< + string, + { + mentions: PredictLabel[]; + renderBase: string; + firstAppearance: { paragraphIndex: number; start_char: number }; + } + >(); + + for (const file of files) { + for (const pred of file.predictions ?? []) { + const { canonical_entity_id, aymurai_label } = pred.attrs; + if (!canonical_entity_id) continue; + + const renderBase = stripSuffix(String(aymurai_label)); + const paragraphIndex = + paragraphIndexByParagraphId.get(pred.paragraphId) ?? 0; + + if (!byCanonicalId.has(canonical_entity_id)) { + byCanonicalId.set(canonical_entity_id, { + mentions: [], + renderBase, + firstAppearance: { + paragraphIndex, + start_char: pred.start_char, + }, + }); + } + + const entry = byCanonicalId.get(canonical_entity_id); + if (!entry) continue; + entry.mentions.push(pred); + + // Keep the earliest appearance + const fa = entry.firstAppearance; + if ( + paragraphIndex < fa.paragraphIndex || + (paragraphIndex === fa.paragraphIndex && + pred.start_char < fa.start_char) + ) { + fa.paragraphIndex = paragraphIndex; + fa.start_char = pred.start_char; + } + } + } + + // Sort canonicalIds by first appearance so suffix numbering is stable and + // matches the document reading order (same as backend behaviour). + const sorted = [...byCanonicalId.entries()].sort(([, a], [, b]) => { + const aPara = a.firstAppearance.paragraphIndex; + const bPara = b.firstAppearance.paragraphIndex; + if (aPara !== bPara) return aPara - bPara; + return a.firstAppearance.start_char - b.firstAppearance.start_char; + }); + + // Assign suffix indices per renderBase. + const suffixCounter = new Map(); + const suffixByCanonicalId = new Map(); + for (const [canonicalId, { renderBase }] of sorted) { + const next = (suffixCounter.get(renderBase) ?? 0) + 1; + suffixCounter.set(renderBase, next); + suffixByCanonicalId.set(canonicalId, next); + } + + // Build EntityGroup objects. + const groups: EntityGroup[] = sorted.map( + ([canonicalId, { mentions, renderBase, firstAppearance }]) => { + // Build normalised → first distinct verbatim form seen for that normalisation. + // We intentionally keep only ONE display representative per normalised bucket: + // multiple document spans may share the same alias and we don't want to show + // the same alias chip multiple times. Verbatim comparison uses NFC so that + // Unicode-composed and decomposed forms of the same visible character are + // treated as identical. + const normalizedToVerbatims = new Map(); + for (const m of mentions) { + const norm = normalizeEntityText(m.text); + if (!norm) continue; + const existing = normalizedToVerbatims.get(norm); + if (!existing) { + normalizedToVerbatims.set(norm, [m.text]); + } else if ( + !existing.some( + (v) => + v.normalize("NFC") === m.text.normalize("NFC"), + ) + ) { + existing.push(m.text); + } + } + const uniqueTexts = [...normalizedToVerbatims.keys()].sort(); + const displayTexts = uniqueTexts.map( + (t) => normalizedToVerbatims.get(t) ?? [], + ); + + const anonymizeCount = mentions.filter( + (m) => m.attrs.aymurai_anonymize !== false, + ).length; + const anonymize = anonymizeCount >= mentions.length / 2; + + const suffixIndex = suffixByCanonicalId.get(canonicalId) ?? 1; + + return { + canonicalId, + renderBase, + suffixIndex, + renderToken: `${renderBase}_${suffixIndex}`, + mentions, + uniqueTexts, + displayTexts, + firstAppearance, + anonymize, + isDuplicate: false, + duplicateOf: null, + }; + }, + ); + + // Duplicate / overlap detection: any two groups under the same renderBase + // that share at least one normalised mention text are flagged as merge + // candidates. + // + // The detection key uses a SORTED-TOKEN fingerprint of the normalised text + // so that name variants differing only in token order + // (e.g. "Pérez, Laura Beatriz" vs "Laura Beatriz Pérez") are recognised as + // the same alias and trigger a merge suggestion. + // + // Primary rule: the group with MORE unique texts is primary (it's more + // "established"). Ties are broken by first-appearance order (already the + // iteration order). When a later group turns out to have more texts than + // the registered primary, they swap roles. + + /** Sorted-token fingerprint — collapses token-order variants of a name. */ + const sortedFingerprint = (normText: string): string => + [...new Set(normText.split(" "))].sort().join(" "); + + const textToGroup = new Map(); // key → primary canonicalId + const groupById = new Map(groups.map((g) => [g.canonicalId, g])); + + for (const group of groups) { + let conflictPrimaryId: string | null = null; + + for (const normText of group.uniqueTexts) { + const key = `${group.renderBase}\x01${sortedFingerprint(normText)}`; + const existing = textToGroup.get(key); + if (existing && existing !== group.canonicalId) { + conflictPrimaryId = existing; + break; + } + } + + if (conflictPrimaryId !== null) { + const primaryGroup = groupById.get(conflictPrimaryId); + if (!primaryGroup) continue; + + if (group.uniqueTexts.length > primaryGroup.uniqueTexts.length) { + // Current group has more texts → it becomes the new primary; + // old primary is demoted to duplicate. + primaryGroup.isDuplicate = true; + primaryGroup.duplicateOf = group.canonicalId; + // Re-register all previously-registered keys under new primary. + for (const normText of primaryGroup.uniqueTexts) { + textToGroup.set( + `${primaryGroup.renderBase}\x01${sortedFingerprint(normText)}`, + group.canonicalId, + ); + } + for (const normText of group.uniqueTexts) { + textToGroup.set( + `${group.renderBase}\x01${sortedFingerprint(normText)}`, + group.canonicalId, + ); + } + } else { + // Existing primary has equal or more texts → current is duplicate. + group.isDuplicate = true; + group.duplicateOf = conflictPrimaryId; + // Register any new texts under the existing primary. + for (const normText of group.uniqueTexts) { + const key = `${group.renderBase}\x01${sortedFingerprint(normText)}`; + if (!textToGroup.has(key)) textToGroup.set(key, conflictPrimaryId); + } + } + } else { + // No conflict — register all own texts as primary. + for (const normText of group.uniqueTexts) { + textToGroup.set( + `${group.renderBase}\x01${sortedFingerprint(normText)}`, + group.canonicalId, + ); + } + } + } + + return groups; + }, [files]); +} diff --git a/frontend/src/renderer/src/hooks/useFileParse.ts b/frontend/src/renderer/src/hooks/useFileParse.ts new file mode 100644 index 00000000..a45da857 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useFileParse.ts @@ -0,0 +1,69 @@ +import { useEffect, useState } from "react"; + +import { useFileDispatch } from "@/hooks"; +import { addParagraphs } from "@/reducers/file/actions"; +import { fileParser } from "@/services/aymurai/queries"; +import type { DocFile } from "@/types/file"; +import { useQueries, useQueryClient } from "@tanstack/react-query"; + +import type { PredictStatus } from "./usePredict"; + +export function useFileParse( + files: DocFile[], +): Record void }> { + const dispatch = useFileDispatch(); + const queryClient = useQueryClient(); + const [abortedFiles, setAbortedFiles] = useState>(new Set()); + + const queries = useQueries({ + queries: files.map((file) => ({ + ...fileParser(file.data), + enabled: !abortedFiles.has(file.data.name), + })), + }); + + useEffect(() => { + queries.forEach((query, fileIndex) => { + const file = files[fileIndex]; + // Use the Redux state as the guard: if paragraphs are already set, skip. + // This correctly re-dispatches when the same file is reloaded after a replace. + if (!query.isSuccess || !query.data || file.paragraphs !== undefined) + return; + dispatch( + addParagraphs( + query.data.document.map((p, paragraphIndex) => ({ + value: p, + document_id: query.data.document_id, + id: `${query.data.document_id}:${paragraphIndex}`, + })), + file.data.name, + ), + ); + }); + }); + + const result: Record void }> = + {}; + files.forEach((file, i) => { + const q = queries[i]; + const fileName = file.data.name; + const isAborted = abortedFiles.has(fileName); + result[fileName] = { + status: isAborted + ? "stopped" + : q.isError + ? "error" + : q.isSuccess + ? "completed" + : "processing", + abort: () => { + queryClient.cancelQueries({ + queryKey: ["file-parser", fileName], + exact: false, + }); + setAbortedFiles((prev) => new Set([...prev, fileName])); + }, + }; + }); + return result; +} diff --git a/frontend/src/renderer/src/hooks/useFiles.ts b/frontend/src/renderer/src/hooks/useFiles.ts new file mode 100644 index 00000000..280012e0 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useFiles.ts @@ -0,0 +1,17 @@ +import { useContext } from "react"; + +import { FileContext, FileDispatchContext } from "@/context/File"; + +/** + * Hook used to view the files in the state + */ +export function useFiles() { + return useContext(FileContext); +} + +/** + * Hook used to dispatch actions that modify the state + */ +export function useFileDispatch() { + return useContext(FileDispatchContext); +} diff --git a/frontend/src/renderer/src/hooks/useForm/index.ts b/frontend/src/renderer/src/hooks/useForm/index.ts new file mode 100644 index 00000000..5dacc409 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useForm/index.ts @@ -0,0 +1,83 @@ +import { LabelDecisiones, type LabelType } from "@/types/aymurai"; +import nArray from "@/utils/nArray"; +import { useRef, useState } from "react"; +import type { FormData, RegisterFunction, SubmitFunction } from "./types"; + +/** + * Creates a group of component references and a function to handle + * @param initialDecisiones Initial amount of decisiones + * @returns `register()` and `submit()` functions + */ +export default function useForm(initialDecisiones = 1) { + // Create an n-array of empty objects + const refs = useRef({ DECISIONES: nArray(initialDecisiones, {}) }); + // State containing decisionAmounts. This is done in this way to make React perform a re-render + // in case we want to add a decision + const [decisionAmount, setDecisionAmount] = useState(initialDecisiones); + + /** + * Adds a reference to a components value to the 'state' + * @param name Name of the reference + * @returns A `(ref) => void` function that should be applied to the `ref={...}` prop of a component + */ + const register: RegisterFunction = (name, decision) => (ref) => { + if (ref) { + // We have to alter the DECISIONES array + if (name in LabelDecisiones) { + // In case we already have an array, modify the label + if (refs.current.DECISIONES) { + const arr = refs.current.DECISIONES; + const i = decision ?? 0; + + arr[i][name as LabelDecisiones] = ref.value; + } else { + // We have no array, create it + refs.current.DECISIONES = [{ [name]: ref.value }]; + } + } else { + // Just modify the label in the object + refs.current[name as LabelType] = ref.value; + } + } + }; + + /** + * Collects all the data from the referrences and creates a submit function with them + * @param callback Callback that is called with all the data collected from the references as an argument + * @returns A `(event) => void` function that should be applied to the `onSubmit={...}` prop of a form + */ + const submit = (callback: SubmitFunction) => () => { + callback(refs.current); + }; + + const addDecision = () => { + const arr = refs.current.DECISIONES; + + if (arr) { + setDecisionAmount((dec) => dec + 1); + refs.current.DECISIONES = [...arr, {}]; + } else { + setDecisionAmount(1); + refs.current.DECISIONES = [{}]; + } + }; + + const getDecisionValue = (n: number) => (field: LabelDecisiones) => { + const arr = refs.current.DECISIONES; + + if (arr && n in arr && field in arr[n]) { + return arr[n][field]; + } + return ""; + }; + + return { + register, + submit, + addDecision, + decisionAmount, + getDecisionValue, + }; +} + +export * from "./types"; diff --git a/frontend/src/renderer/src/hooks/useForm/types.ts b/frontend/src/renderer/src/hooks/useForm/types.ts new file mode 100644 index 00000000..3832a118 --- /dev/null +++ b/frontend/src/renderer/src/hooks/useForm/types.ts @@ -0,0 +1,36 @@ +import type { + AllLabels, + AllLabelsWithSufix, + LabelDecisiones, + LabelType, +} from "@/types/aymurai"; + +/** + * Any value the form can take + */ +export type FormValue = string | boolean | undefined; +/** + * Data structure of the whole form + */ +export type FormData = Partial< + Record & { + DECISIONES: Partial>[]; + } +>; +/** + * Flat form of the `FormData` structure + */ +export type FlatFormData = Partial< + Record +>; + +/** + * Type of the reference for any componente + */ +export type ComponentRef = { value: string | boolean | undefined } | null; + +export type RegisterFunction = ( + name: AllLabels | AllLabelsWithSufix, + decision?: number, +) => (ref: ComponentRef) => void; +export type SubmitFunction = (data: FormData) => void; diff --git a/frontend/src/renderer/src/hooks/usePredict.ts b/frontend/src/renderer/src/hooks/usePredict.ts new file mode 100644 index 00000000..9ab803e2 --- /dev/null +++ b/frontend/src/renderer/src/hooks/usePredict.ts @@ -0,0 +1,171 @@ +import { useRef, useState } from "react"; + +import { useFileDispatch } from "@/hooks"; +import { addPredictions, removePredictions } from "@/reducers/file/actions"; +import predict from "@/services/aymurai/predict"; +import { predictParagraph } from "@/services/aymurai/queries"; +import getStoredValidation from "@/services/aymurai/validation"; +import type { Workflows } from "@/types/aymurai"; +import type { DocFile } from "@/types/file"; +import { useQueries, useQueryClient } from "@tanstack/react-query"; + +// Limit concurrent in-flight predict requests to avoid browser connection exhaustion +const MAX_CONCURRENT = 100; +const semaphore = (() => { + let running = 0; + const queue: Array<() => void> = []; + return { + acquire(): Promise { + return new Promise((resolve) => { + if (running < MAX_CONCURRENT) { + running++; + resolve(); + } else queue.push(resolve); + }); + }, + release() { + const next = queue.shift(); + if (next) next(); + else running = Math.max(0, running - 1); + }, + }; +})(); + +export type PredictStatus = "processing" | "error" | "stopped" | "completed"; + +type FilePredict = { + progress: number; + status: PredictStatus; + abort: () => void; + /** + * True when every paragraph in this file was loaded from the backend's stored + * validation (`anonymization_paragraph.validation`) instead of running the + * model. When true, the disambiguation step must be skipped. + */ + fromValidation: boolean; +}; + +/** + * Runs predictions for all paragraphs across all given files in parallel via + * React Query. Dispatches results to the Redux file store as each paragraph + * resolves. Returns a per-filename map of `{ progress, status, abort }`. + * + * Abort uses `queryClient.cancelQueries` so there are no manual AbortController + * references to track or clean up. + */ +export function usePredict( + files: DocFile[], + workflow: Workflows, +): Record { + const dispatch = useFileDispatch(); + const queryClient = useQueryClient(); + const [abortedFiles, setAbortedFiles] = useState>(new Set()); + + // Paragraph IDs whose labels were loaded from DB validation (not from predict). + // Used to gate the disambiguation step. + const fromValidationRef = useRef>(new Set()); + + // Flatten all (file, paragraph) pairs — one React Query entry each + const pairs = files.flatMap((file) => + (file.paragraphs ?? []).map((paragraph) => ({ file, paragraph })), + ); + + const queries = useQueries({ + queries: pairs.map(({ file, paragraph }) => ({ + // Use the factory for the query key and shared defaults (staleTime, retry) + ...predictParagraph(paragraph, workflow, file.data.name), + enabled: !abortedFiles.has(file.data.name), + // Override queryFn to dispatch to Redux as each paragraph arrives. + // Defined inline to avoid TS's `queryFn?: ...` optionality on the factory type. + queryFn: async ({ signal }: { signal?: AbortSignal }) => { + const controller = new AbortController(); + signal?.addEventListener("abort", () => controller.abort()); + await semaphore.acquire(); + try { + if (!controller.signal.aborted) { + // For anonymizer: check stored DB validation before running the model. + // null → no stored validation; fall through to predict + // [] → explicitly validated with no entities; respect the empty state + // [...] → stored manual annotations; restore them + if (workflow === "anonymizer") { + const stored = await getStoredValidation(paragraph, controller); + if (stored !== null) { + console.debug( + `[predict] Restored ${stored.length} labels from DB validation`, + { paragraphId: paragraph.id.slice(0, 60) }, + ); + fromValidationRef.current.add(paragraph.id); + dispatch(addPredictions(file.data.name, stored)); + return stored; + } + console.debug("[predict] No stored validation — running model predict", { + paragraphId: paragraph.id.slice(0, 60), + }); + } + const predictions = await predict(paragraph, controller, workflow); + dispatch(addPredictions(file.data.name, predictions)); + return predictions; + } + return []; + } finally { + semaphore.release(); + } + }, + })), + }); + + // Build per-file { progress, status, abort } from the flat query results + const result: Record = {}; + + for (const file of files) { + const fileName = file.data.name; + const total = (file.paragraphs ?? []).length; + + // Collect only the queries that belong to this file + const fileQueries = pairs.reduce<(typeof queries)[number][]>( + (acc, pair, i) => { + if (pair.file.data.name === fileName) acc.push(queries[i]); + return acc; + }, + [], + ); + + const successCount = fileQueries.filter((q) => q.isSuccess).length; + const errorCount = fileQueries.filter((q) => q.isError).length; + const progress = total > 0 ? successCount / total : 0; + const isAborted = abortedFiles.has(fileName); + + const status: PredictStatus = isAborted + ? "stopped" + : errorCount > 0 + ? "error" + : total === 0 || successCount === total + ? "completed" + : "processing"; + + // A file is "from validation" when every one of its paragraphs returned + // stored annotations from the backend (none went through model predict). + const fromValidation = + workflow === "anonymizer" && + total > 0 && + (file.paragraphs ?? []).every((p) => fromValidationRef.current.has(p.id)); + + result[fileName] = { + progress, + status, + fromValidation, + abort: () => { + // Cancel all in-flight paragraph queries for this file via RQ's own + // cancellation mechanism — no manual controller refs to clean up + queryClient.cancelQueries({ + queryKey: ["predict", workflow, fileName], + exact: false, + }); + dispatch(removePredictions(fileName)); + setAbortedFiles((prev) => new Set([...prev, fileName])); + }, + }; + } + + return result; +} diff --git a/frontend/src/renderer/src/index.css b/frontend/src/renderer/src/index.css new file mode 100644 index 00000000..e27a23b7 --- /dev/null +++ b/frontend/src/renderer/src/index.css @@ -0,0 +1 @@ +@layer reset, base, tokens, recipes, utilities; diff --git a/frontend/src/renderer/src/layout/home.tsx b/frontend/src/renderer/src/layout/home.tsx new file mode 100644 index 00000000..563d8f80 --- /dev/null +++ b/frontend/src/renderer/src/layout/home.tsx @@ -0,0 +1,50 @@ +import BuiltBy from "@/components/brand/built-by"; +import { css } from "@/styled/css"; + +const background = css({ + display: "flex", + + bg: "bg.primary-highlight", + height: "screen", + width: "screen", + + p: "8", +}); + +const inner = css({ + display: "flex", + flexDirection: "column", + alignItems: "center", + + pos: "relative", + + bg: "bg.secondary", + border: "primary", + + width: "full", + height: "full", + + overflowY: "auto", + pb: "8", +}); + +const childrenArea = css({ + flex: "1", + display: "flex", + alignItems: "center", + justifyContent: "center", +}); + +interface HomeLayoutProps { + children?: React.ReactNode; +} +export default function HomeLayout({ children }: HomeLayoutProps) { + return ( +
+
+
{children}
+ +
+
+ ); +} diff --git a/frontend/src/renderer/src/layout/loading.tsx b/frontend/src/renderer/src/layout/loading.tsx new file mode 100644 index 00000000..1f1a3d39 --- /dev/null +++ b/frontend/src/renderer/src/layout/loading.tsx @@ -0,0 +1,31 @@ +import { css } from "@/styled/css"; +import { stack } from "@/styled/patterns"; + +const background = css( + stack.raw({ + align: "center", + justify: "center", + }), + { + width: "screen", + height: "screen", + + bgGradient: "primary", + }, +); +const image = css({ + animation: "pulse", +}); + +export default function Loading() { + return ( +
+ AymurAI iso white +
+ ); +} diff --git a/frontend/src/renderer/src/layout/section-title.tsx b/frontend/src/renderer/src/layout/section-title.tsx new file mode 100644 index 00000000..26731e61 --- /dev/null +++ b/frontend/src/renderer/src/layout/section-title.tsx @@ -0,0 +1,13 @@ +import { styled } from "@/styled/jsx"; + +interface SectionTitleProps { + children: React.ReactNode; + className?: string; +} +export function SectionTitle({ children, className }: SectionTitleProps) { + return ( + + {children} + + ); +} diff --git a/frontend/src/renderer/src/main.tsx b/frontend/src/renderer/src/main.tsx new file mode 100644 index 00000000..9f8c88be --- /dev/null +++ b/frontend/src/renderer/src/main.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; + +import "./index.css"; +import "./constants/i18n"; +import App from "./app"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/frontend/src/renderer/src/reducers/file/actions.ts b/frontend/src/renderer/src/reducers/file/actions.ts new file mode 100644 index 00000000..331a2edb --- /dev/null +++ b/frontend/src/renderer/src/reducers/file/actions.ts @@ -0,0 +1,454 @@ +import type { FormData } from "@/hooks/useForm"; +import type { + AllLabels, + AllLabelsWithSufix, + PredictLabel, +} from "@/types/aymurai"; +import type { Paragraph } from "@/types/file"; + +/** + * List of action types. + */ +export enum ActionTypes { + ADD = "ADD", + ADD_PREDICTIONS = "ADD_PREDICTIONS", + ADD_PARAGRAPHS = "ADD_PARAGRAPHS", + FILTER_UNSELECTED = "FILTER_UNSELECTED", + FILTER_UNPROCESSED = "FILTER_UNPROCESSED", + TOGGLE_SELECTED = "TOGGLE_SELECTED", + REMOVE_ALL_PREDICTIONS = "REMOVE_ALL_PREDICTIONS", + REMOVE_PREDICTIONS = "REMOVE_PREDICTIONS", + REMOVE_ALL_FILES = "REMOVE_ALL_FILES", + REMOVE_FILE = "REMOVE_FILE", + REPLACE_FILE = "REPLACE_FILE", + VALIDATE = "VALIDATE", + APPEND_VALIDATION = "APPEND_VALIDATION", + APPEND_PREDICTION = "APPEND_PREDICTION", + REMOVE_PREDICTION = "REMOVE_PREDICTION", + REMOVE_PREDICTIONS_BY_TEXT = "REMOVE_PREDICTIONS_BY_TEXT", + UPDATE_PREDICTION_LABEL = "UPDATE_PREDICTION_LABEL", + UPDATE_PREDICTIONS_BY_TEXT = "UPDATE_PREDICTIONS_BY_TEXT", + REMOVE_PREDICTIONS_BY_CANONICAL_ID = "REMOVE_PREDICTIONS_BY_CANONICAL_ID", + REMOVE_PREDICTION_VALUE_BY_CANONICAL_ID = "REMOVE_PREDICTION_VALUE_BY_CANONICAL_ID", + UPDATE_PREDICTIONS_BY_CANONICAL_ID = "UPDATE_PREDICTIONS_BY_CANONICAL_ID", + MOVE_MENTION_TO_GROUP = "MOVE_MENTION_TO_GROUP", + MERGE_GROUPS = "MERGE_GROUPS", +} + +/** + * Generic action + */ +type Action> = { + type: Type; + payload: Payload; +}; + +export type ToggleSelectedAction = Action< + ActionTypes.TOGGLE_SELECTED, + { fileName: string } +>; +/** + * Toggles the `selected` property of the given file + * @param fileName Name of the file to be toggled + */ +export function toggleSelected(fileName: string): ToggleSelectedAction { + return { + type: ActionTypes.TOGGLE_SELECTED, + payload: { fileName }, + }; +} + +export type RemoveAllPredictionsAction = + Action; +/** + * Removes all the predictions for all the files in the state + */ +export function removeAllPredictions(): RemoveAllPredictionsAction { + return { + type: ActionTypes.REMOVE_ALL_PREDICTIONS, + payload: {}, + }; +} + +export type FilterUnselectedAction = Action; +/** + * Removes files from the state whose `selected` property is set on `false` + */ +export function filterUnselected(): FilterUnselectedAction { + return { + type: ActionTypes.FILTER_UNSELECTED, + payload: {}, + }; +} + +export type AddFilesAction = Action; +/** + * Adds a new set of `File[]` to the end of the state + * @param newFiles `File[]` to be added to the state + */ +export function addFiles(newFiles: File[]): AddFilesAction { + return { + type: ActionTypes.ADD, + payload: { newFiles }, + }; +} + +export type RemoveAllFilesAction = Action; +/** + * Removes all the files from the state + */ +export function removeAllFiles(): RemoveAllFilesAction { + return { + type: ActionTypes.REMOVE_ALL_FILES, + payload: {}, + }; +} + +export type RemoveFileAction = Action< + ActionTypes.REMOVE_FILE, + { fileName: string } +>; +/** + * Removes a single file from the array + */ +export function removeFile(fileName: string): RemoveFileAction { + return { + type: ActionTypes.REMOVE_FILE, + payload: { fileName }, + }; +} + +export type AddPredictionsAction = Action< + ActionTypes.ADD_PREDICTIONS, + { fileName: string; predictions: PredictLabel[] } +>; +/** + * Adds predictions to a specific file + * @param fileName File to be modified + * @param predictions `PredictLabel[]` received from the `/predict` API request + */ +export function addPredictions( + fileName: string, + predictions: PredictLabel[], +): AddPredictionsAction { + return { + type: ActionTypes.ADD_PREDICTIONS, + payload: { fileName, predictions }, + }; +} + +export type RemovePredictionsAction = Action< + ActionTypes.REMOVE_PREDICTIONS, + { fileName: string } +>; +/** + * Removes all the predictions from a given file + */ +export function removePredictions(fileName: string): RemovePredictionsAction { + return { + type: ActionTypes.REMOVE_PREDICTIONS, + payload: { fileName }, + }; +} + +export type ReplaceFileAction = Action< + ActionTypes.REPLACE_FILE, + { fileName: string; file: File } +>; +/** + * Replaces a file data with the given `fileName` + * @param fileName File to be replaced on the state + * @param file File data to be inserted in place of the given `fileName` + */ +export function replaceFile(fileName: string, file: File): ReplaceFileAction { + return { + type: ActionTypes.REPLACE_FILE, + payload: { fileName, file }, + }; +} + +export type ValidateAction = Action; +/** + * Sets the validated status to the given file to `true` + * @param fileName Name of the file to be toggled + */ +export function validate(fileName: string): ValidateAction { + return { + type: ActionTypes.VALIDATE, + payload: { fileName }, + }; +} + +export type AppendValidationAction = Action< + ActionTypes.APPEND_VALIDATION, + { fileName: string; validation: FormData } +>; +/** + * Builds the `validation` progressively + * @param fileName Name of the file to be modified + * @param validation Validation object to be appended + */ +export function appendValidation( + fileName: string, + validation: FormData, +): AppendValidationAction { + return { + type: ActionTypes.APPEND_VALIDATION, + payload: { fileName, validation }, + }; +} + +export type FilterUnprocessedAction = Action; +/** + * Removes files from the state whose `predictions` property is set on `undefined` + */ +export function filterUnprocessed(): FilterUnprocessedAction { + return { + type: ActionTypes.FILTER_UNPROCESSED, + payload: {}, + }; +} + +export type AddParagraphsAction = Action< + ActionTypes.ADD_PARAGRAPHS, + { + paragraphs: Paragraph[]; + fileName: string; + } +>; +/** + * Adds paragraphs to the state from the endpoint `/document-extract` + * @param paragraphs List of paragraphs to be added + * @param fileName Name of the file to be modified + */ +export function addParagraphs( + paragraphs: Paragraph[], + fileName: string, +): AddParagraphsAction { + return { + type: ActionTypes.ADD_PARAGRAPHS, + payload: { paragraphs, fileName }, + }; +} + +export type AppendPrediction = Action< + ActionTypes.APPEND_PREDICTION, + { + prediction: PredictLabel; + fileName: string; + } +>; +/** + * Appends a new user made prediction to the file + * @param prediction User made prediction on the file. Should only be used when anonymizing. + * @param fileName Name of the file to be modified + */ +export function appendPrediction( + fileName: string, + prediction: PredictLabel, +): AppendPrediction { + return { + type: ActionTypes.APPEND_PREDICTION, + payload: { prediction, fileName }, + }; +} + +export type RemovePrediction = Action< + ActionTypes.REMOVE_PREDICTION, + { + prediction: PredictLabel; + fileName: string; + } +>; +/** + * Removes a user made prediction from the file + * @param predictionId ID of the prediction to be removed + * @param fileName Name of the file to be modified + */ +export function removePrediction( + fileName: string, + prediction: PredictLabel, +): RemovePrediction { + return { + type: ActionTypes.REMOVE_PREDICTION, + payload: { prediction, fileName }, + }; +} + +export type RemovePredictionsByText = Action< + ActionTypes.REMOVE_PREDICTIONS_BY_TEXT, + { + text: string; + fileName: string; + } +>; +/** + * Removes all predictions with the same text from the file + * @param text Text of the predictions to be removed + * @param fileName Name of the file to be modified + */ +export function removePredictionsByText( + fileName: string, + text: string, +): RemovePredictionsByText { + return { + type: ActionTypes.REMOVE_PREDICTIONS_BY_TEXT, + payload: { text, fileName }, + }; +} + +export type UpdatePredictionLabel = Action< + ActionTypes.UPDATE_PREDICTION_LABEL, + { + fileName: string; + prediction: PredictLabel; + newLabel: string; + } +>; + +/** + * Updates the label of a prediction in a file + * @param fileName Name of the file containing the prediction + * @param prediction The prediction to update + * @param newLabel The new label to set + */ +export function updatePredictionLabel( + fileName: string, + prediction: PredictLabel, + newLabel: string, +): UpdatePredictionLabel { + return { + type: ActionTypes.UPDATE_PREDICTION_LABEL, + payload: { fileName, prediction, newLabel }, + }; +} + +export type UpdatePredictionsByText = Action< + ActionTypes.UPDATE_PREDICTIONS_BY_TEXT, + { + text: string; + fileName: string; + newLabel: AllLabels | AllLabelsWithSufix; + canonicalId?: string; + } +>; + +/** + * Updates all predictions with the same text to a new label + * @param fileName Name of the file containing the predictions + * @param text Text of the predictions to update + * @param newLabel The new label to set + */ +export function updatePredictionsByText( + fileName: string, + text: string, + newLabel: AllLabels | AllLabelsWithSufix, + canonicalId?: string, +): UpdatePredictionsByText { + return { + type: ActionTypes.UPDATE_PREDICTIONS_BY_TEXT, + payload: { fileName, text, newLabel, canonicalId }, + }; +} + +export type RemovePredictionsByCanonicalId = Action< + ActionTypes.REMOVE_PREDICTIONS_BY_CANONICAL_ID, + { canonicalId: string } +>; +/** + * Removes all predictions across all files that share the given canonical_entity_id + * @param canonicalId The canonical entity id to match + */ +export function removePredictionsByCanonicalId( + canonicalId: string, +): RemovePredictionsByCanonicalId { + return { + type: ActionTypes.REMOVE_PREDICTIONS_BY_CANONICAL_ID, + payload: { canonicalId }, + }; +} + +export type RemovePredictionValueByCanonicalId = Action< + ActionTypes.REMOVE_PREDICTION_VALUE_BY_CANONICAL_ID, + { canonicalId: string; value: string } +>; +/** + * Removes predictions across all files matching the given canonical_entity_id and text value + * @param canonicalId The canonical entity id to match + * @param value The prediction text to remove + */ +export function removePredictionValueByCanonicalId( + canonicalId: string, + value: string, +): RemovePredictionValueByCanonicalId { + return { + type: ActionTypes.REMOVE_PREDICTION_VALUE_BY_CANONICAL_ID, + payload: { canonicalId, value }, + }; +} + +export type UpdatePredictionsByCanonicalId = Action< + ActionTypes.UPDATE_PREDICTIONS_BY_CANONICAL_ID, + { canonicalId: string; newLabel: AllLabels | AllLabelsWithSufix } +>; +/** + * Updates the label of all predictions across all files sharing the given canonical_entity_id + * @param canonicalId The canonical entity id to match + * @param newLabel The new label to set + */ +export function updatePredictionsByCanonicalId( + canonicalId: string, + newLabel: AllLabels | AllLabelsWithSufix, +): UpdatePredictionsByCanonicalId { + return { + type: ActionTypes.UPDATE_PREDICTIONS_BY_CANONICAL_ID, + payload: { canonicalId, newLabel }, + }; +} + +export type MoveMentionToGroupAction = Action< + ActionTypes.MOVE_MENTION_TO_GROUP, + { + mentionId: string; + targetCanonicalId: string; + targetLabel: AllLabels | AllLabelsWithSufix; + } +>; +/** + * Moves a single mention (identified by its stable mentionId) to a different + * canonical entity group, updating both `canonical_entity_id` and `aymurai_label`. + */ +export function moveMentionToGroup( + mentionId: string, + targetCanonicalId: string, + targetLabel: AllLabels | AllLabelsWithSufix, +): MoveMentionToGroupAction { + return { + type: ActionTypes.MOVE_MENTION_TO_GROUP, + payload: { mentionId, targetCanonicalId, targetLabel }, + }; +} + +export type MergeGroupsAction = Action< + ActionTypes.MERGE_GROUPS, + { + sourceCanonicalId: string; + targetCanonicalId: string; + targetLabel: AllLabels | AllLabelsWithSufix; + } +>; +/** + * Moves all mentions from `sourceCanonicalId` into `targetCanonicalId`, + * updating their label to `targetLabel`. The source group disappears naturally + * once it has no more mentions. + */ +export function mergeGroups( + sourceCanonicalId: string, + targetCanonicalId: string, + targetLabel: AllLabels | AllLabelsWithSufix, +): MergeGroupsAction { + return { + type: ActionTypes.MERGE_GROUPS, + payload: { sourceCanonicalId, targetCanonicalId, targetLabel }, + }; +} diff --git a/frontend/src/renderer/src/reducers/file/index.ts b/frontend/src/renderer/src/reducers/file/index.ts new file mode 100644 index 00000000..d92c80bb --- /dev/null +++ b/frontend/src/renderer/src/reducers/file/index.ts @@ -0,0 +1,415 @@ +import { + ActionTypes, + type AddFilesAction, + type AddParagraphsAction, + type AddPredictionsAction, + type AppendPrediction, + type AppendValidationAction, + type FilterUnprocessedAction, + type FilterUnselectedAction, + type MergeGroupsAction, + type MoveMentionToGroupAction, + type RemoveAllFilesAction, + type RemoveAllPredictionsAction, + type RemoveFileAction, + type RemovePrediction, + type RemovePredictionValueByCanonicalId, + type RemovePredictionsAction, + type RemovePredictionsByCanonicalId, + type RemovePredictionsByText, + type ReplaceFileAction, + type ToggleSelectedAction, + type UpdatePredictionLabel, + type UpdatePredictionsByCanonicalId, + type UpdatePredictionsByText, + type ValidateAction, +} from "./actions"; +import { + addFiles, + comparePrediction, + replaceFile, + updateFromState, +} from "./utils"; + +import type { AllLabels, AllLabelsWithSufix } from "@/types/aymurai"; +import type { DocFile } from "@/types/file"; +import { + normalizeEntityText, + stripEntityLabelSuffix, +} from "@/utils/anonymizer/entity-similarity"; + +type State = DocFile[]; + +export type Action = + | AddFilesAction + | AddPredictionsAction + | ToggleSelectedAction + | RemoveAllPredictionsAction + | RemoveAllFilesAction + | RemoveFileAction + | RemovePredictionsAction + | ReplaceFileAction + | FilterUnselectedAction + | ValidateAction + | AppendValidationAction + | FilterUnprocessedAction + | AddParagraphsAction + | AppendPrediction + | RemovePrediction + | RemovePredictionsByText + | UpdatePredictionLabel + | UpdatePredictionsByText + | RemovePredictionsByCanonicalId + | RemovePredictionValueByCanonicalId + | UpdatePredictionsByCanonicalId + | MoveMentionToGroupAction + | MergeGroupsAction; + +/** + * Reducer function for `DocFile[]` state + * @param state Current state + * @param action Action to perform + * @returns A new state + */ +export default function reducer(state: State, action: Action): State { + const { type, payload } = action; + + // Update/replace/delete utility function + const update = updateFromState(state); + + switch (type) { + // ---------------- + // ADD + // ---------------- + case ActionTypes.ADD: { + const { newFiles } = payload; + + // Performs some checkings on the file + return addFiles(newFiles, state); + } + // ---------------- + // ADD PREDICTIONS + // ---------------- + case ActionTypes.ADD_PREDICTIONS: { + const { fileName, predictions } = payload; + + return update(fileName, (current) => ({ + ...current, + // Appends the predictions to the already created array + predictions: [...(current.predictions ?? []), ...predictions], + })); + } + // ---------------- + // TOGGLE SELECTED + // ---------------- + case ActionTypes.TOGGLE_SELECTED: { + const { fileName } = payload; + + return update(fileName, (current) => ({ + ...current, + selected: !current.selected, + })); + } + // ---------------- + // REMOVE ALL PREDICTIONS + // ---------------- + case ActionTypes.REMOVE_ALL_PREDICTIONS: { + return state.map((file) => ({ ...file, predictions: undefined })); + } + // ---------------- + // REMOVE PREDICTIONS + // ---------------- + case ActionTypes.REMOVE_PREDICTIONS: { + const { fileName } = payload; + + return update(fileName, (current) => ({ + ...current, + predictions: undefined, + })); + } + // ---------------- + // REMOVE ALL FILES + // ---------------- + case ActionTypes.REMOVE_ALL_FILES: { + return []; + } + case ActionTypes.REMOVE_FILE: { + const { fileName } = payload; + + return state.filter((file) => file.data.name !== fileName); + } + // ---------------- + // REPLACE FILE + // ---------------- + case ActionTypes.REPLACE_FILE: { + const { fileName, file } = payload; + return replaceFile(fileName, file, state); + } + // ---------------- + // FILTER UNSELECTED + // ---------------- + case ActionTypes.FILTER_UNSELECTED: { + return state.filter((file) => file.selected); + } + + // ---------------- + // FILTER UNSELECTED + // ---------------- + case ActionTypes.FILTER_UNPROCESSED: { + return state.filter((file) => file.predictions); + } + // ---------------- + // VALIDATE + // ---------------- + case ActionTypes.VALIDATE: { + const { fileName } = payload; + return update(fileName, (cur) => ({ ...cur, validated: true })); + } + // ---------------- + // APPEND VALIDATION + // ---------------- + case ActionTypes.APPEND_VALIDATION: { + const { fileName, validation } = payload; + + return update(fileName, (cur) => ({ + ...cur, + // Append to the already created object + validationObject: { ...cur.validationObject, ...validation }, + })); + } + + // ---------------- + // ADD PARAGRAPHS + // ---------------- + case ActionTypes.ADD_PARAGRAPHS: { + const { fileName, paragraphs } = payload; + + return update(fileName, (cur) => ({ + ...cur, + paragraphs: paragraphs, + })); + } + + // ---------------- + // APPEND PREDICTION + // ---------------- + case ActionTypes.APPEND_PREDICTION: { + const { fileName, prediction } = payload; + + return update(fileName, (cur) => ({ + ...cur, + predictions: [...(cur.predictions ?? []), prediction], + })); + } + + // ---------------- + // REMOVE PREDICTION + // ---------------- + case ActionTypes.REMOVE_PREDICTION: { + const { fileName, prediction } = payload; + + return update(fileName, (cur) => ({ + ...cur, + predictions: cur.predictions?.filter( + (p) => !comparePrediction(prediction, p), + ), + })); + } + + // ---------------- + // REMOVE PREDICTIONS BY TEXT + // ---------------- + case ActionTypes.REMOVE_PREDICTIONS_BY_TEXT: { + const { fileName, text } = payload; + const normalizedText = normalizeEntityText(text); + + return update(fileName, (cur) => ({ + ...cur, + predictions: cur.predictions?.filter( + (p) => normalizeEntityText(p.text) !== normalizedText, + ), + })); + } + + // ---------------- + // UPDATE PREDICTION LABEL + // ---------------- + case ActionTypes.UPDATE_PREDICTION_LABEL: { + const { fileName, prediction, newLabel } = action.payload; + return state.map((file) => { + if (file.data.name !== fileName) return file; + + return { + ...file, + predictions: file.predictions?.map((p) => { + const isSamePrediction = prediction.mentionId + ? p.mentionId === prediction.mentionId + : p.paragraphId === prediction.paragraphId && + p.text === prediction.text && + p.start_char === prediction.start_char && + p.end_char === prediction.end_char; + + if (isSamePrediction) { + const currentBaseLabel = stripEntityLabelSuffix( + String(p.attrs.aymurai_label), + ); + const nextBaseLabel = stripEntityLabelSuffix(String(newLabel)); + const canonicalPatch = + currentBaseLabel === nextBaseLabel + ? {} + : { canonical_entity_id: crypto.randomUUID() }; + + return { + ...p, + attrs: { + ...p.attrs, + aymurai_label: newLabel as AllLabels | AllLabelsWithSufix, + ...canonicalPatch, + }, + }; + } + return p; + }), + }; + }); + } + + // ---------------- + // UPDATE PREDICTIONS BY TEXT + // ---------------- + case ActionTypes.UPDATE_PREDICTIONS_BY_TEXT: { + const { fileName, text, newLabel, canonicalId } = action.payload; + const normalizedText = normalizeEntityText(text); + const fallbackCanonicalId = crypto.randomUUID(); + const nextBaseLabel = stripEntityLabelSuffix(String(newLabel)); + + return state.map((file) => { + if (file.data.name !== fileName) return file; + + return { + ...file, + predictions: file.predictions?.map((p) => { + if (normalizeEntityText(p.text) === normalizedText) { + const currentBaseLabel = stripEntityLabelSuffix( + String(p.attrs.aymurai_label), + ); + const canonicalPatch = + canonicalId !== undefined + ? { canonical_entity_id: canonicalId } + : currentBaseLabel === nextBaseLabel + ? {} + : { canonical_entity_id: fallbackCanonicalId }; + + return { + ...p, + attrs: { + ...p.attrs, + aymurai_label: newLabel, + ...canonicalPatch, + }, + }; + } + return p; + }), + }; + }); + } + + // ---------------------------------------- + // REMOVE PREDICTIONS BY CANONICAL ID + // ---------------------------------------- + case ActionTypes.REMOVE_PREDICTIONS_BY_CANONICAL_ID: { + const { canonicalId } = action.payload; + return state.map((file) => ({ + ...file, + predictions: file.predictions?.filter( + (p) => p.attrs.canonical_entity_id !== canonicalId, + ), + })); + } + + // ------------------------------------------------ + // REMOVE PREDICTION VALUE BY CANONICAL ID + // ------------------------------------------------ + case ActionTypes.REMOVE_PREDICTION_VALUE_BY_CANONICAL_ID: { + const { canonicalId, value } = action.payload; + const norm = normalizeEntityText(value); + return state.map((file) => ({ + ...file, + predictions: file.predictions?.filter( + (p) => + !( + p.attrs.canonical_entity_id === canonicalId && + normalizeEntityText(p.text) === norm + ), + ), + })); + } + + // ---------------------------------------- + // UPDATE PREDICTIONS BY CANONICAL ID + // ---------------------------------------- + case ActionTypes.UPDATE_PREDICTIONS_BY_CANONICAL_ID: { + const { canonicalId, newLabel } = action.payload; + return state.map((file) => ({ + ...file, + predictions: file.predictions?.map((p) => + p.attrs.canonical_entity_id === canonicalId + ? { ...p, attrs: { ...p.attrs, aymurai_label: newLabel } } + : p, + ), + })); + } + + // ---------------------------------------- + // MOVE MENTION TO GROUP + // ---------------------------------------- + case ActionTypes.MOVE_MENTION_TO_GROUP: { + const { mentionId, targetCanonicalId, targetLabel } = action.payload; + return state.map((file) => ({ + ...file, + predictions: file.predictions?.map((p) => + p.mentionId === mentionId + ? { + ...p, + attrs: { + ...p.attrs, + canonical_entity_id: targetCanonicalId, + aymurai_label: targetLabel, + }, + } + : p, + ), + })); + } + + // ---------------------------------------- + // MERGE GROUPS + // ---------------------------------------- + case ActionTypes.MERGE_GROUPS: { + const { sourceCanonicalId, targetCanonicalId, targetLabel } = + action.payload; + return state.map((file) => ({ + ...file, + predictions: file.predictions?.map((p) => + p.attrs.canonical_entity_id === sourceCanonicalId + ? { + ...p, + attrs: { + ...p.attrs, + canonical_entity_id: targetCanonicalId, + aymurai_label: targetLabel, + }, + } + : p, + ), + })); + } + + // ---------------- + // ADD PARAGRAPHS + // ---------------- + default: + return state; + } +} diff --git a/frontend/src/renderer/src/reducers/file/utils.ts b/frontend/src/renderer/src/reducers/file/utils.ts new file mode 100644 index 00000000..22d7e6b0 --- /dev/null +++ b/frontend/src/renderer/src/reducers/file/utils.ts @@ -0,0 +1,101 @@ +import type { PredictLabel } from "@/types/aymurai"; +import type { DocFile } from "@/types/file"; +import { isAllowed, isAlreadyLoaded } from "@/utils/file"; + +/** + * Util function used along with `filter()` array function to mantain the types + * @param file File to check + * @returns `true` if the file is defined, `false` otherwise + */ +function removeUndefined(file: DocFile | undefined): file is DocFile { + return !!file; +} + +type ModifyFunction = (file: DocFile) => DocFile | undefined; +/** + * Updates/replaces/deletes a file content in the array. The function was created + * in a way that is not necessary to pass the `state` on every call. + * @param state Current `DocFile[]` state + * @param fileName Identifier of the file to replace + * @param modify Modify function that alters the content of the affected `DocFile` + * @returns A new function which accepts as parameters `fileName` and `modify` + */ +export function updateFromState(state: DocFile[]) { + return (fileName: string, modify: ModifyFunction) => { + // Modify/update the desired file + const newState = state.map((file) => { + if (file.data.name === fileName) return modify(file); + return file; + }); + + // Remove any `undefined` value inserted into the array + // This way we can remove a file from the array more easely + const filtered: DocFile[] = newState.filter(removeUndefined); + + return filtered; + }; +} + +/** + * Adds an array of `File` to the current files state + * @param files Files to be added + * @param state Current files state + * @returns A new state with the files added + */ +export function addFiles(files: File[], state: DocFile[]) { + // Check for whitelisted extensions and already loaded files + const filtered = files.filter( + (file) => isAllowed(file) && !isAlreadyLoaded(file.name, state), + ); + + // Add necessary fields to the object + const newFiles: DocFile[] = filtered.map((file) => ({ + data: file, + selected: true, + validationObject: {}, + })); + + return [...state, ...newFiles]; +} + +/** + * Replaces a file by the given name on the files state + * @param fileName Name of the file to be replaced + * @param newFile File to be inserted into the state + * @param state Current files state + * @returns A new array containing the file replaced + */ +export function replaceFile(fileName: string, newFile: File, state: DocFile[]) { + if (isAllowed(newFile) && !isAlreadyLoaded(newFile.name, state)) { + const replaced = state.map((file) => + file.data.name === fileName + ? { + data: newFile, + selected: true, + predictions: undefined, + validationObject: {}, + } + : file, + ); + return replaced; + } + return state; +} + +/** + * Compares two predictions to check if they are equal + * @param a First prediction to compare + * @param b Second prediction to compare + * @returns `true` if the predictions are equal, `false` otherwise. + */ +export function comparePrediction(a: PredictLabel, b: PredictLabel) { + if (a.mentionId && b.mentionId) return a.mentionId === b.mentionId; + + return ( + a.paragraphId === b.paragraphId && + a.start_char === b.start_char && + a.end_char === b.end_char && + a.text === b.text && + a.attrs.aymurai_label === b.attrs.aymurai_label + ); +} diff --git a/frontend/src/renderer/src/routeTree.gen.ts b/frontend/src/renderer/src/routeTree.gen.ts new file mode 100644 index 00000000..18dad6e9 --- /dev/null +++ b/frontend/src/renderer/src/routeTree.gen.ts @@ -0,0 +1,231 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as HomeIndexRouteImport } from './routes/home/index' +import { Route as HomeHostRouteImport } from './routes/home/host' +import { Route as HomeFeaturesRouteImport } from './routes/home/features' +import { Route as FeatureValidationRouteImport } from './routes/$feature/validation' +import { Route as FeatureProcessRouteImport } from './routes/$feature/process' +import { Route as FeaturePreviewRouteImport } from './routes/$feature/preview' +import { Route as FeatureOnboardingRouteImport } from './routes/$feature/onboarding' +import { Route as FeatureFinishRouteImport } from './routes/$feature/finish' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const HomeIndexRoute = HomeIndexRouteImport.update({ + id: '/home/', + path: '/home/', + getParentRoute: () => rootRouteImport, +} as any) +const HomeHostRoute = HomeHostRouteImport.update({ + id: '/home/host', + path: '/home/host', + getParentRoute: () => rootRouteImport, +} as any) +const HomeFeaturesRoute = HomeFeaturesRouteImport.update({ + id: '/home/features', + path: '/home/features', + getParentRoute: () => rootRouteImport, +} as any) +const FeatureValidationRoute = FeatureValidationRouteImport.update({ + id: '/$feature/validation', + path: '/$feature/validation', + getParentRoute: () => rootRouteImport, +} as any) +const FeatureProcessRoute = FeatureProcessRouteImport.update({ + id: '/$feature/process', + path: '/$feature/process', + getParentRoute: () => rootRouteImport, +} as any) +const FeaturePreviewRoute = FeaturePreviewRouteImport.update({ + id: '/$feature/preview', + path: '/$feature/preview', + getParentRoute: () => rootRouteImport, +} as any) +const FeatureOnboardingRoute = FeatureOnboardingRouteImport.update({ + id: '/$feature/onboarding', + path: '/$feature/onboarding', + getParentRoute: () => rootRouteImport, +} as any) +const FeatureFinishRoute = FeatureFinishRouteImport.update({ + id: '/$feature/finish', + path: '/$feature/finish', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/$feature/finish': typeof FeatureFinishRoute + '/$feature/onboarding': typeof FeatureOnboardingRoute + '/$feature/preview': typeof FeaturePreviewRoute + '/$feature/process': typeof FeatureProcessRoute + '/$feature/validation': typeof FeatureValidationRoute + '/home/features': typeof HomeFeaturesRoute + '/home/host': typeof HomeHostRoute + '/home': typeof HomeIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/$feature/finish': typeof FeatureFinishRoute + '/$feature/onboarding': typeof FeatureOnboardingRoute + '/$feature/preview': typeof FeaturePreviewRoute + '/$feature/process': typeof FeatureProcessRoute + '/$feature/validation': typeof FeatureValidationRoute + '/home/features': typeof HomeFeaturesRoute + '/home/host': typeof HomeHostRoute + '/home': typeof HomeIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/$feature/finish': typeof FeatureFinishRoute + '/$feature/onboarding': typeof FeatureOnboardingRoute + '/$feature/preview': typeof FeaturePreviewRoute + '/$feature/process': typeof FeatureProcessRoute + '/$feature/validation': typeof FeatureValidationRoute + '/home/features': typeof HomeFeaturesRoute + '/home/host': typeof HomeHostRoute + '/home/': typeof HomeIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/$feature/finish' + | '/$feature/onboarding' + | '/$feature/preview' + | '/$feature/process' + | '/$feature/validation' + | '/home/features' + | '/home/host' + | '/home' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/$feature/finish' + | '/$feature/onboarding' + | '/$feature/preview' + | '/$feature/process' + | '/$feature/validation' + | '/home/features' + | '/home/host' + | '/home' + id: + | '__root__' + | '/' + | '/$feature/finish' + | '/$feature/onboarding' + | '/$feature/preview' + | '/$feature/process' + | '/$feature/validation' + | '/home/features' + | '/home/host' + | '/home/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + FeatureFinishRoute: typeof FeatureFinishRoute + FeatureOnboardingRoute: typeof FeatureOnboardingRoute + FeaturePreviewRoute: typeof FeaturePreviewRoute + FeatureProcessRoute: typeof FeatureProcessRoute + FeatureValidationRoute: typeof FeatureValidationRoute + HomeFeaturesRoute: typeof HomeFeaturesRoute + HomeHostRoute: typeof HomeHostRoute + HomeIndexRoute: typeof HomeIndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/home/': { + id: '/home/' + path: '/home' + fullPath: '/home' + preLoaderRoute: typeof HomeIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/home/host': { + id: '/home/host' + path: '/home/host' + fullPath: '/home/host' + preLoaderRoute: typeof HomeHostRouteImport + parentRoute: typeof rootRouteImport + } + '/home/features': { + id: '/home/features' + path: '/home/features' + fullPath: '/home/features' + preLoaderRoute: typeof HomeFeaturesRouteImport + parentRoute: typeof rootRouteImport + } + '/$feature/validation': { + id: '/$feature/validation' + path: '/$feature/validation' + fullPath: '/$feature/validation' + preLoaderRoute: typeof FeatureValidationRouteImport + parentRoute: typeof rootRouteImport + } + '/$feature/process': { + id: '/$feature/process' + path: '/$feature/process' + fullPath: '/$feature/process' + preLoaderRoute: typeof FeatureProcessRouteImport + parentRoute: typeof rootRouteImport + } + '/$feature/preview': { + id: '/$feature/preview' + path: '/$feature/preview' + fullPath: '/$feature/preview' + preLoaderRoute: typeof FeaturePreviewRouteImport + parentRoute: typeof rootRouteImport + } + '/$feature/onboarding': { + id: '/$feature/onboarding' + path: '/$feature/onboarding' + fullPath: '/$feature/onboarding' + preLoaderRoute: typeof FeatureOnboardingRouteImport + parentRoute: typeof rootRouteImport + } + '/$feature/finish': { + id: '/$feature/finish' + path: '/$feature/finish' + fullPath: '/$feature/finish' + preLoaderRoute: typeof FeatureFinishRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + FeatureFinishRoute: FeatureFinishRoute, + FeatureOnboardingRoute: FeatureOnboardingRoute, + FeaturePreviewRoute: FeaturePreviewRoute, + FeatureProcessRoute: FeatureProcessRoute, + FeatureValidationRoute: FeatureValidationRoute, + HomeFeaturesRoute: HomeFeaturesRoute, + HomeHostRoute: HomeHostRoute, + HomeIndexRoute: HomeIndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/frontend/src/renderer/src/routes/$feature/finish.tsx b/frontend/src/renderer/src/routes/$feature/finish.tsx new file mode 100644 index 00000000..a79380e4 --- /dev/null +++ b/frontend/src/renderer/src/routes/$feature/finish.tsx @@ -0,0 +1,59 @@ +import FinishAnonymizer from "@/components/finish/finish-anonymizer"; +import FinishDataset from "@/components/finish/finish-dataset"; +import Stepper from "@/components/home/stepper"; +import Header from "@/components/layout/header"; +import HomeButton from "@/components/layout/home-button"; +import RequireFile from "@/features/RequireFile"; +import { useFileDispatch } from "@/hooks"; +import { Stack } from "@/styled/jsx"; +import { removeAllFiles } from "@/reducers/file/actions"; +import { FeatureFlowEnum, featureNamespace, getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import { + Navigate, + createFileRoute, + useNavigate, + useParams, +} from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; + +export const Route = createFileRoute("/$feature/finish")({ + component: RouteComponent, +}); + +function RouteComponent() { + const navigate = useNavigate(); + const { feature: featureSlug } = useParams({ from: "/$feature/finish" }); + const feature = parseFeatureRouteSlug(featureSlug); + + if (!feature) return ; + + const { t } = useTranslation(featureNamespace[feature]); + const dispatch = useFileDispatch(); + + const handleRestart = () => { + dispatch(removeAllFiles()); + navigate({ to: "/$feature/onboarding", params: { feature: getFeatureRouteSlug(feature) } }); + }; + + return ( + + +
+ ) : undefined + } + right={} + /> + + {feature === FeatureFlowEnum.Dataset ? ( + + ) : ( + + )} + + + ); +} diff --git a/frontend/src/renderer/src/routes/$feature/onboarding.tsx b/frontend/src/renderer/src/routes/$feature/onboarding.tsx new file mode 100644 index 00000000..3f6e76f8 --- /dev/null +++ b/frontend/src/renderer/src/routes/$feature/onboarding.tsx @@ -0,0 +1,120 @@ +import { + Navigate, + createFileRoute, + useNavigate, + useParams, +} from "@tanstack/react-router"; + +import DropArea from "@/components/drop-area"; +import HiddenInput from "@/components/hidden-input"; +import HowItWorks from "@/components/how-it-works"; +import Footer from "@/components/layout/footer"; +import Header from "@/components/layout/header"; +import HomeButton from "@/components/layout/home-button"; +import MainContent from "@/components/layout/main-content"; +import BackButton from "@/components/ui/back-button"; +import Button from "@/components/ui/button"; +import { useFileDispatch } from "@/hooks"; +import { SectionTitle } from "@/layout/section-title"; +import { addFiles, removeAllFiles } from "@/reducers/file/actions"; +import { useSetTutorialSeen, useTutorialSeen } from "@/store/useLocal"; +import { HStack, Stack, styled } from "@/styled/jsx"; +import { + FeatureFlowEnum, + featureNamespace, + getFeatureRouteSlug, + parseFeatureRouteSlug, +} from "@/types/features"; +import { filesForFeatureLoad } from "@/utils/file/preview-files"; +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect, useRef } from "react"; +import { flushSync } from "react-dom"; +import { useTranslation } from "react-i18next"; + +// FIRST step of the processing workflow +export const Route = createFileRoute("/$feature/onboarding")({ + component: RouteComponent, +}); + +function RouteComponent() { + const queryClient = useQueryClient(); + const { feature: featureSlug } = useParams({ + from: "/$feature/onboarding", + }); + const feature = parseFeatureRouteSlug(featureSlug); + const navigate = useNavigate(); + + if (!feature) return ; + + const { t } = useTranslation(featureNamespace[feature]); + + const inputRef = useRef(null); + + const dispatch = useFileDispatch(); + const tutorialSeen = useTutorialSeen(feature); + const toggleTutorialSeen = useSetTutorialSeen(); + + const handleAddFiles = async (files: File[]) => { + const filesToAdd = filesForFeatureLoad(feature, files); + flushSync(() => { + if (feature === FeatureFlowEnum.Anonymizer) dispatch(removeAllFiles()); + dispatch(addFiles(filesToAdd)); + }); + await navigate({ + to: "/$feature/preview", + params: { feature: getFeatureRouteSlug(feature) }, + }); + toggleTutorialSeen(feature); + }; + + const handleInputChange: React.ChangeEventHandler = (e) => { + const rawFiles = e.target.files; + if (rawFiles) handleAddFiles(Array.from(rawFiles)); + }; + + const handleOpenInput = () => { + inputRef.current?.click(); + }; + + useEffect(() => { + queryClient.removeQueries({ queryKey: ["predict"] }); + queryClient.removeQueries({ queryKey: ["file-parser"] }); + }, [queryClient]); + + return ( + +
} /> + + {tutorialSeen ? ( + + + + {t("onboarding.sectionTitle")} + + + + ) : ( + + )} + +
+ + {!tutorialSeen && ( + + {t("onboarding.validFormats")} + + )} + + +
+ + + ); +} diff --git a/frontend/src/renderer/src/routes/$feature/preview.tsx b/frontend/src/renderer/src/routes/$feature/preview.tsx new file mode 100644 index 00000000..0c924e87 --- /dev/null +++ b/frontend/src/renderer/src/routes/$feature/preview.tsx @@ -0,0 +1,134 @@ +import { Button, FilePreview } from "@/components"; +import HiddenInput from "@/components/hidden-input"; +import Stepper from "@/components/home/stepper"; +import Footer from "@/components/layout/footer"; +import Header from "@/components/layout/header"; +import HomeButton from "@/components/layout/home-button"; +import MainContent from "@/components/layout/main-content"; +import BackButton from "@/components/ui/back-button"; +import Card from "@/components/ui/card"; +import RequireFile from "@/features/RequireFile"; +import { useFileDispatch, useFiles } from "@/hooks"; +import { useFileParse } from "@/hooks/useFileParse"; +import { SectionTitle } from "@/layout/section-title"; +import { addFiles, filterUnselected } from "@/reducers/file/actions"; +import { css } from "@/styled/css"; +import { Grid, HStack, Stack, styled } from "@/styled/jsx"; +import { FeatureFlowEnum, featureNamespace, getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import { + Navigate, + createFileRoute, + useNavigate, + useParams, +} from "@tanstack/react-router"; +import { useRef } from "react"; +import { useTranslation } from "react-i18next"; + +export const Route = createFileRoute("/$feature/preview")({ + component: RouteComponent, +}); + +function RouteComponent() { + const { feature: featureSlug } = useParams({ + from: "/$feature/preview", + }); + const feature = parseFeatureRouteSlug(featureSlug); + const navigate = useNavigate(); + + if (!feature) return ; + + const { t } = useTranslation(featureNamespace[feature]); + + const inputRef = useRef(null); + + const files = useFiles(); + const dispatch = useFileDispatch(); + const parseStatuses = useFileParse(files); + + const isProcessing = files.some((file) => !file.paragraphs); + + const handleAddFiles: React.ChangeEventHandler = async ( + e, + ) => { + const rawFiles = e.target.files; + if (rawFiles) { + dispatch(addFiles([...rawFiles])); + await navigate({ + to: "/$feature/preview", + params: { feature: getFeatureRouteSlug(feature) }, + }); + } + }; + const handleOpenInput = () => { + inputRef.current?.click(); + }; + + const handleConfirmFiles = () => { + dispatch(filterUnselected()); + navigate({ + to: "/$feature/process", + params: { feature: getFeatureRouteSlug(feature) }, + }); + }; + + return ( + + +
} + feature={feature} + right={} + /> + + + + + {t("preview.sectionTitle")} + + + + + {t("preview.filesLabel")} + + + {files.map((file) => ( + + ))} + + + + + +
+ + {feature === FeatureFlowEnum.Dataset && ( + <> + + {t("preview.validFormats")} + + + + )} + + +
+ + + + ); +} diff --git a/frontend/src/renderer/src/routes/$feature/process.tsx b/frontend/src/renderer/src/routes/$feature/process.tsx new file mode 100644 index 00000000..f573137b --- /dev/null +++ b/frontend/src/renderer/src/routes/$feature/process.tsx @@ -0,0 +1,179 @@ +import { Button, FileProcessing } from "@/components"; +import Stepper from "@/components/home/stepper"; +import Footer from "@/components/layout/footer"; +import Header from "@/components/layout/header"; +import HomeButton from "@/components/layout/home-button"; +import MainContent from "@/components/layout/main-content"; +import BackButton from "@/components/ui/back-button"; +import Callout from "@/components/ui/callout"; +import Card from "@/components/ui/card"; +import RequireFile from "@/features/RequireFile"; +import { useFileDispatch, useFiles } from "@/hooks"; +import { useDisambiguate } from "@/hooks/useDisambiguate"; +import { useFileParse } from "@/hooks/useFileParse"; +import { type PredictStatus, usePredict } from "@/hooks/usePredict"; +import { SectionTitle } from "@/layout/section-title"; +import { filterUnprocessed } from "@/reducers/file/actions"; +import taskbar from "@/services/taskbar"; +import { css } from "@/styled/css"; +import { HStack, Stack, styled } from "@/styled/jsx"; +import type { Workflows } from "@/types/aymurai"; +import { FeatureFlowEnum, featureNamespace, getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import type { DocFile } from "@/types/file"; +import { useQueryClient } from "@tanstack/react-query"; +import { + Navigate, + createFileRoute, + useNavigate, + useParams, +} from "@tanstack/react-router"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; + +export const Route = createFileRoute("/$feature/process")({ + component: RouteComponent, +}); + +function RouteComponent() { + const queryClient = useQueryClient(); + const { feature: featureSlug } = useParams({ + from: "/$feature/process", + }); + const feature = parseFeatureRouteSlug(featureSlug); + if (!feature) return ; + + const { t } = useTranslation(featureNamespace[feature]); + + const dispatch = useFileDispatch(); + const navigate = useNavigate(); + const files = useFiles(); + + const [isDismissed, setIsDismissed] = useState(false); + const hasNotified = useRef(false); + + const workflow: Workflows = + feature === FeatureFlowEnum.Anonymizer ? "anonymizer" : "datapublic"; + const parseStatuses = useFileParse(files); + const fileStatuses = usePredict(files, workflow); + const disambiguateStatuses = useDisambiguate( + files, + fileStatuses, + workflow === "anonymizer", + ); + + const handleNext = () => { + dispatch(filterUnprocessed()); + navigate({ + to: "/$feature/validation", + params: { feature: getFeatureRouteSlug(feature) }, + }); + }; + + const isProcessing = files.some( + (f) => + parseStatuses[f.data.name]?.status === "processing" || + fileStatuses[f.data.name]?.status === "processing" || + disambiguateStatuses[f.data.name]?.status === "processing", + ); + + useEffect(() => { + if (files.length > 0 && !isProcessing && !hasNotified.current) { + hasNotified.current = true; + taskbar.notify(); + } + }, [isProcessing, files.length]); + + // Weighted progress: 10% parse / 70% predict / 20% disambiguate (anonymizer) + // or 10% parse / 90% predict (datapublic). + const getProgress = (fileName: string): number => { + const parseDone = parseStatuses[fileName]?.status === "completed" ? 1 : 0; + const predictProgress = fileStatuses[fileName]?.progress ?? 0; + + if (workflow !== "anonymizer") { + return parseDone * 0.1 + predictProgress * 0.9; + } + const disambiguateDone = + disambiguateStatuses[fileName]?.status === "completed" ? 1 : 0; + return parseDone * 0.1 + predictProgress * 0.7 + disambiguateDone * 0.2; + }; + + // Status accounts for all three stages so "completed" only shows at 100%. + const getCombinedStatus = (fileName: string): PredictStatus => { + const parseStatus = parseStatuses[fileName]?.status ?? "processing"; + if (parseStatus !== "completed") return parseStatus; + + const predictStatus = fileStatuses[fileName]?.status ?? "processing"; + if (workflow !== "anonymizer" || predictStatus !== "completed") + return predictStatus; + + return disambiguateStatuses[fileName]?.status ?? "processing"; + }; + + const handleAbort = (file: DocFile) => () => { + queryClient.removeQueries({ + queryKey: ["file-parser", file.data.name], + exact: false, + }); + queryClient.removeQueries({ + queryKey: ["predict", feature, file.data.name], + exact: false, + }); + fileStatuses[file.data.name]?.abort?.(); + parseStatuses[file.data.name]?.abort?.(); + }; + + return ( + + +
} + right={} + /> + + + + + {t("process.sectionTitle")} + + + + + + {t("process.processingTitle")} + + + {t("process.processingSubtitle")} + + + {!isProcessing && !isDismissed && ( + setIsDismissed(true)} + /> + )} + {files.map((f) => ( + + ))} + + + + +
+ +
+ + + ); +} diff --git a/frontend/src/renderer/src/routes/$feature/validation.tsx b/frontend/src/renderer/src/routes/$feature/validation.tsx new file mode 100644 index 00000000..9d2fd963 --- /dev/null +++ b/frontend/src/renderer/src/routes/$feature/validation.tsx @@ -0,0 +1,79 @@ +import { Button, FileAnnotator, ValidateDataset } from "@/components"; +import Stepper from "@/components/home/stepper"; +import Footer from "@/components/layout/footer"; +import Header from "@/components/layout/header"; +import HomeButton from "@/components/layout/home-button"; +import RequireFile from "@/features/RequireFile"; +import { useFiles } from "@/hooks"; +import { Grid, Stack } from "@/styled/jsx"; +import { FeatureFlowEnum, featureNamespace, getFeatureRouteSlug, parseFeatureRouteSlug } from "@/types/features"; +import { + Navigate, + createFileRoute, + useNavigate, + useParams, +} from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; + +export const Route = createFileRoute("/$feature/validation")({ + component: RouteComponent, +}); + +function RouteComponent() { + const { feature: featureSlug } = useParams({ + from: "/$feature/validation", + }); + const feature = parseFeatureRouteSlug(featureSlug); + + const navigate = useNavigate(); + + if (!feature) return ; + + const { t } = useTranslation(featureNamespace[feature]); + + const file = useFiles()[0]!; + + const handleContinue = () => + navigate({ to: "/$feature/finish", params: { feature: getFeatureRouteSlug(feature) } }); + + if (feature === FeatureFlowEnum.Anonymizer) + return ( + + +
} + feature={feature} + right={} + /> + + + + +
+ +
+ + + ); + return ( + + +
} + feature={feature} + /> + + + + ); +} diff --git a/frontend/src/renderer/src/routes/__root.tsx b/frontend/src/renderer/src/routes/__root.tsx new file mode 100644 index 00000000..38c4bcbc --- /dev/null +++ b/frontend/src/renderer/src/routes/__root.tsx @@ -0,0 +1,14 @@ +import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; +import type { QueryClient } from "@tanstack/react-query"; + +export interface RouterContext { + queryClient: QueryClient; +} + +export const Route = createRootRouteWithContext()({ + component: RootLayout, +}); + +function RootLayout() { + return ; +} diff --git a/frontend/src/renderer/src/routes/home/features.tsx b/frontend/src/renderer/src/routes/home/features.tsx new file mode 100644 index 00000000..70ccb60d --- /dev/null +++ b/frontend/src/renderer/src/routes/home/features.tsx @@ -0,0 +1,87 @@ +import FeatureIcon from "@/components/feature-icon"; +import Footer from "@/components/layout/footer"; +import Header from "@/components/layout/header"; +import MainContent from "@/components/layout/main-content"; +import Card from "@/components/ui/card"; +import APIProtected from "@/features/APIProtected"; +import { css } from "@/styled/css"; +import { Grid, Stack, styled } from "@/styled/jsx"; +import { FeatureFlowEnum, getFeatureRouteSlug } from "@/types/features"; +import { FEATURE_ICON } from "@/utils/config"; +import { + Link, + type LinkComponentProps, + createFileRoute, +} from "@tanstack/react-router"; +import type { Icon } from "phosphor-react"; +import { useTranslation } from "react-i18next"; + +interface CardToolProps extends LinkComponentProps { + title: string; + subtitle: string; + icon: Icon; + disabled?: boolean; +} +function CardTool({ + title, + subtitle, + icon: Icon, + disabled = false, + ...props +}: CardToolProps) { + return ( + + + + + + {title} + + {subtitle} + + + + + + ); +} + +export const Route = createFileRoute("/home/features")({ + component: RouteComponent, +}); + +function RouteComponent() { + const { t } = useTranslation(["common", "dataset", "anonymizer"]); + + return ( + + +
+ + + + {t("home.features.greeting")} + + + + + + + +