diff --git a/.env.example b/.env.example index 2cfa4e3..d5a5963 100644 --- a/.env.example +++ b/.env.example @@ -3,9 +3,22 @@ RETROMOL_DUCKDB_HOST_PATH=/data/retromol.duckdb # Redis PASSWORD (used by redis container command) REDIS_PASSWORD=supersecretpassword -# Paths to models used by the backend -PARAS_MODEL_PATH=/app/models/all_substrates_model.paras.gz -PARAS_MODEL_HOST_PATH=/srv/models/all_substrates_model.paras.gz +# Cache dir for the NRPS substrate model (pmp.yml's predictors.nrps.a_domain -- +# see src/retromol/data/pmp.yml -- selects "paras_cli", src/retromol_paras/'s +# trained-model cache). Populate PARAS_CACHE_HOST_PATH by running `python +# scripts/train_paras.py --cache-dir ` once (needs environment.yml's +# hmmer2/hmmer/muscle tools -- see that script's own docstring), so containers +# mount an already-trained model instead of each one training its own on first +# request. +PARAS_CACHE_DIR=/app/models/paras_cache +PARAS_CACHE_HOST_PATH=/srv/models/paras_cache PFAM_HMM_DIR_PATH=/app/hmms/ -PFAM_HMM_DIR_HOST_PATH=/srv/hmms/ \ No newline at end of file +PFAM_HMM_DIR_HOST_PATH=/srv/hmms/ + +# RQ worker pool sizing -- feeds docker-compose.yml's `deploy.replicas` for the +# `worker` service directly (Compose reads this file for its own ${VAR} +# interpolation; env_file: entries like gui/docker/backend.env are injected into +# containers only, Compose itself never sees them). All replicas drain the single +# heavy_compute queue (see routes/queue.py). +RQ_WORKER_REPLICAS=2 \ No newline at end of file diff --git a/.github/workflows/gui-tests.yml b/.github/workflows/gui-tests.yml new file mode 100644 index 0000000..0487d6f --- /dev/null +++ b/.github/workflows/gui-tests.yml @@ -0,0 +1,43 @@ +name: GUI tests + +on: + pull_request: + branches: [main, dev] + paths: + - "gui/**" + +defaults: + run: + working-directory: gui/src/client + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: gui/src/client/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Type-check + run: npx tsc --noEmit + + - name: Lint + run: npx eslint src --ext .ts,.tsx,.js + + - name: Check SmilesDrawer version attribution + run: npm run check:smiles-drawer-version + + - name: Test + run: npm test -- --watchAll=false + env: + CI: true + + - name: Build + run: npm run build diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 0000000..f28b7dc --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,24 @@ +name: Python tests + +on: + pull_request: + branches: [main, dev] + paths: + - "src/**" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: "pip" + + - name: Install package with dev dependencies + run: pip install -e ".[dev]" + + - name: Run tests + run: pytest -q diff --git a/.gitignore b/.gitignore index 7d598aa..166915e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,21 @@ models/ *.py[cod] .venv/ +.venv310/ /data/ /dumps/ /pgdata/ *.dump *.sql -downloads/ \ No newline at end of file +downloads/ + +tmp/ +out/ +.snakemake/ + +# hmmpress binary index files, auto-generated from the packaged .hmm profile on +# first use (see src/retromol_paras/hmmer.py's ensure_hmmpress). +*.h3f +*.h3i +*.h3m +*.h3p \ No newline at end of file diff --git a/README-GUI.md b/README-GUI.md index dcd8f57..e0695a5 100644 --- a/README-GUI.md +++ b/README-GUI.md @@ -6,28 +6,30 @@ This directory contains both a production-ready Docker setup and a developer-fri ## Overview -The system runs five services: -- web: React UI served by nginx -- backend: FLask API served by gunicorn -- db: PostgreSQL with pgvector -- redis: in-memory session and job state store -- maintenance: periodically relabels stale processing jobs +The system runs four services: +- **web**: React UI served by nginx (reverse-proxies `/api/*` to backend) +- **backend**: Flask API served by gunicorn (2 worker processes by default, see [Sizing the backend](#sizing-the-backend)) +- **worker**: RQ background workers that execute every compute-heavy request (RetroMol parsing, PARAS inference, sequence alignment, RDKit fingerprinting/conformer search) off the `heavy_compute` queue -- 2 replicas by default. Runs the same image as backend, just a different command; see [Background job queue](#background-job-queue) +- **redis**: session/job state store, RQ's job queue broker, and the rate limiter's shared counter store -Redis ensures that sessions and job states survive worker restarts and that all backend workers share consistent shared state. +There is no database *service* -- the compound/BGC database is a read-only DuckDB file, mounted directly into backend and worker (see `RETROMOL_DUCKDB_HOST_PATH` below). + +Redis ensures that sessions, job state, and the job queue itself survive individual container restarts, and that backend/worker can scale to multiple replicas while sharing consistent state. + +A background maintenance loop (relabeling any session item stuck in `"processing"`, e.g. after a crashed job) runs inside the backend container itself -- started once, in gunicorn's arbiter process, regardless of worker count. There is no separate maintenance container. ## Build and run with Docker (production mode) The default setup runs everything containerized: - Builds and serves the frontend React app behind nginx - Runs the Flask backend with gunicorn -- Runs an additional backend maintenance script that periodically checks for stale jobs -- Runs PostgreSQL and initializes it from a dump file -- Runs Redis for session/job state -- Exposes a read-only DB user for the backend +- Runs RQ workers that pick up and execute every heavy-compute request +- Runs Redis for session/job state and the job queue +- Mounts a read-only DuckDB file and the PARAS model file into backend and worker ### Start the full stack -First make sure to copy `.env.example` to `.env` and adjust any environment variables as needed. +First make sure to copy `.env.example` to `.env` and adjust any environment variables as needed (paths to your DuckDB file and PARAS model, `REDIS_PASSWORD`). Then run: @@ -35,9 +37,9 @@ Then run: docker compose up -d --build ``` -The backend itself loads Redis and DB configuration from `docker/backend.env`. +The backend and worker load their Redis/DuckDB/PARAS configuration from `gui/docker/backend.env` (both services share the same environment block in `docker-compose.yml`). -### Access the application +### Access the application - App UI: `http:///**` - API endpoints: `http:///api/...**` @@ -46,24 +48,45 @@ For local user, `` is typically `localhost:4005`. ### Check container health -Check that the backend and database are reachable through the API: +`backend`, `redis`, and `web` each have a Docker healthcheck; `docker compose ps` shows `(healthy)`/`(unhealthy)` directly. `worker` has no HTTP surface to probe -- it relies on `restart: unless-stopped` reacting to the process itself exiting. + +You can also check the backend's health endpoints directly: ```bash -curl -i http:///api/health # should return 200 OK (backend alive) -curl -i http:///api/ready # should return 200 OK (DB connection OK) +curl -i http:///api/health # 200 OK: the process is up +curl -i http:///api/ready # 200 OK: DuckDB *and* Redis are both reachable ``` -For local runs, use: +For local runs, use `http://localhost:4005` in place of ``. -```bash -curl -i http://localhost:4005/api/health # should return 200 OK (backend alive) -curl -i http://localhost:4005/api/ready # should return 200 OK (DB connection OK) -``` +### Sizing the backend + +Gunicorn worker/thread counts and timeouts live in `gui/src/server/gunicorn.conf.py`, driven by env vars set in `gui/docker/backend.env` -- change them there without rebuilding the image: + +| Variable | Default | Meaning | +|---|---|---| +| `GUNICORN_WORKERS` | `2` | gunicorn worker processes | +| `GUNICORN_THREADS` | `4` | threads per worker | +| `GUNICORN_TIMEOUT` | `120` | seconds before gunicorn considers a worker hung | +| `RQ_WORKER_REPLICAS` | `2` | how many `worker` containers process the heavy_compute queue (`docker compose up --scale worker=N` also works if your Compose version doesn't apply `deploy.replicas` outside swarm) | +| `HEAVY_JOB_WAIT_TIMEOUT_SECONDS` | `90` | how long a request blocks waiting on a queued job before returning 503 (kept under `GUNICORN_TIMEOUT`) | + +### Background job queue -> Make sure scripts in `/db/init` are executable before first build: -> ```bash -> chmod +x db/init/*.sh -> ``` +Every endpoint that actually computes something (compound/gene-cluster parsing, reconstruction, Discovery search/alignment, Tanimoto comparison, PMI shape analysis) enqueues its work on Redis via RQ and blocks briefly for the result, rather than running in the request thread -- the request/response contract is unchanged, but the actual compute happens on the `worker` containers, isolated from the web tier. If the queue is backed up, a request returns `503` with a "try again in a moment" message instead of hanging. + +### Observability + +- `/metrics` (Prometheus format) is exposed on the backend, including request latency/count by endpoint and custom counters for job outcomes and rate-limit rejections. It is **not** proxied through nginx (only `/api/*` is), so it isn't publicly reachable -- point Prometheus at the `backend` container directly on the Docker network, or `docker exec` in to check it locally: + ```bash + docker exec retromol_backend conda run -n retromol-gui --no-capture-output \ + python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:4000/metrics').read().decode())" + ``` +- In production, backend logs are structured JSON (one line per request with method/path/status/duration, plus app events) -- pipe `docker compose logs backend` into `jq` to filter/query them. + +### Rate limiting + +Per-client request limits (keyed by the `X-Real-IP` header nginx sets) apply on top of a `120/minute` app-wide default: `60/minute` on parsing/reconstruction/Discovery-search endpoints (generous enough for a full batch compound import), `10/minute` on the heavier Tanimoto/PMI-shape comparison endpoints. A breached limit returns `429`. ## Local development mode @@ -92,6 +115,18 @@ CONDA_SUBDIR=osx-64 conda env create -f ./gui/src/server/environment.backend.dev conda activate retromol-gui ``` +`environment.backend.dev.yml` only installs the GUI's own dependencies +(`requirements.backend.txt` -- Flask, gunicorn, RQ, etc.). The backend imports +`retromol`, `retromol_alignment`, `retromol_antismash`, `retromol_database`, +`retromol_fingerprint`, and `retromol_synthesis` directly (see e.g. +`gui/src/server/routes/discovery.py`) -- those come from the root package, not from +that env file, so install it in editable mode too, from the repo root (same +`pip install -e /app` step `backend.Dockerfile` runs for the Docker image): + +```bash +pip install -e . +``` + Then, run the helper script: ```bash @@ -99,7 +134,7 @@ bash ./gui/scripts/dev_backend.sh ``` This script: -- Exports DB_HOST=localhost and REDIS_URL=redis://localhost:6379/0 +- Exports `RETROMOL_DUCKDB_PATH` and `REDIS_URL=redis://localhost:6379/0` - Runs Flask in debug mode with auto-reload on port 4000 Verify health endpoint to check backend is running: @@ -108,6 +143,26 @@ Verify health endpoint to check backend is running: curl -i http://localhost:4000/api/health ``` +**Also start an RQ worker in a second terminal** (same conda env, same Redis) -- every compute-heavy request (compound/cluster submission, Discovery search, Compare, Shape) now blocks waiting on the `heavy_compute` queue, so without a worker running those requests will just time out after `HEAVY_JOB_WAIT_TIMEOUT_SECONDS` (90s) and return a 503. + +The worker runs the task functions itself, so it needs the same environment as the Flask backend (`PYTHONPATH` to import `routes.*`, plus `PARAS_MODEL_PATH`, `CACHE_DIR`, `RETROMOL_DUCKDB_PATH`) -- not just `REDIS_URL`. Use the helper script rather than a bare `rq worker` command: + +```bash +conda activate retromol-gui +bash ./gui/scripts/dev_worker.sh +``` + +By default this one worker listens to both queues (PMI-first, matching a single shared +pile -- see `routes/queue.py`). Optionally, run a second worker terminal dedicated to +the light queue, so a slow PMI-flagged discovery query can never block fast jobs (this +is what production does by default -- see `docker-compose.yml`'s `worker`/`worker_light` +services): + +```bash +conda activate retromol-gui +WORKER_QUEUES=heavy_compute bash ./gui/scripts/dev_worker.sh +``` + ### Run the frontend locally Make sure to add `.env.development.local` to `src/client` and add the following line for SSE: @@ -141,9 +196,10 @@ Production: docker compose up -d --build ``` -Local development: +Local development (three terminals: Redis, backend + RQ worker, frontend; run once, before terminal 2: `pip install -e .` from the repo root, in the activated `retromol-gui` conda env): ```bash docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d redis -bash ./gui/scripts/dev_backend.sh -cd ./gui/src/client && npm start -``` \ No newline at end of file +bash ./gui/scripts/dev_backend.sh # terminal 2 +bash ./gui/scripts/dev_worker.sh # terminal 2b, same conda env +cd ./gui/src/client && npm start # terminal 3 +``` diff --git a/README.md b/README.md index cf55791..87919f4 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,16 @@

- - testing & quality + + GUI tests + + Python tests PyPI PyPI - Python Version - DOI + DOI

RetroMol is retrosynthetic parsing and fingerprinting tool for modular natural products. diff --git a/database/Snakemake b/database/Snakemake new file mode 100644 index 0000000..d743c9c --- /dev/null +++ b/database/Snakemake @@ -0,0 +1,398 @@ +"""RetroMol database-construction pipeline. + +Run from the repo root with: + + snakemake -s database/Snakemake --configfile database/config.yaml --cores 4 + +Fill in database/config.yaml's `sources` URLs before running. The pipeline: + + 1. create_db - empty DuckDB database + 2. download_* - fetch NPAtlas SDF + MIBiG JSON/GBK archives + 3. parse_npatlas } run RetroMol on NPAtlas compounds + 4. load_npatlas_compounds} turn results into "compound" db entries + 5. extract_mibig_compounds + parse_mibig_compounds } run RetroMol on MIBiG's compounds + 6. load_mibig_compounds } turn results into "compound" db entries (linked to MIBiG's URL) + 7. parse_mibig_gbks - antiSMASH GBKs -> linear module readouts (PARAS-annotated) + 8. load_mibig_bgcs - turn readouts into "bgc" db entries + 9. annotate_npclassifier - chemical-class annotation (compounds only, NPClassifier API) + 10. annotate_chebi - bioactivity annotation (compounds only, local ChEBI flat files) + +Steps 4, 6, 8, 9, and 10 all mutate the same DuckDB file, so they're chained through +marker files (rather than each declaring the database itself as `output`) to force +Snakemake to serialize them -- DuckDB doesn't support concurrent writers. +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(workflow.basedir) / "scripts")) + +WORKDIR = Path(config["paths"]["workdir"]) +DB_PATH = Path(config["paths"]["database"]) +MARKERS = WORKDIR / "markers" + +RXN_RULES = config["paths"].get("reaction_rules") +MXN_RULES = config["paths"].get("matching_rules") +MATCH_STEREOCHEMISTRY = config.get("match_stereochemistry", False) + +# null -> pmp.yml's packaged default. See that file's "predictors.nrps.a_domain" for +# which model (if any) this actually selects -- the same file the GUI backend and +# scripts/predict_bgc.py read, so all three resolve GenBank substrates identically. +PMP_PATH = config["paras"].get("pmp_path") +PARAS_THRESHOLD = config["paras"]["threshold"] +PARAS_KEEP_TOP = config["paras"]["keep_top"] +PARAS_CACHE_DIR = config["paras"].get("cache_dir") or str(WORKDIR / "paras_cache") +PARAS_FORCE_RETRAIN = config["paras"].get("force_retrain") or False + +PARSE_COMPOUNDS_WORKERS = config["compute"]["parse_compounds_workers"] +PARSE_GBKS_WORKERS = config["compute"]["parse_gbks_workers"] + +TAXONOMY_ENABLED = config.get("taxonomy", {}).get("enabled", False) +TAXDUMP_DIR = WORKDIR / "taxdump" + +NPCLASSIFIER_REQUESTS_PER_SECOND = config.get("npclassifier", {}).get("requests_per_second", 2.0) +NPCLASSIFIER_WORKERS = config.get("npclassifier", {}).get("workers", 8) + +CHEBI_DIR = WORKDIR / "chebi" + +# Toggle whole branches of the pipeline off (see config.yaml's `enabled` comment) -- +# disabling npatlas/mibig skips their download+parse rules entirely, not just db +# loading, since the "real" inputs below are only declared when enabled: nothing else +# in the DAG needs npatlas/results.jsonl or mibig_gbk/readouts.jsonl, so Snakemake +# never schedules the rules that would produce them. Same idea for chebi: disabling it +# skips its bulk download entirely. +ENABLED = config.get("enabled", {}) +NPATLAS_ENABLED = ENABLED.get("npatlas", True) +MIBIG_ENABLED = ENABLED.get("mibig", True) +NPCLASSIFIER_ENABLED = ENABLED.get("npclassifier", True) +CHEBI_ENABLED = ENABLED.get("chebi", True) + + +rule all: + input: + MARKERS / "bgcs_loaded.done", + MARKERS / "npclassifier_annotated.done", + MARKERS / "chebi_annotated.done" + + +# --------------------------------------------------------------------------- +# NCBI taxonomy dump (used to standardize phylogeny genus/species/type to taxids) +# --------------------------------------------------------------------------- + +rule download_taxdump: + output: + names=TAXDUMP_DIR / "names.dmp", + nodes=TAXDUMP_DIR / "nodes.dmp" + run: + import taxonomy + taxonomy.download_taxdump(TAXDUMP_DIR) + + +# --------------------------------------------------------------------------- +# Step 1: empty database +# --------------------------------------------------------------------------- + +rule create_db: + output: + marker=touch(MARKERS / "db_created.done") + run: + import create_db + create_db.run(db_path=DB_PATH, overwrite=True) + + +# --------------------------------------------------------------------------- +# Step 2: downloads +# --------------------------------------------------------------------------- + +rule download_npatlas: + output: + raw=WORKDIR / "npatlas" / "download.raw", + extract_dir=directory(WORKDIR / "npatlas" / "extracted") + params: + url=config["sources"]["npatlas_sdf_url"] + run: + import download_sources + download_sources.run(url=params.url, download_path=output.raw, extract_dir=output.extract_dir) + + +rule resolve_npatlas_sdf: + input: + extract_dir=WORKDIR / "npatlas" / "extracted" + output: + sdf=WORKDIR / "npatlas" / "npatlas.sdf" + run: + import shutil + candidates = sorted(Path(input.extract_dir).rglob("*.sdf")) + if not candidates: + raise FileNotFoundError(f"no .sdf file found under {input.extract_dir}") + shutil.copy2(candidates[0], output.sdf) + + +rule download_mibig_json: + output: + raw=WORKDIR / "mibig_json" / "download.raw", + extract_dir=directory(WORKDIR / "mibig_json" / "extracted") + params: + url=config["sources"]["mibig_json_url"] + run: + import download_sources + download_sources.run(url=params.url, download_path=output.raw, extract_dir=output.extract_dir) + + +rule download_mibig_gbk: + output: + raw=WORKDIR / "mibig_gbk" / "download.raw", + extract_dir=directory(WORKDIR / "mibig_gbk" / "extracted") + params: + url=config["sources"]["mibig_gbk_url"] + run: + import download_sources + download_sources.run(url=params.url, download_path=output.raw, extract_dir=output.extract_dir) + + +# --------------------------------------------------------------------------- +# Steps 3-4: NPAtlas compounds +# --------------------------------------------------------------------------- + +rule parse_npatlas: + input: + sdf=WORKDIR / "npatlas" / "npatlas.sdf" + output: + results=WORKDIR / "npatlas" / "results.jsonl" + threads: PARSE_COMPOUNDS_WORKERS + run: + import parse_compounds + parse_compounds.run( + input_path=input.sdf, + input_format="sdf", + output_path=output.results, + reaction_rules_path=RXN_RULES, + matching_rules_path=MXN_RULES, + workers=threads, + ) + + +rule load_npatlas_compounds: + input: + db_created=MARKERS / "db_created.done", + # Only declared when enabled -- nothing else in the DAG needs npatlas/results.jsonl, + # so download_npatlas/resolve_npatlas_sdf/parse_npatlas never run when disabled. + **({"results": WORKDIR / "npatlas" / "results.jsonl"} if NPATLAS_ENABLED else {}), + **({"taxdump_names": TAXDUMP_DIR / "names.dmp", "taxdump_nodes": TAXDUMP_DIR / "nodes.dmp"} if TAXONOMY_ENABLED and NPATLAS_ENABLED else {}) + output: + marker=touch(MARKERS / "npatlas_loaded.done") + run: + if NPATLAS_ENABLED: + import load_compounds + load_compounds.run( + results_path=input.results, + db_path=DB_PATH, + source="npatlas", + reaction_rules_path=RXN_RULES, + matching_rules_path=MXN_RULES, + match_stereochemistry=MATCH_STEREOCHEMISTRY, + taxdump_dir=TAXDUMP_DIR if TAXONOMY_ENABLED else None, + ) + + +# --------------------------------------------------------------------------- +# Steps 5-6: MIBiG compounds +# --------------------------------------------------------------------------- + +rule extract_mibig_compounds: + input: + extract_dir=WORKDIR / "mibig_json" / "extracted" + output: + compounds=WORKDIR / "mibig_json" / "compounds.jsonl", + # Sole source of MIBiG accession -> version now (see extract_mibig_compounds.py + # module docstring): MIBiG 4.0's GBKs dropped the ACCESSION.VERSION suffix + # parse_gbks.py used to read this from, but the JSON's own top-level "version" + # field still has it. + versions=WORKDIR / "mibig_json" / "versions.json", + # accession -> {organism_name, ncbi_tax_id, biosyn_class}, consumed by both + # load_mibig_compounds and load_mibig_bgcs to populate phylogeny/chemical_class + # annotations (see RetroMolDuckDB.add_phylogeny_annotation/add_flat_annotation). + annotations=WORKDIR / "mibig_json" / "annotations.json" + run: + import extract_mibig_compounds + extract_mibig_compounds.run( + mibig_json_dir=input.extract_dir, + output_path=output.compounds, + versions_output_path=output.versions, + annotations_output_path=output.annotations, + ) + + +rule parse_mibig_compounds: + input: + compounds=WORKDIR / "mibig_json" / "compounds.jsonl" + output: + results=WORKDIR / "mibig_json" / "results.jsonl" + threads: PARSE_COMPOUNDS_WORKERS + run: + import parse_compounds + parse_compounds.run( + input_path=input.compounds, + input_format="jsonl", + output_path=output.results, + reaction_rules_path=RXN_RULES, + matching_rules_path=MXN_RULES, + workers=threads, + ) + + +rule load_mibig_compounds: + input: + prev=MARKERS / "npatlas_loaded.done", + # Only declared when enabled -- nothing else in the DAG needs these, so + # download_mibig_json/extract_mibig_compounds/parse_mibig_compounds never run + # when disabled. MIBiG URLs need an accession's version, from the JSON (see + # extract_mibig_compounds rule above) -- not from the GBKs, which no longer carry it. + **( + { + "results": WORKDIR / "mibig_json" / "results.jsonl", + "versions": WORKDIR / "mibig_json" / "versions.json", + "annotations": WORKDIR / "mibig_json" / "annotations.json", + } + if MIBIG_ENABLED else {} + ), + **({"taxdump_names": TAXDUMP_DIR / "names.dmp", "taxdump_nodes": TAXDUMP_DIR / "nodes.dmp"} if TAXONOMY_ENABLED and MIBIG_ENABLED else {}) + output: + marker=touch(MARKERS / "mibig_compounds_loaded.done") + run: + if MIBIG_ENABLED: + import load_compounds + load_compounds.run( + results_path=input.results, + db_path=DB_PATH, + source="mibig", + reaction_rules_path=RXN_RULES, + matching_rules_path=MXN_RULES, + match_stereochemistry=MATCH_STEREOCHEMISTRY, + mibig_versions_path=input.versions, + mibig_annotations_path=input.annotations, + taxdump_dir=TAXDUMP_DIR if TAXONOMY_ENABLED else None, + ) + + +# --------------------------------------------------------------------------- +# Steps 7-8: MIBiG BGCs +# --------------------------------------------------------------------------- + +rule parse_mibig_gbks: + input: + gbk_dir=WORKDIR / "mibig_gbk" / "extracted" + output: + readouts=WORKDIR / "mibig_gbk" / "readouts.jsonl" + threads: PARSE_GBKS_WORKERS + run: + import parse_gbks + parse_gbks.run( + gbk_dir=input.gbk_dir, + readouts_output_path=output.readouts, + pmp_path=PMP_PATH, + paras_threshold=PARAS_THRESHOLD, + paras_keep_top=PARAS_KEEP_TOP, + paras_cache_dir=PARAS_CACHE_DIR, + force_retrain=PARAS_FORCE_RETRAIN, + workers=threads, + ) + + +rule load_mibig_bgcs: + input: + prev=MARKERS / "mibig_compounds_loaded.done", + # Only declared when enabled -- nothing else in the DAG needs these, so + # download_mibig_gbk/parse_mibig_gbks never run when disabled. + **( + { + "readouts": WORKDIR / "mibig_gbk" / "readouts.jsonl", + "versions": WORKDIR / "mibig_json" / "versions.json", + "annotations": WORKDIR / "mibig_json" / "annotations.json", + } + if MIBIG_ENABLED else {} + ), + **({"taxdump_names": TAXDUMP_DIR / "names.dmp", "taxdump_nodes": TAXDUMP_DIR / "nodes.dmp"} if TAXONOMY_ENABLED and MIBIG_ENABLED else {}) + output: + marker=touch(MARKERS / "bgcs_loaded.done") + run: + if MIBIG_ENABLED: + import load_bgcs + load_bgcs.run( + readouts_path=input.readouts, + db_path=DB_PATH, + reaction_rules_path=RXN_RULES, + matching_rules_path=MXN_RULES, + match_stereochemistry=MATCH_STEREOCHEMISTRY, + mibig_versions_path=input.versions, + mibig_annotations_path=input.annotations, + taxdump_dir=TAXDUMP_DIR if TAXONOMY_ENABLED else None, + ) + + +# --------------------------------------------------------------------------- +# Step 9: NPClassifier chemical-class annotation (compounds only) +# --------------------------------------------------------------------------- + +rule annotate_npclassifier: + input: + # Chained after bgcs_loaded.done (not just the compound-loading steps) purely to + # serialize this write against load_mibig_bgcs's -- both mutate DB_PATH and DuckDB + # doesn't support concurrent writers (see module docstring at the top of this file). + prev=MARKERS / "bgcs_loaded.done" + output: + marker=touch(MARKERS / "npclassifier_annotated.done") + # Declared like parse_npatlas/parse_mibig_gbks's worker counts, even though this + # rule is I/O-bound rather than CPU-bound: without a `threads:` declaration, + # Snakemake assumes a 1-core job while it actually opens NPCLASSIFIER_WORKERS + # concurrent connections, so `--cores` wouldn't see or cap it. Declaring it here + # means `--cores` fewer than npclassifier.workers automatically caps this job's + # `threads` value at run time -- passed through below instead of the raw config + # number, so a run with e.g. `--cores 6` gets at most 6 workers even if + # npclassifier.workers says 8. + threads: NPCLASSIFIER_WORKERS + run: + if NPCLASSIFIER_ENABLED: + import annotate_npclassifier + annotate_npclassifier.run( + db_path=DB_PATH, + cache_path=WORKDIR / "npclassifier" / "cache.jsonl", + requests_per_second=NPCLASSIFIER_REQUESTS_PER_SECOND, + workers=threads, + ) + + +# --------------------------------------------------------------------------- +# Step 10: ChEBI bioactivity annotation (compounds only) +# --------------------------------------------------------------------------- + +rule download_chebi: + output: + compounds=CHEBI_DIR / "compounds.tsv.gz", + structures=CHEBI_DIR / "structures.tsv.gz", + relation=CHEBI_DIR / "relation.tsv.gz" + run: + import chebi + chebi.download_chebi_flat_files(CHEBI_DIR) + + +rule annotate_chebi: + input: + # Chained after npclassifier_annotated.done purely to serialize this write + # against annotate_npclassifier's (see module docstring at the top of this file). + prev=MARKERS / "npclassifier_annotated.done", + # Only declared when enabled -- download_chebi never runs when disabled. + **( + { + "compounds": CHEBI_DIR / "compounds.tsv.gz", + "structures": CHEBI_DIR / "structures.tsv.gz", + "relation": CHEBI_DIR / "relation.tsv.gz", + } + if CHEBI_ENABLED else {} + ) + output: + marker=touch(MARKERS / "chebi_annotated.done") + run: + if CHEBI_ENABLED: + import annotate_chebi + annotate_chebi.run(db_path=DB_PATH, chebi_dir=CHEBI_DIR) diff --git a/database/config.yaml b/database/config.yaml new file mode 100644 index 0000000..2c831a9 --- /dev/null +++ b/database/config.yaml @@ -0,0 +1,83 @@ +sources: + # Direct download links. MIBiG ships as tar.gz archives (one file per BGC inside); + # NPAtlas ships as a single SDF (optionally gzipped) -- download_sources.py handles + # extraction based on the URL's extension either way. + npatlas_sdf_url: "https://www.npatlas.org/static/downloads/NPAtlas_download.sdf" + mibig_json_url: "https://dl.secondarymetabolites.org/mibig/mibig_json_4.0.tar.gz" + mibig_gbk_url: "https://dl.secondarymetabolites.org/mibig/mibig_gbk_4.0.tar.gz" + +enabled: + # Toggle whole branches of the pipeline off. Disabling a source skips its downloads + # and RetroMol parsing entirely (not just db loading) -- e.g. npatlas: false means + # download_npatlas/parse_npatlas never run. mibig covers both MIBiG compounds and + # BGCs (they share the same JSON/GBK downloads). npclassifier gates step 9 + # (chemical-class annotation) independently of which source(s) loaded the compounds + # it classifies. + npatlas: true + mibig: true + npclassifier: true + chebi: true + +paths: + # Final DuckDB database produced by the pipeline. + database: "/Users/davidmeijer/Desktop/retromol.duckdb" + + # Scratch space for downloads and intermediate per-step results. + workdir: "/Users/davidmeijer/Desktop/retromol_tmp" + + # null -> RuleSet.load_default()'s bundled reaction/matching rules. + reaction_rules: null + matching_rules: null + +# Whether structural matching (both compound parsing and NRPS substrate resolution, +# e.g. matching a PARAS-predicted substrate's SMILES against mxn.yml -- see +# retromol_antismash.modules.module_primary_sequence_tokens) requires an exact +# stereochemistry match, not just constitution. False (the default) treats e.g. two +# rules that only differ in a stereocenter as interchangeable. +match_stereochemistry: false + +npclassifier: + # Rate limit for the free, GNPS2-hosted NPClassifier API (no published limit -- + # kept conservative). This is the *combined* rate across all workers below, not + # per-worker. Classifications are cached in workdir/npclassifier/cache.jsonl, so + # reruns only pay for compounds not already classified. + requests_per_second: 50.0 + + # Concurrent requests. Classification is I/O-bound (waiting on the API), not + # CPU-bound, so this is safe to raise well past your core count -- it's bounded by + # requests_per_second above either way. + workers: 8 + +taxonomy: + # NCBI taxdump (names.dmp/nodes.dmp), downloaded once into workdir/taxdump and reused + # across pipeline runs -- used to standardize phylogeny genus/species/type to NCBI + # taxids (see database/scripts/taxonomy.py). Set to null to skip taxid resolution + # entirely (phylogeny is then stored as raw, unstandardized text/no taxids). + enabled: true + +paras: + threshold: 0.1 + keep_top: 3 + + # Path to a pmp.yml prediction-mapping file (see src/retromol/data/pmp.yml). + # null -> the packaged default. Its "predictors.nrps.a_domain" selects which + # model (if any) actually runs here -- "paras" (pyhmmer, a pretrained model + # file), "paras_cli" (src/retromol_paras/, retrained/cached locally from + # command-line HMMER2/HMMER3/MUSCLE3 -- see envs/retromol_paras.yaml -- or + # rather environment.yml at repo root), or a source: qualifier method that + # reads antiSMASH's own NRPS substrate call and runs no model at all. Same + # file the GUI backend and scripts/predict_bgc.py read, so switching + # predictors here changes every entry point at once. + pmp_path: null + + # Cache directory for whichever model pmp_path selects (a download cache for + # "paras", a training-signature + fitted-model cache for "paras_cli"). + cache_dir: null + + # "paras_cli"-only: retrain from scratch even if a cached model is present. + force_retrain: false + +compute: + # Both are embarrassingly parallel over independent compounds/files. + parse_compounds_workers: 4 + parse_gbks_workers: 4 diff --git a/database/profiles/slurm/config.yaml b/database/profiles/slurm/config.yaml new file mode 100644 index 0000000..796187c --- /dev/null +++ b/database/profiles/slurm/config.yaml @@ -0,0 +1,83 @@ +# Snakemake workflow profile for running database/Snakemake on a Slurm cluster. +# +# Usage (from the repo root, with environment.yml's env active -- see that +# file's own comment for why there's no --use-conda here): +# +# conda activate retromol +# snakemake -p -s database/Snakemake --configfile database/config.yaml \ +# --workflow-profile database/profiles/slurm +executor: slurm +jobs: 6 # concurrent Slurm jobs -- mainly matters for the independent download_* rules; + # everything downstream of them is a strict chain (create_db -> load_* -> + # annotate_*), serialized through marker files because DuckDB doesn't support + # concurrent writers (see database/Snakemake's module docstring), so raising + # this past ~6-8 buys little. +latency-wait: 60 +rerun-incomplete: true +printshellcmds: true +# Independent rules (e.g. the download_* rules, or npclassifier/chebi annotation vs. +# each other before they're serialized) shouldn't all be killed by one unrelated rule +# failing/timing out -- let everything already running finish. +keep-going: true + +default-resources: + slurm_account: null # fill in your account, if your cluster requires one + slurm_partition: null # fill in your cluster's partition name + runtime: 60 + mem_mb: 4000 + disk_mb: 20000 + +# parse_npatlas/parse_mibig_compounds/parse_mibig_gbks' worker counts come from +# database/config.yaml's `compute:` section (PARSE_COMPOUNDS_WORKERS/PARSE_GBKS_WORKERS), +# not from this profile -- deliberately not duplicated here as set-threads to avoid two +# sources of truth for the same value; edit config.yaml instead. +set-resources: + download_npatlas: + runtime: 120 + mem_mb: 4000 + disk_mb: 10000 + download_mibig_json: + runtime: 60 + mem_mb: 2000 + disk_mb: 5000 + download_mibig_gbk: + runtime: 60 + mem_mb: 2000 + disk_mb: 5000 + download_taxdump: + runtime: 60 + mem_mb: 2000 + disk_mb: 5000 + download_chebi: + # A few hundred MB total across compounds/structures/relation flat files. + runtime: 60 + mem_mb: 2000 + disk_mb: 5000 + parse_npatlas: + runtime: 720 + mem_mb: 16000 + parse_mibig_compounds: + runtime: 240 + mem_mb: 8000 + parse_mibig_gbks: + runtime: 480 + mem_mb: 16000 + load_npatlas_compounds: + runtime: 60 + mem_mb: 4000 + load_mibig_compounds: + runtime: 60 + mem_mb: 4000 + load_mibig_bgcs: + runtime: 60 + mem_mb: 4000 + annotate_npclassifier: + # Rate-limited API calls (see database/config.yaml's npclassifier.requests_per_second) + # -- mostly wall-clock waiting on the network, not compute. + runtime: 1440 + mem_mb: 2000 + annotate_chebi: + # Loads all of ChEBI's flat files into memory once (see chebi.py's ChebiDB.load) -- + # a few hundred MB, generous headroom here. + runtime: 120 + mem_mb: 8000 diff --git a/database/scripts/annotate_chebi.py b/database/scripts/annotate_chebi.py new file mode 100644 index 0000000..ad8de7f --- /dev/null +++ b/database/scripts/annotate_chebi.py @@ -0,0 +1,77 @@ +"""Step 10: annotate every compound entry's bioactivity via ChEBI's role ontology. + +Runs after compound loading, so every distinct molecule -- regardless of which +source(s) it came from -- is looked up exactly once. BGC entries are skipped: +bioactivity here is a property of the compound structure (looked up by its InChIKey +entry id), not of the producing organism/cluster. + +This queries a local bulk release (see chebi.py) rather than a rate-limited API -- no +pacing or caching needed, lookups are just local reads. +""" + +import argparse +import logging +from pathlib import Path + +from tqdm import tqdm + +from chebi import ChebiDB +from retromol_database.duckdb import RetroMolDuckDB + +log = logging.getLogger(__name__) + + +def run(db_path: str | Path, chebi_dir: str | Path, log_every: int = 500) -> None: + processed = 0 + matched = 0 + annotated = 0 + + db = RetroMolDuckDB.open(db_path) + try: + total = db.count_entries_by_type(["compound"]) + chebi = ChebiDB.load(chebi_dir) + with tqdm(total=total, desc="annotate_chebi", unit="cmpd") as pbar: + for entry in db.iter_entries(): + if entry.type != "compound": + continue + + processed += 1 + result = chebi.roles_for_inchikey(entry.id) + if result is not None: + matched += 1 + for role in result.biological_roles: + db.add_bioactivity_annotation( + entry.id, level="chebi_biological_role", label=role.label, external_id=role.chebi_accession + ) + annotated += 1 + for role in result.chemical_roles: + db.add_bioactivity_annotation( + entry.id, level="chebi_chemical_role", label=role.label, external_id=role.chebi_accession + ) + annotated += 1 + + pbar.update(1) + pbar.set_postfix(matched=matched, annotated=annotated) + + if log_every > 0 and processed % log_every == 0: + log.info("annotate_chebi: processed=%d matched=%d annotated=%d", processed, matched, annotated) + finally: + db.close() + + log.info("annotate_chebi: processed=%d matched=%d annotated=%d", processed, matched, annotated) + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--db-path", required=True) + ap.add_argument("--chebi-dir", required=True, help="dir with ChEBI's compounds.tsv.gz/structures.tsv.gz/relation.tsv.gz") + ap.add_argument("--log-every", type=int, default=500) + args = ap.parse_args() + + run(db_path=args.db_path, chebi_dir=args.chebi_dir, log_every=args.log_every) + + +if __name__ == "__main__": + main() diff --git a/database/scripts/annotate_npclassifier.py b/database/scripts/annotate_npclassifier.py new file mode 100644 index 0000000..a51b8c7 --- /dev/null +++ b/database/scripts/annotate_npclassifier.py @@ -0,0 +1,245 @@ +"""Step 9: classify every compound entry's chemical class via NPClassifier. + +Runs after both compound-loading steps (NPAtlas + MIBiG compounds are already +deduplicated by inchikey in `entries` by then -- see load_compounds.py), so each +distinct molecule is classified exactly once no matter which source(s) it came from. +BGC entries are skipped: NPClassifier needs a compound's own SMILES structure +(`entry.raw`), which a bgc entry doesn't have. + +Idempotent across reruns via a local JSONL cache (`--cache-path`) keyed by entry_id: +already-classified compounds are skipped without hitting the API again, and only +successes are cached -- flushed to disk as each one completes, not batched at the end +-- so a kill mid-run loses at most the handful of requests in flight, and a transient +failure gets retried on the next run. + +Classification is I/O-bound (waiting on NPClassifier's API), not CPU-bound, so +`--workers` runs multiple requests concurrently via a thread pool -- the GIL doesn't +block this the way it would CPU-bound work, since each thread is blocked on network +I/O rather than holding the GIL. `--requests-per-second` still caps the *combined* +rate across all workers (a shared, thread-safe RateLimiter -- see below), since that +cap is about being a good citizen towards NPClassifier's free, GNPS2-hosted service, +not about this process's own resources. DB writes and cache appends only ever happen +on the main thread (as results complete), so there's no concurrent-write concern +there -- only the HTTP calls themselves run in parallel. +""" + +import argparse +import itertools +import json +import logging +import threading +import time +from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait +from pathlib import Path + +from tqdm import tqdm + +from npclassifier import ClassificationResult, classify_smiles +from retromol_database.duckdb import Entry, RetroMolDuckDB + +log = logging.getLogger(__name__) + + +class RateLimiter: + """Thread-safe request pacing shared across worker threads. + + Each call reserves the next available time slot under a lock (cheap, no waiting + while holding it), then sleeps outside the lock until its own slot arrives -- so + threads don't serialize on the actual waiting, only on the quick slot reservation. + """ + + def __init__(self, requests_per_second: float) -> None: + self._min_interval = 1.0 / requests_per_second if requests_per_second > 0 else 0.0 + self._lock = threading.Lock() + self._next_slot = time.monotonic() + + def wait_for_slot(self) -> None: + with self._lock: + now = time.monotonic() + slot = max(now, self._next_slot) + self._next_slot = slot + self._min_interval + delay = slot - now + if delay > 0: + time.sleep(delay) + + +def _load_cache(cache_path: Path) -> dict[str, ClassificationResult]: + if not cache_path.exists(): + return {} + + cache: dict[str, ClassificationResult] = {} + with open(cache_path) as fh: + for line in fh: + line = line.strip() + if not line: + continue + row = json.loads(line) + cache[row["entry_id"]] = ClassificationResult( + pathway=row["pathway"], + superclass=row["superclass"], + class_=row["class_"], + is_glycoside=row["is_glycoside"], + ) + return cache + + +def _append_cache(cache_path: Path, entry_id: str, result: ClassificationResult) -> None: + with open(cache_path, "a") as fh: + fh.write( + json.dumps( + { + "entry_id": entry_id, + "pathway": result.pathway, + "superclass": result.superclass, + "class_": result.class_, + "is_glycoside": result.is_glycoside, + } + ) + + "\n" + ) + + +def _apply(db: RetroMolDuckDB, entry_id: str, result: ClassificationResult) -> None: + for label in result.pathway: + db.add_chemical_class_annotation(entry_id, level="pathway", label=label) + for label in result.superclass: + db.add_chemical_class_annotation(entry_id, level="superclass", label=label) + for label in result.class_: + db.add_chemical_class_annotation(entry_id, level="class", label=label) + # Presence-only, same convention as every other flag-shaped annotation: only stored + # when true, and with a real name ("Glycoside") rather than a bare "Yes" that means + # nothing once it's sitting in a chart/chip out of context. + if result.is_glycoside: + db.add_chemical_class_annotation(entry_id, level="is_glycoside", label="Glycoside") + + +def run( + db_path: str | Path, + cache_path: str | Path, + requests_per_second: float = 2.0, + workers: int = 8, + limit: int | None = None, + log_every: int = 100, +) -> None: + cache_path = Path(cache_path) + cache_path.parent.mkdir(parents=True, exist_ok=True) + cache = _load_cache(cache_path) + log.info("annotate_npclassifier: loaded %d cached classifications from %s", len(cache), cache_path) + + rate_limiter = RateLimiter(requests_per_second) + + classified = 0 + reused = 0 + skipped_no_smiles = 0 + failed = 0 + + def classify_one(entry: Entry) -> tuple[Entry, ClassificationResult | None]: + rate_limiter.wait_for_slot() + return entry, classify_smiles(entry.raw) + + db = RetroMolDuckDB.open(db_path) + try: + to_classify: list[Entry] = [] + for entry in db.iter_entries(): + if entry.type != "compound": + continue + + if entry.id in cache: + _apply(db, entry.id, cache[entry.id]) + reused += 1 + continue + + if not entry.raw: + skipped_no_smiles += 1 + continue + + if limit is not None and len(to_classify) >= limit: + continue + + to_classify.append(entry) + + log.info( + "annotate_npclassifier: %d compounds to classify (workers=%d, requests_per_second=%s)", + len(to_classify), workers, requests_per_second, + ) + + # Bounded sliding window rather than submitting the whole backlog up front -- + # at hundreds/thousands of compounds, an upfront submit() for everything means + # a Ctrl+C has to wait for every already-launched request (each with its own + # retry/backoff chain, worse under rate-limiting) before the pool can actually + # exit, since ThreadPoolExecutor won't drop already-running work. Keeping at + # most `max_pending` in flight means an interrupt only has to wait for that many + # -- same shape as parse_gbks.py's own bounded-window loop, same reason. + max_pending = max(workers * 2, 1) + entries_iter = iter(to_classify) + + pool = ThreadPoolExecutor(max_workers=workers) + try: + pending = {pool.submit(classify_one, e) for e in itertools.islice(entries_iter, max_pending)} + + with tqdm(total=len(to_classify), desc="annotate_npclassifier", unit="cmpd") as pbar: + while pending: + done_futures, pending = wait(pending, return_when=FIRST_COMPLETED) + + for future in done_futures: + entry, result = future.result() + + if result is None: + failed += 1 + else: + _apply(db, entry.id, result) + _append_cache(cache_path, entry.id, result) + classified += 1 + + pbar.update(1) + pbar.set_postfix(classified=classified, reused=reused, failed=failed) + + done = classified + failed + if log_every > 0 and done % log_every == 0: + log.info( + "annotate_npclassifier: classified=%d reused=%d failed=%d skipped_no_smiles=%d", + classified, reused, failed, skipped_no_smiles, + ) + + next_entry = next(entries_iter, None) + if next_entry is not None: + pending.add(pool.submit(classify_one, next_entry)) + except KeyboardInterrupt: + log.warning("annotate_npclassifier: interrupted -- cancelling not-yet-started requests") + pool.shutdown(wait=False, cancel_futures=True) + raise + else: + pool.shutdown(wait=True) + finally: + db.close() + + log.info( + "annotate_npclassifier: classified=%d reused=%d failed=%d skipped_no_smiles=%d", + classified, reused, failed, skipped_no_smiles, + ) + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--db-path", required=True) + ap.add_argument("--cache-path", required=True) + ap.add_argument("--requests-per-second", type=float, default=2.0) + ap.add_argument("--workers", type=int, default=8, help="concurrent requests (I/O-bound, not CPU-bound)") + ap.add_argument("--limit", type=int, default=None, help="classify at most N new compounds (testing/dry-run)") + ap.add_argument("--log-every", type=int, default=100) + args = ap.parse_args() + + run( + db_path=args.db_path, + cache_path=args.cache_path, + requests_per_second=args.requests_per_second, + workers=args.workers, + limit=args.limit, + log_every=args.log_every, + ) + + +if __name__ == "__main__": + main() diff --git a/database/scripts/chebi.py b/database/scripts/chebi.py new file mode 100644 index 0000000..d4652d4 --- /dev/null +++ b/database/scripts/chebi.py @@ -0,0 +1,239 @@ +"""ChEBI role-ontology client for bioactivity annotation. + +Downloads ChEBI's flat-file bulk release (ftp.ebi.ac.uk) once, then looks up a +compound's biological/chemical roles by its standard InChIKey. ChEBI's role ontology +(`has_role` edges under CHEBI:24432 "biological role" / CHEBI:51086 "chemical role") +is annotated on generic compound classes more often than on specific stereo-defined +structures -- confirmed +live (2026-08-26): erythromycin A (CHEBI:42355) itself carries no has_role edges, only +its `is_a` parent "erythromycin" (CHEBI:48923) does (roles: xenobiotic, bacterial +metabolite, environmental contaminant). So lookups walk a few steps up the `is_a` +ancestor chain collecting has_role edges at every level, not just the leaf's own. + +Schema confirmed live by downloading and inspecting +ftp.ebi.ac.uk/pub/databases/chebi/flat_files/: structures.tsv (compound_id, +standard_inchi_key), relation.tsv (relation_type_id 4=has_role, 5=is_a; init_id/final_id +are compounds.tsv ids), compounds.tsv (id, name, chebi_accession). +""" + +from __future__ import annotations + +import csv +import gzip +import logging +import re +import urllib.request +from dataclasses import dataclass +from pathlib import Path + +from tqdm import tqdm + +log = logging.getLogger(__name__) + +CHEBI_FLAT_FILES_URL = "https://ftp.ebi.ac.uk/pub/databases/chebi/flat_files" +CHEBI_FILES = ["compounds.tsv.gz", "structures.tsv.gz", "relation.tsv.gz"] + +# ChEBI's `name` column carries inline HTML for italicized genus/species names and +# similar formatting (e.g. "Aspergillus metabolite", "S-configuration" +# -- confirmed live in compounds.tsv.gz/chemical_data.tsv.gz). Stripped here, once, at +# load time -- so every consumer of name_by_compound_id (role labels today, anything +# else later) gets plain text without needing to know this quirk exists. +_HTML_TAG_RE = re.compile(r"<[^>]+>") + + +def _strip_html(text: str) -> str: + return _HTML_TAG_RE.sub("", text) + +RELATION_TYPE_HAS_ROLE = "4" +RELATION_TYPE_IS_A = "5" + +# ChEBI's role-ontology roots (CHEBI:50906 "role"'s direct children) -- a has_role +# target is classified by walking its own is_a ancestry for one of these two ids. +# "application" (CHEBI:33232), the third root, isn't surfaced -- not requested. +CHEBI_ID_BIOLOGICAL_ROLE = "24432" +CHEBI_ID_CHEMICAL_ROLE = "51086" + +# How far to walk up `is_a` ancestry: role classification (shallow ontology, ~1-4 hops +# in practice) gets a bit more headroom than role-collection on the queried compound +# (kept shallow deliberately -- climbing further starts pulling in unrelated purely +# structural classes, e.g. erythromycin A's other is_a parent "cyclic ketone", which +# carry no roles of their own but would otherwise cost unbounded fan-out for nothing). +MAX_ROLE_CLASSIFICATION_HOPS = 10 +MAX_ROLE_COLLECTION_HOPS = 4 + + +def download_chebi_flat_files(dest_dir: str | Path, *, force: bool = False) -> Path: + """Download compounds.tsv.gz/structures.tsv.gz/relation.tsv.gz into `dest_dir` + (no-op if all three already exist, unless `force`).""" + dest_dir = Path(dest_dir).expanduser() + dest_dir.mkdir(parents=True, exist_ok=True) + + if not force and all((dest_dir / name).exists() for name in CHEBI_FILES): + return dest_dir + + for name in CHEBI_FILES: + url = f"{CHEBI_FLAT_FILES_URL}/{name}" + dest = dest_dir / name + log.info("downloading %s", url) + with urllib.request.urlopen(url) as resp, open(dest, "wb") as out: + total = int(resp.headers.get("Content-Length") or 0) or None + with tqdm(total=total, desc=f"download_chebi[{name}]", unit="B", unit_scale=True, unit_divisor=1024) as pbar: + while chunk := resp.read(1024 * 1024): + out.write(chunk) + pbar.update(len(chunk)) + + return dest_dir + + +@dataclass(frozen=True) +class ChebiRole: + label: str + chebi_accession: str # e.g. "CHEBI:35703" -- the role term's own id, for linking out + + +@dataclass(frozen=True) +class ChebiRoles: + chebi_id: str # the matched compound's own CHEBI accession, e.g. "CHEBI:42355" + biological_roles: list[ChebiRole] + chemical_roles: list[ChebiRole] + + +class ChebiDB: + """In-memory index over ChEBI's flat files, built once per pipeline run.""" + + def __init__( + self, + *, + chebi_accession_by_compound_id: dict[str, str], + name_by_compound_id: dict[str, str], + compound_id_by_inchikey: dict[str, str], + is_a_parents: dict[str, list[str]], + has_role: dict[str, list[str]], + ) -> None: + self._chebi_accession_by_compound_id = chebi_accession_by_compound_id + self._name_by_compound_id = name_by_compound_id + self._compound_id_by_inchikey = compound_id_by_inchikey + self._is_a_parents = is_a_parents + self._has_role = has_role + + @classmethod + def load(cls, chebi_dir: str | Path) -> "ChebiDB": + chebi_dir = Path(chebi_dir).expanduser() + + chebi_accession_by_compound_id: dict[str, str] = {} + name_by_compound_id: dict[str, str] = {} + with gzip.open(chebi_dir / "compounds.tsv.gz", "rt", encoding="utf-8", errors="replace") as fh: + reader = csv.reader(fh, delimiter="\t", quotechar='"') + header = next(reader) + id_idx = header.index("id") + name_idx = header.index("name") + accession_idx = header.index("chebi_accession") + for row in reader: + if len(row) <= accession_idx or not row[accession_idx]: + continue + chebi_accession_by_compound_id[row[id_idx]] = row[accession_idx] + name_by_compound_id[row[id_idx]] = _strip_html(row[name_idx]) + + compound_id_by_inchikey: dict[str, str] = {} + with gzip.open(chebi_dir / "structures.tsv.gz", "rt", encoding="utf-8", errors="replace") as fh: + reader = csv.reader(fh, delimiter="\t", quotechar='"') + header = next(reader) + cid_idx = header.index("compound_id") + key_idx = header.index("standard_inchi_key") + for row in reader: + if len(row) <= key_idx or not row[key_idx]: + continue + compound_id_by_inchikey.setdefault(row[key_idx], row[cid_idx]) + + is_a_parents: dict[str, list[str]] = {} + has_role: dict[str, list[str]] = {} + with gzip.open(chebi_dir / "relation.tsv.gz", "rt", encoding="utf-8", errors="replace") as fh: + reader = csv.reader(fh, delimiter="\t", quotechar='"') + header = next(reader) + type_idx = header.index("relation_type_id") + init_idx = header.index("init_id") + final_idx = header.index("final_id") + for row in reader: + if len(row) <= final_idx: + continue + rtype, init_id, final_id = row[type_idx], row[init_idx], row[final_idx] + if rtype == RELATION_TYPE_IS_A: + is_a_parents.setdefault(init_id, []).append(final_id) + elif rtype == RELATION_TYPE_HAS_ROLE: + has_role.setdefault(init_id, []).append(final_id) + + log.info( + "loaded ChEBI flat files: %d compounds, %d structures, %d is_a edges, %d has_role edges", + len(chebi_accession_by_compound_id), len(compound_id_by_inchikey), len(is_a_parents), len(has_role), + ) + return cls( + chebi_accession_by_compound_id=chebi_accession_by_compound_id, + name_by_compound_id=name_by_compound_id, + compound_id_by_inchikey=compound_id_by_inchikey, + is_a_parents=is_a_parents, + has_role=has_role, + ) + + def _walk_is_a(self, start: str, max_hops: int): + """Yield ancestor compound ids reachable from `start` via `is_a`, breadth-first, + up to `max_hops` levels, never revisiting a node.""" + seen = {start} + frontier = [start] + for _ in range(max_hops): + next_frontier = [] + for cid in frontier: + for parent in self._is_a_parents.get(cid, []): + if parent not in seen: + seen.add(parent) + next_frontier.append(parent) + yield parent + if not next_frontier: + return + frontier = next_frontier + + def _role_category(self, role_compound_id: str) -> str | None: + """Classify a has_role target by walking its own is_a ancestry for one of + CHEBI_ID_BIOLOGICAL_ROLE/CHEBI_ID_CHEMICAL_ROLE. None if neither is hit (e.g. + it falls under "application" instead, or the chain doesn't resolve).""" + if role_compound_id == CHEBI_ID_BIOLOGICAL_ROLE: + return "biological_role" + if role_compound_id == CHEBI_ID_CHEMICAL_ROLE: + return "chemical_role" + for ancestor in self._walk_is_a(role_compound_id, MAX_ROLE_CLASSIFICATION_HOPS): + if ancestor == CHEBI_ID_BIOLOGICAL_ROLE: + return "biological_role" + if ancestor == CHEBI_ID_CHEMICAL_ROLE: + return "chemical_role" + return None + + def roles_for_inchikey(self, inchikey: str) -> ChebiRoles | None: + """None if `inchikey` has no match in ChEBI at all. Roles are collected from + the matched compound and a few levels of its `is_a` ancestors (see module + docstring -- ChEBI usually annotates roles on a generic parent class, not every + specific stereo-defined child structure).""" + compound_id = self._compound_id_by_inchikey.get(inchikey) + if compound_id is None: + return None + + role_compound_ids: set[str] = set(self._has_role.get(compound_id, [])) + for ancestor in self._walk_is_a(compound_id, MAX_ROLE_COLLECTION_HOPS): + role_compound_ids.update(self._has_role.get(ancestor, [])) + + biological_roles: list[ChebiRole] = [] + chemical_roles: list[ChebiRole] = [] + for role_id in role_compound_ids: + label = self._name_by_compound_id.get(role_id) + accession = self._chebi_accession_by_compound_id.get(role_id) + if not label or not accession: + continue + category = self._role_category(role_id) + if category == "biological_role": + biological_roles.append(ChebiRole(label=label, chebi_accession=accession)) + elif category == "chemical_role": + chemical_roles.append(ChebiRole(label=label, chebi_accession=accession)) + + return ChebiRoles( + chebi_id=self._chebi_accession_by_compound_id.get(compound_id, compound_id), + biological_roles=biological_roles, + chemical_roles=chemical_roles, + ) diff --git a/database/scripts/common.py b/database/scripts/common.py new file mode 100644 index 0000000..2668725 --- /dev/null +++ b/database/scripts/common.py @@ -0,0 +1,268 @@ +"""Shared helpers for the database-construction pipeline. + +The fingerprint recipe here (vocabulary = every matching rule's name + pseudonyms, +Fingerprinter with n_bits=FINGERPRINT_SIZE, n_hashes=2) must match +gui/src/server/routes/discovery.py's `_build_context` exactly -- that's what the +webapp uses to encode a query at search time. Deviating here would silently make +every fingerprint stored by this pipeline incomparable to a live query. +""" + +from multiprocessing import Pool +from pathlib import Path +from typing import Any, Iterable, Iterator + +from rdkit import RDLogger + +from retromol.io.streaming import ResultEvent, _init_worker, _process_compound, _task_buffered_iterator +from retromol.model.result import Result +from retromol.model.rules import MatchingRule, RuleSet +from retromol_database.duckdb import FINGERPRINT_SIZE +from retromol_fingerprint.fingerprint import TOKEN_LINK, Fingerprinter, Vocabulary + +# Silences RDKit's kekulization/valence/etc. warnings in *this* (single) process -- +# every pipeline script imports common, so this alone covers create_db.py, +# load_compounds.py, load_bgcs.py, and parse_gbks.py's main process. It does NOT +# reliably reach parse_compounds.py's multiprocessing workers: those are spawned +# fresh by retromol.io.streaming.run_retromol_stream's own Pool, with its own +# initializer, and whether a fresh worker re-runs this module-level call at all +# depends on whether the *original* entry point was this script directly (works) +# or something else re-importing it, like Snakemake's generated run: script +# (doesn't) -- see run_retromol_stream_quiet below for the reliable fix. +RDLogger.DisableLog("rdApp.*") + +# Group-level PKS pseudo-tokens a BGC's PKS module resolves to (see +# retromol_antismash.modules.PKSExtenderUnit / module_primary_sequence_tokens). +# Not matching-rule names themselves, but already part of the fingerprint +# vocabulary as pseudonyms of every rule at that reduction level. +PK_GROUP_TOKENS = ("PK_A", "PK_B", "PK_C", "PK_D") + +MIBIG_URL_TEMPLATE = "https://mibig.secondarymetabolites.org/repository/{accession}.{version}/index.html#r1c1" +NPATLAS_URL_TEMPLATE = "https://www.npatlas.org/explore/compounds/{npaid}" + + +def load_ruleset( + reaction_rules_path: str | Path | None, + matching_rules_path: str | Path | None, + match_stereochemistry: bool = False, +) -> RuleSet: + """Load a RuleSet, falling back to RetroMol's bundled default rules when a path is None/empty.""" + return RuleSet.load_from_files( + reaction_rules_path=reaction_rules_path or None, + matching_rules_path=matching_rules_path or None, + match_stereochemistry=match_stereochemistry, + ) + + +def build_fingerprint_context(ruleset: RuleSet) -> tuple[dict[str, MatchingRule], Fingerprinter]: + """Build the (name_to_rule, fingerprinter) pair used to fingerprint primary sequences.""" + vocab_tokens: set[str] = set() + name_to_rule: dict[str, MatchingRule] = {} + + for rule in ruleset.matching_rules: + vocab_tokens.add(rule.name) + vocab_tokens.update(rule.pseudonyms) + name_to_rule.setdefault(rule.name, rule) + + vocab = Vocabulary(vocab_tokens) + fingerprinter = Fingerprinter(vocab, n_bits=FINGERPRINT_SIZE, n_hashes=2) + return name_to_rule, fingerprinter + + +def per_monomer_tokens(name: str, name_to_rule: dict[str, MatchingRule]) -> list[str]: + """Fingerprinting token list for one compound primary sequence block (mirrors discovery.py's `_per_monomer_tokens`). + + TOKEN_LINK is not a building block -- it just joins two merged paths -- and is + filtered out by callers before fingerprinting (see load_compounds.py), so it's + never actually looked up here. Handled explicitly anyway so a stray call can't + silently fall through to the empty-tokens/TOKEN_UNK path instead. + """ + if name == TOKEN_LINK: + return [TOKEN_LINK] + + if name in PK_GROUP_TOKENS: + return [name, "PK"] + + rule = name_to_rule.get(name) + if rule is None: + return [] + + tokens = {rule.name} + tokens.update(rule.pseudonyms) + return list(tokens) + + +def find_key_ci(record: dict[str, Any], substrings: list[str]) -> str | None: + """Find the first key in `record` whose lowercased form contains any of `substrings`.""" + for key in record: + lowered = str(key).lower() + if any(sub in lowered for sub in substrings): + return key + return None + + +def mibig_url(accession: str | None, version: str | None) -> str | None: + if not accession or not version: + return None + return MIBIG_URL_TEMPLATE.format(accession=accession, version=version) + + +def npatlas_url(npaid: str | None) -> str | None: + if not npaid: + return None + return NPATLAS_URL_TEMPLATE.format(npaid=npaid) + + +def primary_sequence_from_result(result: Result) -> list[str]: + """ + The single primary sequence for a parsed compound, read directly off + `result.linear_readout` -- no backbone reconstruction involved, that's a + display-only concern this pipeline has no use for. `result.linear_readout` is + already computed by retromol.pipelines.parsing.run_retromol (it's just a field + on Result); every path through the molecule -- including single-node paths for + tailoring events that don't connect to any chain, e.g. glycosylation/methylation + (AssemblyGraph only keeps C-C/C-N bonds as "connections", so a sugar attached via + a glycosidic C-O-C linkage always ends up disconnected from the main chain) -- + is merged into one sequence (longest first, ties broken lexicographically, joined + by TOKEN_LINK -- see `LinearReadout.primary_sequence`), and that single sequence + becomes this compound's one db entry (see load_compounds.py). Nothing found in + the assembly graph is dropped. An unidentified node is named "X", the same + convention used everywhere else in RetroMol. + + :param result: a parsed RetroMol Result + :return: the single primary sequence + """ + return result.linear_readout.primary_sequence() + + +def _init_worker_quiet(ruleset: RuleSet) -> None: + """Worker-process initializer: set up the ruleset global exactly like retromol.io.streaming's own + _init_worker does, then also disable RDKit logging -- unlike a module-level RDLogger call, this is + guaranteed to run once per worker process no matter how the pool was launched.""" + _init_worker(ruleset) + RDLogger.DisableLog("rdApp.*") + + +def run_retromol_stream_quiet( + ruleset: RuleSet, + row_iter: Iterable[dict[str, Any]], + smiles_col: str = "smiles", + workers: int = 1, + batch_size: int = 2000, + pool_chunksize: int = 50, + maxtasksperchild: int = 2000, +) -> Iterator[ResultEvent]: + """ + Drop-in replacement for retromol.io.streaming.run_retromol_stream that also + disables RDKit's C-level logging inside every worker process. Reuses that + module's own batching/worker-task functions -- only the Pool's initializer + differs (see _init_worker_quiet). + """ + with Pool( + processes=workers, + initializer=_init_worker_quiet, + initargs=(ruleset,), + maxtasksperchild=maxtasksperchild, + ) as pool: + for task_batch in _task_buffered_iterator(row_iter, smiles_col=smiles_col, batch_size=batch_size): + for serialized, err in pool.imap_unordered(_process_compound, task_batch, chunksize=pool_chunksize): + yield ResultEvent(serialized, err) + + +# Common secondary-metabolite-producing fungal genera -- MIBiG's JSON has no direct +# kingdom/type field (unlike NPAtlas), so `phylogeny_from_organism_name` falls back to +# this bundled set to distinguish fungal from bacterial entries. MIBiG is overwhelmingly +# bacterial, so "bacterium" is the default and this set only needs to catch the fungal +# minority. Not exhaustive -- a genus missing from this list is classified "bacterium". +FUNGAL_GENERA = { + "aspergillus", "penicillium", "fusarium", "trichoderma", "curvularia", + "colletotrichum", "alternaria", "cladosporium", "talaromyces", "chaetomium", + "acremonium", "beauveria", "metarhizium", "monascus", "epicoccum", + "pestalotiopsis", "phoma", "botrytis", "verticillium", "myrothecium", +} + + +# Metagenomic/environmental-sample naming conventions (e.g. "uncultured Streptomyces sp.", +# "unidentified bacterium") -- not a genus, so skipped when picking the genus token, and +# rejected outright if a whole genus/species value collapses to just one of these. +_NON_TAXONOMIC_PREFIXES = {"uncultured", "unclassified", "unidentified"} + +# Species-epithet placeholders meaning "no real species-level identification" -- checked +# against a species token after any leading genus-name duplicate is stripped (see +# clean_species_epithet), not just the bare "sp."/"sp" abbreviation. Shared by both +# MIBiG's free-text organism_name and NPAtlas's own origin_species SDF field, which turn +# out to carry the same kinds of placeholder values (confirmed live: NPAtlas's +# origin_species includes bare "sp.", genus-duplicated "Streptomyces sp.", and +# "unidentified" -- none of which are real species, but nothing was rejecting them before +# this, so they leaked into the phylogeny_annotations species column as if they were). +_NON_TAXONOMIC_SPECIES_TOKENS = {"sp", "spp", "unidentified", "uncultured", "unclassified"} + + +def clean_genus(genus_raw: str | None) -> str | None: + """Reject a genus value that's actually a non-taxonomic placeholder + ("unidentified"/"uncultured"/"unclassified") rather than a real genus name.""" + if not genus_raw: + return None + genus = genus_raw.strip() + if not genus or genus.lower() in _NON_TAXONOMIC_PREFIXES: + return None + return genus + + +def clean_species_epithet(genus: str | None, species_raw: str | None) -> str | None: + """Reject a species value carrying no real species-level information: a bare + "sp."/"sp" abbreviation, a genus name duplicated into the species field (e.g. + NPAtlas's origin_species="Streptomyces sp." alongside genus="Streptomyces"), or an + "unidentified"/"uncultured"/"unclassified" placeholder.""" + if not species_raw: + return None + + species = species_raw.strip() + if genus and species.lower().startswith(genus.lower() + " "): + species = species[len(genus):].strip() + + if not species or species.rstrip(".").lower() in _NON_TAXONOMIC_SPECIES_TOKENS: + return None + + return species + + +def phylogeny_from_organism_name(organism_name: str | None) -> tuple[str | None, str | None, str | None]: + """Split MIBiG's free-text `organism_name` (e.g. "Streptomyces coelicolor A3(2)") + into (type, genus, species). Type is inferred from `FUNGAL_GENERA` since MIBiG's + JSON carries no kingdom field; genus/species are the name's first two tokens (after + dropping a leading "uncultured"/"unclassified"/"unidentified" marker), with a + non-taxonomic species value (see clean_species_epithet) dropped. + + :param organism_name: MIBiG cluster.organism_name, or None + :return: (type_label, genus, species) -- each may be None if unresolvable + """ + if not organism_name: + return None, None, None + + tokens = organism_name.split() + if tokens and tokens[0].lower() in _NON_TAXONOMIC_PREFIXES: + tokens = tokens[1:] + if not tokens: + return None, None, None + + genus = clean_genus(tokens[0]) + if not genus: + return None, None, None + + species = clean_species_epithet(genus, tokens[1] if len(tokens) > 1 else None) + + type_label = "Fungus" if genus.lower() in FUNGAL_GENERA else "Bacterium" + return type_label, genus, species + + +def split_accession_version(record_id: str) -> tuple[str, str | None]: + """ + Split a GenBank-style "ACCESSION.VERSION" id (e.g. "BGC0000001.5") in two. + + :param record_id: the record id, as set on retromol_antismash Region/LinearReadout.id + :return: (accession, version) -- version is None if record_id has no "." suffix + """ + if "." in record_id: + accession, version = record_id.rsplit(".", 1) + return accession, version + return record_id, None diff --git a/database/scripts/create_db.py b/database/scripts/create_db.py new file mode 100644 index 0000000..1f96d06 --- /dev/null +++ b/database/scripts/create_db.py @@ -0,0 +1,26 @@ +"""Step 1: create an empty RetroMol database.""" + +import argparse +from pathlib import Path + +from retromol_database.duckdb import RetroMolDuckDB + + +def run(db_path: str | Path, overwrite: bool = True) -> None: + db_path = Path(db_path) + db_path.parent.mkdir(parents=True, exist_ok=True) + db = RetroMolDuckDB.create(db_path, overwrite=overwrite) + db.close() + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--db-path", required=True) + ap.add_argument("--overwrite", action="store_true") + args = ap.parse_args() + + run(db_path=args.db_path, overwrite=args.overwrite) + + +if __name__ == "__main__": + main() diff --git a/database/scripts/download_sources.py b/database/scripts/download_sources.py new file mode 100644 index 0000000..8b91496 --- /dev/null +++ b/database/scripts/download_sources.py @@ -0,0 +1,92 @@ +"""Step 2: download a source file/archive and optionally extract it. + +Handles the two shapes the pipeline needs: a single (possibly gzipped) file, like +the NPAtlas SDF, and a tar.gz/zip archive containing many files, like MIBiG's JSON +and GenBank bundles. +""" + +import argparse +import gzip +import shutil +import tarfile +import zipfile +from pathlib import Path +from urllib.parse import urlsplit + +import requests +from tqdm import tqdm + + +def download(url: str, dest: str | Path) -> None: + dest = Path(dest) + dest.parent.mkdir(parents=True, exist_ok=True) + with requests.get(url, stream=True, timeout=300) as resp: + resp.raise_for_status() + total = int(resp.headers.get("Content-Length") or 0) or None + with open(dest, "wb") as fh: + with tqdm(total=total, desc=f"download[{dest.name}]", unit="B", unit_scale=True, unit_divisor=1024) as pbar: + for chunk in resp.iter_content(chunk_size=1024 * 1024): + if chunk: + fh.write(chunk) + pbar.update(len(chunk)) + + +def _url_filename(url: str) -> str: + """Best-effort original filename from a URL's path (ignores query strings).""" + name = Path(urlsplit(url).path).name + return name or "download" + + +def extract(path: str | Path, out_dir: str | Path, source_name: str | None = None) -> None: + """ + Extract/decompress a downloaded file, or copy it through unchanged. + + :param path: the downloaded file on disk -- often a fixed, extension-less name + (e.g. "download.raw") the caller chose for the raw download, so it can't be + used to decide *how* to extract or to name a plain copy/decompressed output. + :param out_dir: directory to extract/copy into. + :param source_name: the original filename (e.g. from the source URL), used both + to pick the extraction strategy and, for the plain-copy/gzip cases, to name + the resulting file so its extension survives (e.g. "*.sdf" stays findable). + Falls back to `path`'s own name if not given. + """ + path = Path(path) + out_dir = Path(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + + display_name = source_name or path.name + name = display_name.lower() + + if name.endswith((".tar.gz", ".tgz", ".tar")): + mode = "r:gz" if name.endswith((".tar.gz", ".tgz")) else "r" + with tarfile.open(path, mode) as tf: + tf.extractall(out_dir) + elif name.endswith(".zip"): + with zipfile.ZipFile(path) as zf: + zf.extractall(out_dir) + elif name.endswith(".gz"): + out_path = out_dir / Path(display_name).with_suffix("").name + with gzip.open(path, "rb") as fin, open(out_path, "wb") as fout: + shutil.copyfileobj(fin, fout) + else: + shutil.copy2(path, out_dir / display_name) + + +def run(url: str, download_path: str | Path, extract_dir: str | Path | None = None) -> None: + download(url, download_path) + if extract_dir is not None: + extract(download_path, extract_dir, source_name=_url_filename(url)) + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--url", required=True) + ap.add_argument("--download-path", required=True, help="where the raw downloaded file is stored") + ap.add_argument("--extract-dir", default=None, help="if set, extract/decompress the download into this directory") + args = ap.parse_args() + + run(url=args.url, download_path=args.download_path, extract_dir=args.extract_dir) + + +if __name__ == "__main__": + main() diff --git a/database/scripts/extract_mibig_compounds.py b/database/scripts/extract_mibig_compounds.py new file mode 100644 index 0000000..e0baf0d --- /dev/null +++ b/database/scripts/extract_mibig_compounds.py @@ -0,0 +1,199 @@ +"""Step 5 (prep): flatten MIBiG's per-BGC JSON files into one compounds JSONL, +plus an accession -> version map (also consumed by load_bgcs.py, see versions_output). + +MIBiG's JSON schema has shifted over releases (older releases nest everything +under a "cluster" key; newer ones are flatter), so field lookup here is +deliberately tolerant -- entries that don't have a resolvable accession/SMILES +are skipped (and counted) rather than raising. + +The accession -> version map used to come from the GBK files' own +ACCESSION.VERSION line (see common.split_accession_version) -- that broke with +MIBiG 4.0, whose GBKs dropped the version suffix entirely ("ACCESSION +BGC0000001", no "VERSION" line at all). The JSON files still carry it, as a +top-level "version" int (e.g. 5, matching the ".5" in +https://mibig.secondarymetabolites.org/repository/BGC0000001.5/index.html), +so that's the source of truth now -- extracted here, unconditionally (even for +files with no usable compound records), since parse_gbks.py can no longer +provide it. +""" + +import argparse +import json +import logging +from pathlib import Path +from typing import Any, Iterator + +from tqdm import tqdm + +log = logging.getLogger(__name__) + + +def _entry_root(data: dict[str, Any]) -> dict[str, Any]: + """Older MIBiG JSON nests everything under "cluster"; newer releases are flat.""" + cluster = data.get("cluster") + return cluster if isinstance(cluster, dict) else data + + +def _accession(data: dict[str, Any], root: dict[str, Any]) -> str | None: + return root.get("mibig_accession") or root.get("accession") or data.get("accession") + + +def _version(data: dict[str, Any], root: dict[str, Any]) -> str | None: + version = data.get("version") + if version is None: + version = root.get("version") + return str(version) if version is not None else None + + +def _iter_compound_records(data: dict[str, Any], root: dict[str, Any], accession: str) -> Iterator[dict[str, Any]]: + compounds = root.get("compounds") + if not isinstance(compounds, list): + return + + for idx, compound in enumerate(compounds): + if not isinstance(compound, dict): + continue + + smiles = compound.get("chem_struct") or compound.get("smiles") or compound.get("structure") + if not smiles: + continue + + name = compound.get("compound") or compound.get("name") or f"{accession} compound {idx + 1}" + + yield { + "id": f"{accession}:{idx}", + "smiles": smiles, + "name": name, + "mibig_accession": accession, + } + + +# MIBiG's own short biosynthesis-class codes -> friendlier display labels for the +# chemical_class annotation. Covers every value observed across the full 4.0 corpus +# (PKS/NRPS/ribosomal/other/terpene/saccharide); an unrecognized future code falls +# back to itself unchanged rather than being dropped. +BIOSYN_CLASS_LABELS = { + "PKS": "Polyketide", + "NRPS": "Nonribosomal peptide", + "ribosomal": "RiPP", + "terpene": "Terpene", + "saccharide": "Saccharide", + "other": "Other", +} + + +def _annotations(root: dict[str, Any]) -> dict[str, Any]: + """Phylogeny + chemical-class metadata shared by every compound/BGC under one accession. + + MIBiG 4.0's real (flat) schema nests these under "taxonomy" ({"name", "ncbiTaxId"}) + and "biosynthesis" ({"classes": [{"class": "PKS", ...}, ...]}) -- not the top-level + "organism_name"/"ncbi_tax_id"/"biosyn_class" keys older MIBiG releases used. + """ + taxonomy = root.get("taxonomy") + taxonomy = taxonomy if isinstance(taxonomy, dict) else {} + organism_name = taxonomy.get("name") + ncbi_tax_id = taxonomy.get("ncbiTaxId") + + biosynthesis = root.get("biosynthesis") + biosynthesis = biosynthesis if isinstance(biosynthesis, dict) else {} + classes = biosynthesis.get("classes") + classes = classes if isinstance(classes, list) else [] + biosyn_class = [ + BIOSYN_CLASS_LABELS.get(c.get("class"), c.get("class")) + for c in classes + if isinstance(c, dict) and c.get("class") + ] + + return { + "organism_name": organism_name if isinstance(organism_name, str) else None, + "ncbi_tax_id": str(ncbi_tax_id) if ncbi_tax_id is not None else None, + "biosyn_class": biosyn_class, + } + + +def run( + mibig_json_dir: str | Path, + output_path: str | Path, + versions_output_path: str | Path, + annotations_output_path: str | Path, +) -> None: + mibig_json_dir = Path(mibig_json_dir) + output_path = Path(output_path) + versions_output_path = Path(versions_output_path) + annotations_output_path = Path(annotations_output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + versions_output_path.parent.mkdir(parents=True, exist_ok=True) + annotations_output_path.parent.mkdir(parents=True, exist_ok=True) + + json_files = sorted(mibig_json_dir.rglob("*.json")) + written = 0 + skipped = 0 + versions: dict[str, str] = {} + annotations: dict[str, dict[str, Any]] = {} + + with open(output_path, "w") as out: + for path in tqdm(json_files, desc="extract_mibig_compounds", unit="file"): + try: + with open(path) as fh: + data = json.load(fh) + + if not isinstance(data, dict): + skipped += 1 + continue + + root = _entry_root(data) + accession = _accession(data, root) + if not accession: + skipped += 1 + continue + + version = _version(data, root) + if version is not None: + versions[accession] = version + + annotations[accession] = _annotations(root) + + had_any = False + for record in _iter_compound_records(data, root, accession): + out.write(json.dumps(record) + "\n") + written += 1 + had_any = True + if not had_any: + skipped += 1 + except Exception: + log.exception("failed to parse MIBiG JSON file: %s", path) + skipped += 1 + + with open(versions_output_path, "w") as fh: + json.dump(versions, fh, indent=2, sort_keys=True) + + with open(annotations_output_path, "w") as fh: + json.dump(annotations, fh, indent=2, sort_keys=True) + + log.info( + "extract_mibig_compounds: wrote %d compound records, skipped %d files, " + "resolved %d accession versions, %d accession annotations", + written, skipped, len(versions), len(annotations), + ) + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--mibig-json-dir", required=True) + ap.add_argument("--output", required=True) + ap.add_argument("--versions-output", required=True) + ap.add_argument("--annotations-output", required=True) + args = ap.parse_args() + + run( + mibig_json_dir=args.mibig_json_dir, + output_path=args.output, + versions_output_path=args.versions_output, + annotations_output_path=args.annotations_output, + ) + + +if __name__ == "__main__": + main() diff --git a/database/scripts/load_bgcs.py b/database/scripts/load_bgcs.py new file mode 100644 index 0000000..99b6bc5 --- /dev/null +++ b/database/scripts/load_bgcs.py @@ -0,0 +1,177 @@ +"""Step 8: turn parsed BGC readouts into database entries. + +Each antiSMASH region becomes one "bgc" entry. bgc_primary_sequence maps every +module's predicted substrate onto the same matching-rule vocabulary compound +primary sequences use (PKS modules resolve to a reduction-level pseudonym like +"PK_A", NRPS modules resolve by matching PARAS' predicted substrate structurally +against the ruleset) -- see retromol_antismash.modules for why the two module +types resolve differently. That shared vocabulary is what makes a BGC entry's +fingerprint and primary sequence comparable to a compound's. + +The MIBiG URL needs an accession's version, which parse_gbks.py can no longer +provide (MIBiG 4.0's GBKs dropped the ACCESSION.VERSION suffix) -- so it's +looked up here instead, from the same JSON-derived mibig_versions.json +extract_mibig_compounds.py produces for load_compounds.py. + +BGCs are deduplicated per source .gbk file via `file_hash` (parse_gbks.py's +sha256 of the whole file's raw text): a region's entry id is derived from that +hash plus its own readout id, and a file whose hash is already stored (checked +via RetroMolDuckDB.bgc_content_hash_exists) is skipped outright, so rerunning +the pipeline over an already-ingested file doesn't redo any fingerprinting work. +""" + +import argparse +import hashlib +import json +import logging +from pathlib import Path + +from tqdm import tqdm + +from common import build_fingerprint_context, load_ruleset, mibig_url, phylogeny_from_organism_name +from retromol_antismash.modules import LinearReadout, bgc_primary_sequence +from retromol_database.duckdb import RetroMolDuckDB +from taxonomy import TaxonomyDB, resolve_phylogeny + +log = logging.getLogger(__name__) + + +def run( + readouts_path: str | Path, + db_path: str | Path, + reaction_rules_path: str | Path | None, + matching_rules_path: str | Path | None, + mibig_versions_path: str | Path, + mibig_annotations_path: str | Path, + match_stereochemistry: bool = False, + include_raw_gbk: bool = True, + taxdump_dir: str | Path | None = None, +) -> None: + ruleset = load_ruleset(reaction_rules_path, matching_rules_path, match_stereochemistry) + _, fingerprinter = build_fingerprint_context(ruleset) + + taxdb = TaxonomyDB.load(taxdump_dir) if taxdump_dir else None + + with open(mibig_versions_path) as fh: + versions: dict[str, str] = json.load(fh) + + with open(mibig_annotations_path) as fh: + annotations: dict[str, dict] = json.load(fh) + + added = 0 + skipped = 0 + skipped_existing_file = 0 + + db = RetroMolDuckDB.open(db_path) + try: + with open(readouts_path) as fh: + pbar = tqdm(fh, desc="load_bgcs", unit="region") + for line in pbar: + line = line.strip() + if not line: + continue + + entry = json.loads(line) + file_hash = entry.get("file_hash") + + if file_hash and db.bgc_content_hash_exists(file_hash): + skipped_existing_file += 1 + pbar.set_postfix(added=added, skipped=skipped, skipped_existing=skipped_existing_file) + continue + + readout = LinearReadout.from_dict(entry["readout"]) + names, tokens = bgc_primary_sequence(readout, ruleset) + + if not names: + skipped += 1 + pbar.set_postfix(added=added, skipped=skipped, skipped_existing=skipped_existing_file) + continue + + fp = fingerprinter.encode(tokens) + accession = entry.get("accession") + name = f"{accession} ({readout.id})" if accession else readout.id + url = mibig_url(accession, versions.get(accession)) if accession else None + entry_id = hashlib.sha256(f"{file_hash}:{readout.id}".encode("utf-8")).hexdigest() + + db.add_entry( + entry_id=entry_id, + name=name, + database_name="MIBiG", + url=url, + raw=entry.get("raw_gbk") if include_raw_gbk else None, + entry_type="bgc", + primary_sequence=names, + fingerprint=fp, + content_hash=file_hash, + ) + + record = annotations.get(accession) if accession else None + if record: + fallback_type, fallback_genus, fallback_species = phylogeny_from_organism_name( + record.get("organism_name") + ) + resolution = resolve_phylogeny( + taxdb, + ncbi_tax_id=record.get("ncbi_tax_id"), + genus=fallback_genus, + species=fallback_species, + fallback_type_label=fallback_type, + ) + db.add_phylogeny_annotation( + entry_id, + type_label=resolution.type_label, + type_taxid=resolution.type_taxid, + genus=resolution.genus, + genus_taxid=resolution.genus_taxid, + species=resolution.species, + species_taxid=resolution.species_taxid, + ) + # biosynthetic_class describes this BGC's own biosynthesis machinery + # (PKS/NRPS/RiPP/...) -- a gene-cluster property, not populated for + # compounds (see RetroMolDuckDB.add_biosynthetic_class_annotation / + # load_compounds.py). + for chemical_class in record.get("biosyn_class") or []: + if chemical_class: + db.add_biosynthetic_class_annotation(entry_id, str(chemical_class)) + + added += 1 + pbar.set_postfix(added=added, skipped=skipped, skipped_existing=skipped_existing_file) + finally: + db.close() + + log.info( + "load_bgcs: added=%d skipped=%d skipped_existing_file=%d", + added, skipped, skipped_existing_file, + ) + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--readouts", required=True) + ap.add_argument("--db-path", required=True) + ap.add_argument("--rxn-rules", default=None) + ap.add_argument("--mxn-rules", default=None) + ap.add_argument("--mibig-versions", required=True) + ap.add_argument("--mibig-annotations", required=True) + ap.add_argument("--match-stereochemistry", action="store_true") + ap.add_argument("--no-raw-gbk", action="store_true") + ap.add_argument("--taxdump-dir", default=None, help="dir with NCBI names.dmp/nodes.dmp (skips taxid resolution if omitted)") + args = ap.parse_args() + + run( + readouts_path=args.readouts, + db_path=args.db_path, + reaction_rules_path=args.rxn_rules, + matching_rules_path=args.mxn_rules, + mibig_versions_path=args.mibig_versions, + mibig_annotations_path=args.mibig_annotations, + match_stereochemistry=args.match_stereochemistry, + include_raw_gbk=not args.no_raw_gbk, + taxdump_dir=args.taxdump_dir, + ) + + +if __name__ == "__main__": + main() diff --git a/database/scripts/load_compounds.py b/database/scripts/load_compounds.py new file mode 100644 index 0000000..1898591 --- /dev/null +++ b/database/scripts/load_compounds.py @@ -0,0 +1,251 @@ +"""Steps 4 & 6: turn parsed compound results into database entries. + +For each RetroMol Result, every path found in result.linear_readout -- including a +compound's tailoring events (glycosylation, methylation -- anything that shows up as +its own disconnected single-node path) -- is merged into the one primary sequence +stored for that compound: longest path first, ties broken lexicographically, joined +by TOKEN_LINK (see common.primary_sequence_from_result). `raw` is the original input +SMILES. + +Compounds are deduplicated on `result.submission.inchikey` (stereo-aware, computed by +retromol.model.submission.Submission with keep_stereo=True). The same molecule +appearing in both NPAtlas and MIBiG lands as one database entry with two source +records (see RetroMolDuckDB.add_entry) rather than two separate rows. +""" + +import argparse +import json +import logging +from pathlib import Path +from typing import Literal + +from tqdm import tqdm + +from common import ( + build_fingerprint_context, + clean_genus, + clean_species_epithet, + find_key_ci, + load_ruleset, + mibig_url, + npatlas_url, + per_monomer_tokens, + phylogeny_from_organism_name, + primary_sequence_from_result, +) +from retromol.model.result import Result +from retromol_database.duckdb import RetroMolDuckDB +from retromol_fingerprint.fingerprint import TOKEN_LINK +from taxonomy import TaxonomyDB, resolve_phylogeny + +log = logging.getLogger(__name__) + +DATABASE_NAMES = {"npatlas": "NPAtlas", "mibig": "MIBiG"} + + +def _npatlas_name_and_url(props: dict) -> tuple[str | None, str | None]: + npaid_key = find_key_ci(props, ["npaid"]) + npaid = props.get(npaid_key) if npaid_key else None + + name_key = find_key_ci(props, ["original_name", "compound_name", "name"]) + name = props.get(name_key) if name_key else None + + return name, npatlas_url(npaid) + + +def _npatlas_phylogeny(props: dict) -> tuple[str | None, str | None, str | None]: + """NPAtlas's SDF carries type/genus/species directly (unlike MIBiG's free-text + organism_name) -- see the `origin_type`/`genus`/`origin_species` SDF properties. + + Both genus and origin_species are used as-is by NPAtlas's own curators, which + turns out to include the same non-taxonomic placeholders MIBiG's free text does + (bare "sp.", a genus name duplicated into the species field e.g. "Streptomyces + sp.", "unidentified") -- cleaned the same way as MIBiG's organism_name parsing + (see common.clean_genus/clean_species_epithet), so neither source's placeholder + values end up stored as if they were real species-level identifications. + """ + type_key = find_key_ci(props, ["origin_type"]) + type_label = props.get(type_key) if type_key else None + + genus_key = find_key_ci(props, ["genus"]) + genus = clean_genus(props.get(genus_key) if genus_key else None) + + species_key = find_key_ci(props, ["origin_species"]) + species_raw = props.get(species_key) if species_key else None + species = clean_species_epithet(genus, species_raw) + + return (type_label or None), genus, species + + +def _mibig_name_and_url(props: dict, versions: dict[str, str]) -> tuple[str | None, str | None]: + accession = props.get("mibig_accession") + version = versions.get(accession) if accession else None + name = props.get("name") + return name, mibig_url(accession, version) + + +def _apply_mibig_annotations( + db: RetroMolDuckDB, entry_id: str, props: dict, annotations: dict[str, dict], taxdb: TaxonomyDB | None +) -> None: + accession = props.get("mibig_accession") + record = annotations.get(accession) if accession else None + if not record: + return + + fallback_type, fallback_genus, fallback_species = phylogeny_from_organism_name(record.get("organism_name")) + resolution = resolve_phylogeny( + taxdb, + ncbi_tax_id=record.get("ncbi_tax_id"), + genus=fallback_genus, + species=fallback_species, + fallback_type_label=fallback_type, + ) + db.add_phylogeny_annotation( + entry_id, + type_label=resolution.type_label, + type_taxid=resolution.type_taxid, + genus=resolution.genus, + genus_taxid=resolution.genus_taxid, + species=resolution.species, + species_taxid=resolution.species_taxid, + ) + # biosynthetic_class describes the BGC's own biosynthesis machinery (PKS/NRPS/...), + # not the compound structure -- populated in load_bgcs.py instead, not here. + + +def run( + results_path: str | Path, + db_path: str | Path, + source: Literal["npatlas", "mibig"], + reaction_rules_path: str | Path | None, + matching_rules_path: str | Path | None, + match_stereochemistry: bool = False, + mibig_versions_path: str | Path | None = None, + mibig_annotations_path: str | Path | None = None, + taxdump_dir: str | Path | None = None, + log_every: int = 1000, +) -> None: + ruleset = load_ruleset(reaction_rules_path, matching_rules_path, match_stereochemistry) + name_to_rule, fingerprinter = build_fingerprint_context(ruleset) + + taxdb = TaxonomyDB.load(taxdump_dir) if taxdump_dir else None + + versions: dict[str, str] = {} + if source == "mibig" and mibig_versions_path is not None: + with open(mibig_versions_path) as fh: + versions = json.load(fh) + + annotations: dict[str, dict] = {} + if source == "mibig" and mibig_annotations_path is not None: + with open(mibig_annotations_path) as fh: + annotations = json.load(fh) + + compounds = 0 + added = 0 + skipped = 0 + + db = RetroMolDuckDB.open(db_path) + try: + with open(results_path) as fh: + with tqdm(desc=f"load_compounds[{source}]", unit="cmpd") as pbar: + for line in fh: + line = line.strip() + if not line: + continue + + result = Result.from_dict(json.loads(line)) + props = result.submission.props or {} + + if source == "npatlas": + name, url = _npatlas_name_and_url(props) + else: + name, url = _mibig_name_and_url(props, versions) + + name = name or result.submission.name or result.submission.inchikey + + names = primary_sequence_from_result(result) + + if not names: + skipped += 1 + else: + # TOKEN_LINK only marks where two merged paths join -- it isn't + # a building block, so it's excluded from the fingerprint (an + # empty token list would otherwise silently add TOKEN_UNK mass). + tokens = [per_monomer_tokens(n, name_to_rule) for n in names if n != TOKEN_LINK] + fp = fingerprinter.encode(tokens) + + db.add_entry( + entry_id=result.submission.inchikey, + name=name, + database_name=DATABASE_NAMES[source], + url=url, + raw=result.submission.smiles, + entry_type="compound", + primary_sequence=names, + fingerprint=fp, + ) + if source == "mibig": + _apply_mibig_annotations(db, result.submission.inchikey, props, annotations, taxdb) + else: + type_label, genus, species = _npatlas_phylogeny(props) + resolution = resolve_phylogeny( + taxdb, genus=genus, species=species, fallback_type_label=type_label + ) + db.add_phylogeny_annotation( + result.submission.inchikey, + type_label=resolution.type_label, + type_taxid=resolution.type_taxid, + genus=resolution.genus, + genus_taxid=resolution.genus_taxid, + species=resolution.species, + species_taxid=resolution.species_taxid, + ) + added += 1 + + compounds += 1 + pbar.update(1) + pbar.set_postfix(added=added, skipped=skipped) + + if log_every > 0 and compounds % log_every == 0: + log.info( + "load_compounds[%s]: processed %d compounds (added=%d skipped=%d)", + source, compounds, added, skipped, + ) + finally: + db.close() + + log.info("load_compounds[%s]: compounds=%d added=%d skipped=%d", source, compounds, added, skipped) + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--results", required=True) + ap.add_argument("--db-path", required=True) + ap.add_argument("--source", choices=["npatlas", "mibig"], required=True) + ap.add_argument("--rxn-rules", default=None) + ap.add_argument("--mxn-rules", default=None) + ap.add_argument("--match-stereochemistry", action="store_true") + ap.add_argument("--mibig-versions", default=None, help="required when --source=mibig") + ap.add_argument("--mibig-annotations", default=None, help="required when --source=mibig") + ap.add_argument("--taxdump-dir", default=None, help="dir with NCBI names.dmp/nodes.dmp (skips taxid resolution if omitted)") + ap.add_argument("--log-every", type=int, default=1000, help="log a progress line every N compounds (0 to disable)") + args = ap.parse_args() + + run( + results_path=args.results, + db_path=args.db_path, + source=args.source, + reaction_rules_path=args.rxn_rules, + matching_rules_path=args.mxn_rules, + match_stereochemistry=args.match_stereochemistry, + mibig_versions_path=args.mibig_versions, + mibig_annotations_path=args.mibig_annotations, + taxdump_dir=args.taxdump_dir, + log_every=args.log_every, + ) + + +if __name__ == "__main__": + main() diff --git a/database/scripts/npclassifier.py b/database/scripts/npclassifier.py new file mode 100644 index 0000000..2276f17 --- /dev/null +++ b/database/scripts/npclassifier.py @@ -0,0 +1,84 @@ +"""NPClassifier API client for chemical-class annotation. + +Classifies a compound's chemical class directly from its own structure via GNPS2's +free, public NPClassifier service (https://npclassifier.gnps2.org/classify?smiles=...) -- +independent of MIBiG's coarse biosynthetic-class labels (PKS/NRPS/...), and available +for NPAtlas compounds too, which MIBiG's biosyn_class never was. A classification has +three list-valued levels (pathway/superclass/class, most to least general) plus a single +is_glycoside boolean; response shape confirmed live against the API (2026-08-26): +{"pathway_results": [...], "superclass_results": [...], "class_results": [...], "isglycoside": bool}. + +No published rate limit exists for this service, so callers are responsible for pacing +requests themselves (see annotate_npclassifier.py's --requests-per-second) -- this module +only wraps a single call with retry/backoff. +""" + +from __future__ import annotations + +import json +import logging +import time +import urllib.error +import urllib.parse +import urllib.request +from dataclasses import dataclass + +log = logging.getLogger(__name__) + +NPCLASSIFIER_URL = "https://npclassifier.gnps2.org/classify" + + +@dataclass(frozen=True) +class ClassificationResult: + pathway: list[str] + superclass: list[str] + class_: list[str] + is_glycoside: bool + + +def classify_smiles( + smiles: str, + *, + timeout: float = 30.0, + max_retries: int = 3, + backoff_seconds: float = 2.0, +) -> ClassificationResult | None: + """Classify one SMILES via NPClassifier, retrying transient failures (timeouts, 5xx, + 429, malformed JSON) with linear backoff. Returns None (logged, not raised) if every + attempt fails -- one unclassifiable/unreachable-service molecule shouldn't abort a + whole pipeline run.""" + url = f"{NPCLASSIFIER_URL}?smiles={urllib.parse.quote(smiles, safe='')}" + + for attempt in range(1, max_retries + 1): + try: + with urllib.request.urlopen(url, timeout=timeout) as resp: + data = json.loads(resp.read()) + return ClassificationResult( + pathway=[str(p) for p in data.get("pathway_results") or []], + superclass=[str(s) for s in data.get("superclass_results") or []], + class_=[str(c) for c in data.get("class_results") or []], + is_glycoside=bool(data.get("isglycoside", False)), + ) + except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError, TimeoutError, OSError) as exc: + if isinstance(exc, urllib.error.HTTPError) and exc.code == 429: + # Rate-limited: back off much harder than a transient failure, and + # respect Retry-After if the server sent one -- retrying at the same + # pace that just got us 429'd only makes the storm worse. + retry_after = exc.headers.get("Retry-After") if exc.headers else None + try: + delay = float(retry_after) if retry_after is not None else backoff_seconds * attempt * 5 + except ValueError: + delay = backoff_seconds * attempt * 5 + log.warning( + "NPClassifier rate-limited (429) on attempt %d/%d for %r -- backing off %.1fs", + attempt, max_retries, smiles, delay, + ) + else: + delay = backoff_seconds * attempt + log.warning("NPClassifier request failed (attempt %d/%d) for %r: %s", attempt, max_retries, smiles, exc) + + if attempt < max_retries: + time.sleep(delay) + + log.error("NPClassifier: giving up on %r after %d attempts", smiles, max_retries) + return None diff --git a/database/scripts/parse_compounds.py b/database/scripts/parse_compounds.py new file mode 100644 index 0000000..6d4b33a --- /dev/null +++ b/database/scripts/parse_compounds.py @@ -0,0 +1,106 @@ +"""Steps 3 & 5: run RetroMol over a batch of compounds (NPAtlas SDF or MIBiG compounds JSONL). + +Shared between both compound sources -- only the input format differs. This is the +slow step (one retrosynthetic analysis per compound), so it's parallelized with +`workers` worker processes via common.run_retromol_stream_quiet -- the same +worker/batching machinery the `retromol` CLI's batch mode uses +(retromol.io.streaming.run_retromol_stream), but with RDKit's C-level logging also +disabled inside every worker (see common.py for why that needs its own initializer). +""" + +import argparse +import json +import logging +from pathlib import Path +from typing import Literal + +from tqdm import tqdm + +from common import load_ruleset, run_retromol_stream_quiet +from retromol.io.streaming import stream_json_records, stream_sdf_records + +log = logging.getLogger(__name__) + + +def run( + input_path: str | Path, + input_format: Literal["sdf", "jsonl"], + output_path: str | Path, + reaction_rules_path: str | Path | None, + matching_rules_path: str | Path | None, + match_stereochemistry: bool = False, + smiles_col: str = "smiles", + workers: int = 1, + batch_size: int = 2000, + log_every: int = 1000, +) -> None: + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + ruleset = load_ruleset(reaction_rules_path, matching_rules_path, match_stereochemistry) + + if input_format == "sdf": + row_iter = stream_sdf_records(str(input_path)) + else: + row_iter = stream_json_records(str(input_path), jsonl=True) + + successes = 0 + errors = 0 + + with open(output_path, "w", buffering=1) as out: + with tqdm(desc="parse_compounds", unit="cmpd") as pbar: + for evt in run_retromol_stream_quiet( + ruleset=ruleset, + row_iter=row_iter, + smiles_col=smiles_col, + workers=workers, + batch_size=batch_size, + ): + if evt.error is not None: + errors += 1 + elif evt.result is not None: + out.write(json.dumps(evt.result) + "\n") + successes += 1 + + pbar.update(1) + pbar.set_postfix(ok=successes, err=errors) + + total = successes + errors + if log_every > 0 and total % log_every == 0: + log.info("parse_compounds: parsed %d (successes=%d errors=%d)", total, successes, errors) + + log.info("parse_compounds: successes=%d errors=%d -> %s", successes, errors, output_path) + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--input", required=True) + ap.add_argument("--input-format", choices=["sdf", "jsonl"], required=True) + ap.add_argument("--output", required=True) + ap.add_argument("--rxn-rules", default=None) + ap.add_argument("--mxn-rules", default=None) + ap.add_argument("--match-stereochemistry", action="store_true") + ap.add_argument("--smiles-col", default="smiles") + ap.add_argument("--workers", type=int, default=1) + ap.add_argument("--batch-size", type=int, default=2000) + ap.add_argument("--log-every", type=int, default=1000, help="log a progress line every N compounds (0 to disable)") + args = ap.parse_args() + + run( + input_path=args.input, + input_format=args.input_format, + output_path=args.output, + reaction_rules_path=args.rxn_rules, + matching_rules_path=args.mxn_rules, + match_stereochemistry=args.match_stereochemistry, + smiles_col=args.smiles_col, + workers=args.workers, + batch_size=args.batch_size, + log_every=args.log_every, + ) + + +if __name__ == "__main__": + main() diff --git a/database/scripts/parse_gbks.py b/database/scripts/parse_gbks.py new file mode 100644 index 0000000..6c2c5a7 --- /dev/null +++ b/database/scripts/parse_gbks.py @@ -0,0 +1,226 @@ +"""Step 7: parse antiSMASH GenBank files into linear module readouts. + +For each region: whichever model pmp.yml's "predictors.nrps.a_domain" currently +selects predicts NRPS A-domain substrate specificities +(retromol_antismash.inference.factory.build_nrps_a_domain_model + +retromol_antismash.inference.registry.annotate_region), then +retromol_antismash.modules.linear_readout collects PKS/NRPS modules in +biosynthetic order -- using the SAME factory + pmp.yml the GUI backend +(gui/src/server/routes/jobs.py) and the CLI test script (scripts/predict_bgc.py) +use, so all three ways of parsing a GenBank file resolve substrates identically. +One file per worker process (model loading is the expensive per-process setup +cost, so it's paid once per worker, not once per file). + +If pmp.yml's selected method is `source: qualifier` instead of `source: model` +(e.g. reading antiSMASH's own NRPS substrate call straight from the GenBank +record), no model is built or run at all -- collect_nrps_modules reads the +qualifier directly, same as everywhere else this config is used. + +Emits one output: readouts JSONL, one line per antiSMASH region, with its +LinearReadout, the raw GenBank text of its source file, and the MIBiG accession +parsed out of the region id. + +MIBiG's URL (built from an accession + a per-entry revision number, e.g. +".../BGC0000001.5/...") used to be derivable right here, from the GBK's own +ACCESSION.VERSION line -- but MIBiG 4.0's GBKs dropped the version suffix +entirely ("ACCESSION BGC0000001", no VERSION line). The version now only +exists in the JSON's own top-level "version" field, so extract_mibig_compounds.py +is the sole source of the accession -> version map (mibig_versions.json) both +this script's own consumer (load_bgcs.py) and load_compounds.py need -- this +script only emits `accession` (still correct: it's just region.id, with or +without a version suffix) for load_bgcs.py to look up. +""" + +import argparse +import hashlib +import itertools +import json +import logging +from concurrent.futures import FIRST_COMPLETED, ProcessPoolExecutor, wait +from pathlib import Path + +from rdkit import RDLogger +from tqdm import tqdm + +from common import split_accession_version +from retromol_antismash.inference.base import DomainInferenceModel +from retromol_antismash.inference.factory import build_nrps_a_domain_model +from retromol_antismash.inference.registry import annotate_region +from retromol_antismash.io import AntiSmashOptions, parse_antismash_gbk +from retromol_antismash.modules import linear_readout +from retromol_antismash.predictions import PredictionConfig + +log = logging.getLogger(__name__) + +GBK_GLOBS = ("*.gbk", "*.gb", "*.gbff") + +_G_CONFIG: PredictionConfig | None = None +_G_NRPS_MODEL: DomainInferenceModel | None = None + + +def _init_worker( + pmp_path: str | None, + threshold: float, + keep_top: int, + cache_dir: str, + force_retrain: bool, +) -> None: + global _G_CONFIG, _G_NRPS_MODEL + # Belt-and-braces: importing this module (to resolve _init_worker as this pool's + # initializer) already re-runs common.py's own RDLogger.DisableLog at the top of + # this file's import chain, but this initializer is what the pool guarantees runs + # once per worker no matter what -- see common.run_retromol_stream_quiet's + # docstring for why that guarantee matters more than it might seem. + RDLogger.DisableLog("rdApp.*") + _G_CONFIG = PredictionConfig.load_from_file(pmp_path) if pmp_path else PredictionConfig.load_default() + # None when pmp.yml's predictors.nrps.a_domain is source: qualifier -- nothing to + # build or register, collect_nrps_modules reads the qualifier directly. + _G_NRPS_MODEL = build_nrps_a_domain_model( + _G_CONFIG, + threshold=threshold, + keep_top=keep_top, + cache_dir=cache_dir, + force_retrain=force_retrain, + ) + + +def _process_file(path_str: str) -> tuple[list[dict], str | None]: + """Parse one GenBank file. Returns (entries, error message).""" + path = Path(path_str) + try: + raw_gbk = path.read_text() + file_hash = hashlib.sha256(raw_gbk.encode("utf-8")).hexdigest() + regions = parse_antismash_gbk(path, AntiSmashOptions()) + + entries: list[dict] = [] + + for region in regions: + if _G_NRPS_MODEL is not None: + annotate_region(region, domain_models=[_G_NRPS_MODEL]) + + readout = linear_readout(region, config=_G_CONFIG) + + accession, _version = split_accession_version(region.id) + + entries.append({ + "accession": accession, + "file_name": region.file_name, + "raw_gbk": raw_gbk, + "file_hash": file_hash, + "readout": readout.to_dict(), + }) + + return entries, None + except Exception as e: + return [], f"{path}: {e}" + + +def run( + gbk_dir: str | Path, + readouts_output_path: str | Path, + pmp_path: str | Path | None = None, + paras_threshold: float = 0.1, + paras_keep_top: int = 3, + paras_cache_dir: str | Path = "paras_cache", + force_retrain: bool = False, + workers: int = 1, +) -> None: + """ + :param gbk_dir: directory of antiSMASH GenBank files to parse (searched recursively). + :param readouts_output_path: where to write the readouts JSONL. + :param pmp_path: path to a pmp.yml prediction-mapping file, or None to use the packaged + default -- see retromol_antismash.inference.factory.build_nrps_a_domain_model for + how this selects (or skips) an NRPS substrate-prediction model. + :param paras_threshold: minimum predicted probability for a substrate call to be kept. + :param paras_keep_top: number of top-scoring substrate predictions to keep per domain. + :param paras_cache_dir: training-signature + fitted-model cache directory (see + retromol_paras.train.train_model). + :param force_retrain: retrain from scratch even if a cached model exists. + :param workers: number of worker processes. + """ + gbk_dir = Path(gbk_dir) + readouts_output_path = Path(readouts_output_path) + readouts_output_path.parent.mkdir(parents=True, exist_ok=True) + + paths = sorted({p for pattern in GBK_GLOBS for p in gbk_dir.rglob(pattern)}) + + n_files = 0 + n_entries = 0 + n_errors = 0 + + init_args = ( + str(pmp_path) if pmp_path else None, + paras_threshold, + paras_keep_top, + str(paras_cache_dir), + force_retrain, + ) + + # Bounded sliding window rather than submitting every file as a future up front -- + # at hundreds of thousands of files, submitting everything at once means that many + # Future/WorkItem objects (plus each one's eventual result: raw GBK text + a full + # serialized LinearReadout) sit in memory simultaneously. Keeping at most + # `max_pending` in flight bounds memory to a small multiple of `workers`, + # regardless of corpus size -- same shape as retromol.io.streaming's own batching + # that parse_compounds.py relies on for the same reason. + max_pending = max(workers * 4, 1) + paths_iter = iter(paths) + + with open(readouts_output_path, "w", buffering=1) as out: + with ProcessPoolExecutor(max_workers=workers, initializer=_init_worker, initargs=init_args) as pool: + with tqdm(total=len(paths), desc="parse_gbks", unit="file") as pbar: + pending = {pool.submit(_process_file, str(p)) for p in itertools.islice(paths_iter, max_pending)} + + while pending: + done, pending = wait(pending, return_when=FIRST_COMPLETED) + + for fut in done: + entries, error = fut.result() + n_files += 1 + + if error is not None: + log.error("parse_gbks: %s", error) + n_errors += 1 + else: + for entry in entries: + out.write(json.dumps(entry) + "\n") + n_entries += 1 + + pbar.update(1) + pbar.set_postfix(regions=n_entries, errors=n_errors) + + next_path = next(paths_iter, None) + if next_path is not None: + pending.add(pool.submit(_process_file, str(next_path))) + + log.info("parse_gbks: files=%d regions=%d errors=%d", n_files, n_entries, n_errors) + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--gbk-dir", required=True) + ap.add_argument("--readouts-output", required=True) + ap.add_argument("--pmp", default=None, help="path to a pmp.yml prediction-mapping file (default: packaged pmp.yml)") + ap.add_argument("--paras-threshold", type=float, default=0.1) + ap.add_argument("--paras-keep-top", type=int, default=3) + ap.add_argument("--paras-cache-dir", default="paras_cache") + ap.add_argument("--force-retrain", action="store_true", help="retrain from scratch even if a cached model exists") + ap.add_argument("--workers", type=int, default=1) + args = ap.parse_args() + + run( + gbk_dir=args.gbk_dir, + readouts_output_path=args.readouts_output, + pmp_path=args.pmp, + paras_threshold=args.paras_threshold, + paras_keep_top=args.paras_keep_top, + paras_cache_dir=args.paras_cache_dir, + force_retrain=args.force_retrain, + workers=args.workers, + ) + + +if __name__ == "__main__": + main() diff --git a/database/scripts/taxonomy.py b/database/scripts/taxonomy.py new file mode 100644 index 0000000..3f6b580 --- /dev/null +++ b/database/scripts/taxonomy.py @@ -0,0 +1,235 @@ +"""NCBI taxonomy dump download + name/taxid resolution for phylogeny annotation. + +Downloads and parses `taxdump.tar.gz` (https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz) +once, then resolves organism names <-> NCBI taxids and classifies a taxid's broad "type" +(Bacterium/Archaeon/Fungus/Other) by walking its lineage. MIBiG's JSON gives a taxid +directly (see extract_mibig_compounds.py's `taxonomy.ncbiTaxId`); NPAtlas gives only +genus/species text, resolved here by scientific-name/synonym lookup instead -- so both +sources end up with identically-standardized taxids and canonical names. +""" + +from __future__ import annotations + +import logging +import tarfile +import urllib.request +from dataclasses import dataclass +from pathlib import Path +from typing import Iterator + +from tqdm import tqdm + +log = logging.getLogger(__name__) + +TAXDUMP_URL = "https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz" + +# Fixed NCBI taxids for the lineage nodes used to classify a taxon's broad "type" (see +# TaxonomyDB.type_label_and_taxid) -- these are stable anchor points in NCBI's taxonomy, +# not resolved by name. +TAXID_BACTERIA = 2 +TAXID_ARCHAEA = 2157 +TAXID_FUNGI = 4751 +TYPE_LABELS_BY_TAXID = { + TAXID_BACTERIA: "Bacterium", + TAXID_ARCHAEA: "Archaeon", + TAXID_FUNGI: "Fungus", +} + + +def download_taxdump(dest_dir: str | Path, *, force: bool = False) -> Path: + """Download and extract names.dmp/nodes.dmp from NCBI's taxdump into `dest_dir` + (no-op if both files already exist there, unless `force`).""" + dest_dir = Path(dest_dir).expanduser() + dest_dir.mkdir(parents=True, exist_ok=True) + + names_path = dest_dir / "names.dmp" + nodes_path = dest_dir / "nodes.dmp" + if not force and names_path.exists() and nodes_path.exists(): + return dest_dir + + log.info("downloading NCBI taxdump from %s", TAXDUMP_URL) + archive_path = dest_dir / "taxdump.tar.gz" + with urllib.request.urlopen(TAXDUMP_URL) as resp, open(archive_path, "wb") as out: + total = int(resp.headers.get("Content-Length") or 0) or None + with tqdm(total=total, desc="download_taxdump", unit="B", unit_scale=True, unit_divisor=1024) as pbar: + while chunk := resp.read(1024 * 1024): + out.write(chunk) + pbar.update(len(chunk)) + + with tarfile.open(archive_path, mode="r:gz") as tar: + for member in tar.getmembers(): + if member.name in ("names.dmp", "nodes.dmp"): + tar.extract(member, path=dest_dir) + archive_path.unlink() + + if not names_path.exists() or not nodes_path.exists(): + raise FileNotFoundError(f"taxdump at {TAXDUMP_URL} did not contain names.dmp/nodes.dmp") + + return dest_dir + + +def _iter_dmp_rows(path: Path) -> Iterator[list[str]]: + """NCBI .dmp rows are "\\t|\\t"-separated, terminated by a trailing "\\t|".""" + with open(path, encoding="utf-8", errors="replace") as fh: + for line in fh: + line = line.rstrip("\n").rstrip("\t|") + yield [field.strip() for field in line.split("\t|\t")] + + +@dataclass(frozen=True) +class PhylogenyResolution: + type_label: str | None + type_taxid: str | None + genus: str | None + genus_taxid: str | None + species: str | None + species_taxid: str | None + + +@dataclass +class TaxonomyDB: + """In-memory index over NCBI's taxdump, built once per pipeline run.""" + + parent_by_taxid: dict[int, int] + rank_by_taxid: dict[int, str] + scientific_name_by_taxid: dict[int, str] + taxid_by_name: dict[str, int] # lowercased scientific name/synonym -> taxid + + @classmethod + def load(cls, taxdump_dir: str | Path) -> "TaxonomyDB": + taxdump_dir = Path(taxdump_dir).expanduser() + + parent_by_taxid: dict[int, int] = {} + rank_by_taxid: dict[int, str] = {} + for row in _iter_dmp_rows(taxdump_dir / "nodes.dmp"): + taxid, parent_taxid, rank = int(row[0]), int(row[1]), row[2] + parent_by_taxid[taxid] = parent_taxid + rank_by_taxid[taxid] = rank + + scientific_name_by_taxid: dict[int, str] = {} + taxid_by_name: dict[str, int] = {} + for row in _iter_dmp_rows(taxdump_dir / "names.dmp"): + taxid, name_txt, name_class = int(row[0]), row[1], row[3] + if name_class == "scientific name": + scientific_name_by_taxid[taxid] = name_txt + # Index every name class (scientific name, synonym, common name, ...) -- + # an organism string from a compound source can be any of these. + taxid_by_name.setdefault(name_txt.lower(), taxid) + + log.info("loaded NCBI taxdump: %d nodes, %d names", len(parent_by_taxid), len(taxid_by_name)) + return cls( + parent_by_taxid=parent_by_taxid, + rank_by_taxid=rank_by_taxid, + scientific_name_by_taxid=scientific_name_by_taxid, + taxid_by_name=taxid_by_name, + ) + + def resolve_taxid(self, name: str | None) -> int | None: + """Look up a taxid by scientific name or synonym, case-insensitive.""" + if not name: + return None + return self.taxid_by_name.get(name.strip().lower()) + + def canonical_name(self, taxid: int | None) -> str | None: + if taxid is None: + return None + return self.scientific_name_by_taxid.get(taxid) + + def lineage(self, taxid: int) -> list[int]: + """The ancestor chain from `taxid` (inclusive) up to the root, stopping early + if a cycle or a gap in nodes.dmp is hit.""" + chain = [taxid] + seen = {taxid} + current = taxid + while current in self.parent_by_taxid and current != 1: + parent = self.parent_by_taxid[current] + if parent == current or parent in seen: + break + chain.append(parent) + seen.add(parent) + current = parent + return chain + + def ancestor_at_rank(self, taxid: int | None, rank: str) -> int | None: + """The ancestor of `taxid` (inclusive) whose rank is exactly `rank` (e.g. + "genus", "species"), or None if no such ancestor exists in the lineage.""" + if taxid is None: + return None + for ancestor in self.lineage(taxid): + if self.rank_by_taxid.get(ancestor) == rank: + return ancestor + return None + + def type_label_and_taxid(self, taxid: int | None) -> tuple[str | None, int | None]: + """Classify a taxid's broad type by walking its lineage for one of the fixed + Bacteria/Archaea/Fungi anchor nodes; "Other" if the lineage resolves but hits + none of them (e.g. Viruses, Protista).""" + if taxid is None: + return None, None + for ancestor in self.lineage(taxid): + if ancestor in TYPE_LABELS_BY_TAXID: + return TYPE_LABELS_BY_TAXID[ancestor], ancestor + return "Other", None + + +_UNRESOLVED = PhylogenyResolution(None, None, None, None, None, None) + + +def resolve_phylogeny( + taxdb: TaxonomyDB | None, + *, + ncbi_tax_id: str | int | None = None, + genus: str | None = None, + species: str | None = None, + fallback_type_label: str | None = None, +) -> PhylogenyResolution: + """Standardize phylogeny fields to NCBI taxids -- and *only* to NCBI taxids: every + label stored is read back off a resolved taxid via TaxonomyDB.canonical_name, never + passed through as raw, unstandardized source text. If a taxid can't be resolved at + all (no taxdb loaded, or the given ncbi_tax_id/genus/species doesn't match anything + in NCBI's taxonomy), the result is fully unannotated -- type/genus/species all + None -- rather than a best-effort guess from whatever text the source gave us. + `fallback_type_label` (NPAtlas's own origin_type field) is accepted for API + compatibility with callers but is deliberately unused for the same reason: it isn't + NCBI vocabulary either. + + If `ncbi_tax_id` is given (MIBiG), it's the source of truth: genus/species/type and + their taxids are all derived from its lineage, overriding any given genus/species + text. Otherwise (NPAtlas, or an MIBiG entry whose ncbi_tax_id didn't parse), + `genus`/`species` text is resolved to a taxid by name lookup -- "Genus species" + first, falling back to genus alone -- so both sources end up identically + standardized when they resolve at all. + """ + if taxdb is None: + return _UNRESOLVED + + leaf_taxid: int | None = None + if ncbi_tax_id: + try: + leaf_taxid = int(ncbi_tax_id) + except (TypeError, ValueError): + leaf_taxid = None + + if leaf_taxid is None and genus: + query = f"{genus} {species}" if species else genus + leaf_taxid = taxdb.resolve_taxid(query) or taxdb.resolve_taxid(genus) + + if leaf_taxid is None: + return _UNRESOLVED + + type_label, type_taxid = taxdb.type_label_and_taxid(leaf_taxid) + + genus_taxid = taxdb.ancestor_at_rank(leaf_taxid, "genus") + genus_name = taxdb.canonical_name(genus_taxid) if genus_taxid else None + + species_taxid = taxdb.ancestor_at_rank(leaf_taxid, "species") + species_name = taxdb.canonical_name(species_taxid) if species_taxid else None + + return PhylogenyResolution( + type_label=type_label, + type_taxid=str(type_taxid) if type_taxid is not None else None, + genus=genus_name, + genus_taxid=str(genus_taxid) if genus_taxid is not None else None, + species=species_name, + species_taxid=str(species_taxid) if species_taxid is not None else None, + ) diff --git a/docker-compose.yml b/docker-compose.yml index 2a48d2b..a8ccae8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,25 @@ +# Shared by backend and worker -- both run the same image (gui/docker/backend.Dockerfile) +# and need the same Redis/DuckDB/PARAS access, now that discovery_query and +# reconstruct_gene_cluster (among others) run as RQ jobs on worker too, not just on +# backend. Kept in one place so the two services can't drift out of sync. +# +# PARAS_CACHE_DIR is read by whichever NRPS model pmp.yml's predictors.nrps.a_domain +# selects (see retromol_antismash.inference.factory.build_nrps_a_domain_model) -- in +# practice, "paras_cli"'s (src/retromol_paras/) trained-model + extracted-training- +# signatures cache. Mounted read-only here so containers pick up an already-trained +# model instead of each one training its own on first use -- run scripts/ +# train_paras.py once (locally, or via `docker compose run backend ...`) to populate +# PARAS_CACHE_HOST_PATH, then restart backend/worker to pick it up. +x-backend-env: &backend-common + env_file: + - gui/docker/backend.env + environment: + PARAS_CACHE_DIR: ${PARAS_CACHE_DIR} + RETROMOL_DUCKDB_PATH: /data/retromol.duckdb + volumes: + - ${PARAS_CACHE_HOST_PATH}:${PARAS_CACHE_DIR}:ro + - ${RETROMOL_DUCKDB_HOST_PATH}:/data/retromol.duckdb:ro + services: redis: image: redis:7 @@ -5,39 +27,62 @@ services: restart: unless-stopped command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"] # No ports: exposed only inside the Docker network + healthcheck: + test: ["CMD-SHELL", "redis-cli -a \"${REDIS_PASSWORD}\" ping | grep -q PONG"] + interval: 30s + timeout: 5s + retries: 3 networks: [ retromol_network ] - maintenance: + backend: build: context: . - dockerfile: gui/docker/maintenance.Dockerfile - container_name: retromol_maintenance + dockerfile: gui/docker/backend.Dockerfile + container_name: retromol_backend restart: unless-stopped - env_file: - - gui/docker/backend.env + <<: *backend-common depends_on: redis: - condition: service_started + condition: service_healthy networks: [ retromol_network ] + # Healthcheck comes from the image's own HEALTHCHECK (hits /api/ready) -- + # nothing to override here. - backend: + # Runs the same image as backend, just executing RQ jobs instead of serving HTTP -- + # see routes/queue.py and app.py's blueprints for what ends up here (RetroMol + # parsing, PARAS inference, alignment, RDKit fingerprinting). + # + # A single pool draining the one `heavy_compute` queue (routes/queue.py). Scale + # `RQ_WORKER_REPLICAS` in the repo root .env (Compose resolves its own ${VAR} + # interpolation from there, not from an env_file: like gui/docker/backend.env -- + # or use `docker compose up --scale worker=N` if this Compose version doesn't + # apply `deploy.replicas` outside swarm mode). + worker: build: context: . dockerfile: gui/docker/backend.Dockerfile - container_name: retromol_backend restart: unless-stopped - env_file: - - gui/docker/backend.env - environment: - PARAS_MODEL_PATH: ${PARAS_MODEL_PATH} - RETROMOL_DUCKDB_PATH: /data/retromol.duckdb - volumes: - - ${PARAS_MODEL_HOST_PATH}:${PARAS_MODEL_PATH}:ro - - ${RETROMOL_DUCKDB_HOST_PATH}:/data/retromol.duckdb:ro + <<: *backend-common + # entrypoint inherited from the image (conda run -n retromol-gui ...); only the + # command differs from backend's. REDIS_URL comes from gui/docker/backend.env + # (loaded into the *container's* environment via env_file above) -- run through + # a shell so `$REDIS_URL` expands there. `$$` escapes Compose's own interpolation + # (which matches bare $VAR too, not just ${VAR}, and only sees the host/.env + # environment, not env_file) so the container receives the literal `$REDIS_URL`. + command: ["sh", "-c", "rq worker --url \"$$REDIS_URL\" heavy_compute"] + deploy: + replicas: ${RQ_WORKER_REPLICAS:-2} depends_on: redis: - condition: service_started + condition: service_healthy networks: [ retromol_network ] + # No fixed container_name -- incompatible with replicas > 1. + # The image's inherited HEALTHCHECK probes an HTTP server this container never + # runs (it runs `rq worker`, not gunicorn) -- disabled here rather than left to + # fail forever. A simple worker loop's meaningful failure mode is the process + # exiting, which `restart: unless-stopped` already reacts to. + healthcheck: + disable: true web: build: @@ -49,8 +94,17 @@ services: - backend ports: - "4005:80" + healthcheck: + # 127.0.0.1, not localhost -- this Alpine image resolves "localhost" to ::1 + # first, but nginx's `listen 80` only binds IPv4, so wget would hit + # "connection refused" on the (nothing-listening) IPv6 address and the + # container would report unhealthy despite actually serving requests fine. + test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1/"] + interval: 30s + timeout: 5s + retries: 3 networks: [ retromol_network ] networks: retromol_network: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..f77db9b --- /dev/null +++ b/environment.yml @@ -0,0 +1,74 @@ +# Single conda env for this repo: everything needed to run the database-construction +# Snakemake pipeline (database/Snakemake) AND retromol_paras (src/retromol_paras/, the +# self-contained PARAS reimplementation used by retromol_antismash's "paras_cli" +# DomainInferenceModel and selected via pmp.yml's predictors.nrps.a_domain: paras_cli). +# These two used to be separate env files (database/envs/retromol.yaml, +# envs/retromol_paras.yaml) -- consolidated here since there's no reason to keep two +# conda envs around for one repo. +# +# Snakemake / database pipeline: +# Every rule in database/Snakemake uses Snakemake's `run:` directive (inline Python +# that imports and calls database/scripts/*.py functions directly), not `shell:`. +# Snakemake executes `run:` blocks in its own process rather than a subprocess, so a +# per-rule `conda:` env (the --use-conda mechanism) would NOT actually reach any of +# this pipeline's own code -- only `shell()` calls made from *within* a `run:` block, +# which this pipeline never makes. There is deliberately no per-rule conda: directive +# in database/Snakemake, and no --use-conda flag in the invocation below, because of +# this. This env is instead the one Snakemake itself runs in, so it needs both +# Snakemake and every runtime dependency database/scripts/*.py imports (rdkit, +# duckdb, etc. -- all already listed in the repo's own pyproject.toml, pulled in by +# the editable install below rather than duplicated here). +# +# retromol_paras / PARAS CLI tools: +# None of these are pip-installable -- they're the command-line binaries +# retromol_paras shells out to instead of pyhmmer: +# - hmmpfam2 (legacy HMMER2) -> bioconda "hmmer2" +# - hmmscan, hmmpress (HMMER3) -> bioconda "hmmer" +# - muscle, MUSCLE v3 CLI syntax -> bioconda "muscle=3.8.1551" (pinned: bioconda's +# unpinned "muscle" resolves to MUSCLE v5 today, whose CLI is incompatible -- +# `-in1/-in2/-profile` is v3-only) +# +# Usage: +# +# conda env create -f environment.yml +# conda activate retromol +# pip install -e . +# snakemake -p -s database/Snakemake --configfile database/config.yaml \ +# --workflow-profile database/profiles/slurm +# +# --------------------------------------------------------------------------- +# Apple Silicon (arm64) note +# --------------------------------------------------------------------------- +# hmmer2 and muscle=3.8.1551 are old, low-traffic bioconda packages -- osx-arm64 +# builds may not exist for every build number, or may exist but not solve +# together with the rest of this environment's dependencies. If `conda env +# create` fails to solve, or the env solves but a binary immediately fails at +# runtime (segfault / "bad CPU type in executable"), force the whole +# environment onto the osx-64 (Intel) package set instead, which conda then +# runs through Rosetta 2 automatically: +# +# softwareupdate --install-rosetta # one-time, if not already installed +# CONDA_SUBDIR=osx-64 conda env create -f environment.yml -n retromol +# conda activate retromol +# conda config --env --set subdir osx-64 # pin osx-64 for this env so future +# # `conda install`/`conda update` in it +# # don't drift back to arm64 packages +# pip install -e . +# +# This makes the whole env (Python included) run under Rosetta, which is fine -- +# it's only these binaries retromol_paras invokes as subprocesses that need it, +# and there's no measurable performance concern for occasional HMMER/MUSCLE runs. +name: retromol + +channels: + - bioconda + - conda-forge + +dependencies: + - python=3.11 + - pip + - snakemake + - snakemake-executor-plugin-slurm + - hmmer2 + - hmmer + - muscle=3.8.1551 diff --git a/gui/docker/backend.Dockerfile b/gui/docker/backend.Dockerfile index 4e7c6ea..1a5e869 100644 --- a/gui/docker/backend.Dockerfile +++ b/gui/docker/backend.Dockerfile @@ -14,8 +14,9 @@ RUN groupadd --gid $USER_GID $USERNAME \ # Switch to /app as working dir WORKDIR /app -# System deps (git) -RUN apt-get update && apt-get install -y --no-install-recommends build-essential git && rm -rf /var/lib/apt/lists/* +# System deps (git; libxrender1/libxext6/libsm6 are runtime deps of rdkit.Chem.Draw, +# which dlopens X11 libs for 2D rendering even though the container is headless) +RUN apt-get update && apt-get install -y --no-install-recommends build-essential git libxrender1 libxext6 libsm6 && rm -rf /var/lib/apt/lists/* # Copy env + requirements before env creation for caching COPY gui/src/server/environment.backend.yml /app/ @@ -60,5 +61,15 @@ USER $USERNAME # Run everything inside the conda env without manual activation ENTRYPOINT ["conda", "run", "-n", "retromol-gui", "--no-capture-output"] +# Worker/thread/timeout sizing lives in gunicorn.conf.py (env-var overridable via +# gui/docker/backend.env), not hardcoded here. # Let Flask/gunicorn find the app: "app:app" -CMD ["gunicorn", "-w", "1", "--threads", "4", "-b", "0.0.0.0:4000", "--access-logfile", "-", "--error-logfile", "-", "--log-level", "info", "--timeout", "120", "app:app"] \ No newline at end of file +CMD ["gunicorn", "-c", "gunicorn.conf.py", "app:app"] + +# /api/ready checks both DuckDB and Redis connectivity -- a more meaningful signal +# than "the process is up" for whether this container should receive traffic. Uses +# stdlib urllib rather than curl/wget (neither is installed in this image, and +# adding one just for a healthcheck isn't worth the extra apt layer). +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD conda run -n retromol-gui --no-capture-output python -c \ + "import urllib.request; urllib.request.urlopen('http://localhost:4000/api/ready', timeout=3)" || exit 1 \ No newline at end of file diff --git a/gui/docker/backend.env b/gui/docker/backend.env index d79bf6a..3822622 100644 --- a/gui/docker/backend.env +++ b/gui/docker/backend.env @@ -6,4 +6,19 @@ REDIS_PASSWORD=supersecretpassword SESSION_TTL_SECONDS=604800 # 7 days JOB_TIMEOUT_SECONDS=120 -JOB_WATCHDOG_INTERVAL_SECONDS=130 \ No newline at end of file +JOB_WATCHDOG_INTERVAL_SECONDS=130 + +# Gunicorn sizing -- the one place to change worker/thread/timeout counts without +# rebuilding the image (see gui/src/server/gunicorn.conf.py). +GUNICORN_WORKERS=2 +GUNICORN_THREADS=4 +GUNICORN_TIMEOUT=120 + +# RQ_WORKER_REPLICAS/RQ_LIGHT_WORKER_REPLICAS are NOT set here -- they feed +# `deploy.replicas` in docker-compose.yml, which Compose resolves from the repo +# root .env (or the shell environment), never from an `env_file:` like this one. +# No application code reads them either; see the root .env for the actual knob. + +# How long a Flask handler blocks waiting on a heavy job before returning 503 -- +# kept safely under GUNICORN_TIMEOUT above. +HEAVY_JOB_WAIT_TIMEOUT_SECONDS=90 \ No newline at end of file diff --git a/gui/docker/maintenance.Dockerfile b/gui/docker/maintenance.Dockerfile deleted file mode 100644 index 673f04c..0000000 --- a/gui/docker/maintenance.Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -# Ultra-light image for maintenance loop -FROM python:3.10-slim - -WORKDIR /app - -# Unbuffered logs so they appear in docker logs immediately -ENV PYTHONUNBUFFERED=1 - -# Copy only server code (includes maintenance.py and routes/) -COPY gui/src/server /app - -# Install the same dependencies as the backend (could be slimmed down) -RUN pip install --no-cache-dir -r requirements.maintenance.txt - -# Run the maintenance loop -CMD ["python", "-m", "maintenance"] \ No newline at end of file diff --git a/gui/scripts/dev_backend.sh b/gui/scripts/dev_backend.sh index ccce31e..852e668 100644 --- a/gui/scripts/dev_backend.sh +++ b/gui/scripts/dev_backend.sh @@ -10,7 +10,7 @@ export FLASK_ENV=development export PORT=4000 # DB connection -export RETROMOL_DUCKDB_PATH="$HOME/Downloads/retromol.duckdb" +export RETROMOL_DUCKDB_PATH="$HOME/Desktop/retromol.duckdb" # Redis connection (uses Dockerized Redis) export REDIS_URL="redis://localhost:6379/0" @@ -19,8 +19,11 @@ export SESSION_TTL_SECONDS=$((7 * 24 * 3600)) # Define cache dir for backend (temp files, etc.) export CACHE_DIR="$(pwd)/cache" -# Define model paths -export PARAS_MODEL_PATH="$(pwd)/models/all_substrates_model.paras.gz" +# PARAS "paras_cli" model cache dir (the folder containing model.paras.joblib and +# extended_signatures.cache.tsv, from `python scripts/train_paras.py --cache-dir +# `) -- NOT the model file itself. Missing/wrong here means the first cluster +# upload retrains from scratch (slow, likely to hit the job timeout). +export PARAS_CACHE_DIR="$HOME/Desktop/paras_cache" # Make sure Flask can find the app export PYTHONPATH="$(pwd)/src/server" diff --git a/gui/scripts/dev_worker.sh b/gui/scripts/dev_worker.sh new file mode 100755 index 0000000..c8bd664 --- /dev/null +++ b/gui/scripts/dev_worker.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Run an RQ worker locally. Needs the same environment as dev_backend.sh -- task +# functions run in this process and read PARAS_CACHE_DIR/RETROMOL_DUCKDB_PATH +# directly, and routes.* must be importable here too. +# +# By default listens to both queues (heavy_compute_pmi first, then heavy_compute), +# matching a single local worker sharing one pile across everything -- see +# routes/queue.py. To test the production split locally (a worker dedicated to +# light jobs that never blocks behind a PMI job), run a second terminal with: +# WORKER_QUEUES=heavy_compute bash gui/scripts/dev_worker.sh +# +# Usage: ./scripts/dev_worker.sh + +set -euo pipefail +cd "$(dirname "$0")/.." # go to repo root + +# --- Setup environment --- +export RETROMOL_DUCKDB_PATH="$HOME/Desktop/retromol.duckdb" + +# Redis connection (uses Dockerized Redis) +export REDIS_URL="redis://localhost:6379/0" +export SESSION_TTL_SECONDS=$((7 * 24 * 3600)) + +# Define cache dir for backend (temp files, etc.) +export CACHE_DIR="$(pwd)/cache" + +# PARAS "paras_cli" model cache dir (the folder containing model.paras.joblib and +# extended_signatures.cache.tsv, from `python scripts/train_paras.py --cache-dir +# `) -- NOT the model file itself. Missing/wrong here means the first cluster +# upload retrains from scratch (slow, likely to hit the job timeout). +export PARAS_CACHE_DIR="$HOME/Desktop/paras_cache" + +# Make sure the worker can import routes.jobs etc. +export PYTHONPATH="$(pwd)/src/server" + +QUEUES="${WORKER_QUEUES:-heavy_compute_pmi heavy_compute}" + +echo "Starting RQ worker for queue(s): ${QUEUES}" +echo + +python -c "import logging; logging.basicConfig(level=logging.INFO); from routes.jobs import check_paras_model_cache; check_paras_model_cache()" +echo + +rq worker --url "${REDIS_URL}" ${QUEUES} diff --git a/gui/src/client/craco.config.js b/gui/src/client/craco.config.js index 5db7833..a099545 100644 --- a/gui/src/client/craco.config.js +++ b/gui/src/client/craco.config.js @@ -1,20 +1 @@ -module.exports = { - webpack: { - configure: (config) => { - // Remove both the TypeScript checker and CRA’s ESLint plugin just in case - const kill = new Set([ - 'ForkTsCheckerWebpackPlugin', // TS type checker - 'ESLintPlugin', // eslint-webpack-plugin - 'ESLintWebpackPlugin' // alt name on some setups - ]); - - // Log plugins so you can confirm removal the first time - console.log('CRA plugins:', (config.plugins || []).map(p => p?.constructor?.name)); - - config.plugins = (config.plugins || []).filter( - (p) => !kill.has(p?.constructor?.name) - ); - return config; - }, - }, -}; \ No newline at end of file +module.exports = {}; \ No newline at end of file diff --git a/gui/src/client/package-lock.json b/gui/src/client/package-lock.json index 80caae6..9819b29 100644 --- a/gui/src/client/package-lock.json +++ b/gui/src/client/package-lock.json @@ -1,49 +1,54 @@ { "name": "retromol-gui", - "version": "1.1.0", + "version": "1.0.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "retromol-gui", - "version": "1.1.0", + "version": "1.0.0-dev", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.5", "@mui/x-charts": "^8.17.0", - "@mui/x-data-grid": "^8.18.0", + "@mui/x-data-grid": "^9.12.0", "@react-spring/web": "^10.0.3", - "@react-three/drei": "^8.20.2", - "@react-three/fiber": "^8.13.5", "@tanstack/react-query": "^5.76.1", "@types/react": "18.3.10", "@types/react-dom": "18.3.1", "clsx": "^2.1.1", "dayjs": "^1.11.19", + "dompurify": "^3.2.4", "framer-motion": "^12.6.3", - "html2canvas": "^1.4.1", - "lodash.debounce": "^4.0.8", + "html-to-image": "^1.11.13", "react": "18.3.1", "react-dom": "18.3.1", - "react-force-graph-2d": "^1.27.1", "react-router-dom": "^7.9.5", - "react-window": "^1.8.11", "smiles-drawer": "^2.1.7", - "three": "^0.175.0", - "typescript": "4.9.5", + "typescript": "^5.4.5", "zod": "3.23.8" }, "devDependencies": { "@craco/craco": "^7.1.0", - "@types/lodash.debounce": "^4.0.9", - "@types/react-window": "^1.8.8", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/dompurify": "^3.0.5", "react-scripts": "^5.0.1" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -58,12 +63,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -72,9 +77,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -82,22 +87,22 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -131,9 +136,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.6.tgz", - "integrity": "sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.29.7.tgz", + "integrity": "sha512-zxt+UJTOMKvUt3yOg+D58MLuz334pHp93qifMFcjIIO+9hN6t+ufw2gi7vDPMpxvfnHRR+3VVXvIjineCcgyXw==", "dev": true, "license": "MIT", "dependencies": { @@ -170,13 +175,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -186,27 +191,27 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -226,18 +231,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "dev": true, "license": "MIT", "dependencies": { - "@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.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "engines": { @@ -258,13 +263,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, @@ -303,51 +308,51 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -357,22 +362,22 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -380,15 +385,15 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -398,15 +403,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -416,41 +421,41 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -458,41 +463,41 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -502,14 +507,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -519,13 +524,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -535,13 +540,30 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -551,15 +573,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -569,14 +591,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", - "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/traverse": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -604,15 +626,15 @@ } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", - "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", + "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-syntax-decorators": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -763,13 +785,13 @@ } }, "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", - "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -779,14 +801,14 @@ } }, "node_modules/@babel/plugin-syntax-flow": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", - "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -796,13 +818,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", - "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -812,13 +834,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -854,13 +876,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -980,13 +1002,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1013,13 +1035,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1029,15 +1051,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", - "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.29.0" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1047,15 +1069,15 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", - "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1065,13 +1087,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1081,13 +1103,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1097,14 +1119,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1114,14 +1136,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", - "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1131,18 +1153,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1152,14 +1174,14 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1169,14 +1191,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1186,14 +1208,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", - "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1203,13 +1225,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1219,14 +1241,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1236,13 +1258,13 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1252,14 +1274,14 @@ } }, "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", - "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1269,13 +1291,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", - "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1285,13 +1307,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1301,14 +1323,14 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", - "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-flow": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1318,14 +1340,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1335,15 +1357,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1353,13 +1375,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", - "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1369,13 +1391,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1385,13 +1407,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1401,13 +1423,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1417,14 +1439,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1434,14 +1456,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1451,16 +1473,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz", + "integrity": "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.8" }, "engines": { "node": ">=6.9.0" @@ -1470,14 +1492,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1487,14 +1509,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1504,13 +1526,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1520,13 +1542,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1536,13 +1558,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1552,17 +1574,17 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1572,14 +1594,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1589,13 +1611,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1605,14 +1627,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1622,13 +1644,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1638,14 +1660,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1655,15 +1677,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1673,13 +1695,13 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1689,13 +1711,13 @@ } }, "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", - "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.29.7.tgz", + "integrity": "sha512-J0wGhKan+rIiE2OhfhRptySLrJ6SjQYM6b6N1FMlhyhCcw1Mig8vQjWchyB+bgHGDvaWo6Diu6CLRMra2uMtmg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1705,13 +1727,13 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1721,18 +1743,18 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", - "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-syntax-jsx": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1742,13 +1764,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz", + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.27.1" + "@babel/plugin-transform-react-jsx": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1758,14 +1780,14 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", - "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", + "integrity": "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1775,13 +1797,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1791,14 +1813,14 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", - "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1808,13 +1830,13 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1824,14 +1846,14 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", - "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", @@ -1869,13 +1891,13 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1885,14 +1907,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz", + "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1902,13 +1924,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1918,13 +1940,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1934,13 +1956,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1950,17 +1972,17 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", - "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1970,13 +1992,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1986,14 +2008,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", - "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2003,14 +2025,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2020,14 +2042,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", - "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2037,76 +2059,77 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", - "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.28.6", - "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.29.0", - "@babel/plugin-transform-async-to-generator": "^7.28.6", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.6", - "@babel/plugin-transform-class-properties": "^7.28.6", - "@babel/plugin-transform-class-static-block": "^7.28.6", - "@babel/plugin-transform-classes": "^7.28.6", - "@babel/plugin-transform-computed-properties": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-dotall-regex": "^7.28.6", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.6", - "@babel/plugin-transform-exponentiation-operator": "^7.28.6", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.28.6", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.29.0", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", - "@babel/plugin-transform-numeric-separator": "^7.28.6", - "@babel/plugin-transform-object-rest-spread": "^7.28.6", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.28.6", - "@babel/plugin-transform-optional-chaining": "^7.28.6", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.28.6", - "@babel/plugin-transform-private-property-in-object": "^7.28.6", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.29.0", - "@babel/plugin-transform-regexp-modifiers": "^7.28.6", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.28.6", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.28.6", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.15", "babel-plugin-polyfill-corejs3": "^0.14.0", @@ -2147,18 +2170,18 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", - "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz", + "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.28.0", - "@babel/plugin-transform-react-jsx": "^7.27.1", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-react-display-name": "^7.29.7", + "@babel/plugin-transform-react-jsx": "^7.29.7", + "@babel/plugin-transform-react-jsx-development": "^7.29.7", + "@babel/plugin-transform-react-pure-annotations": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2168,17 +2191,17 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2188,40 +2211,40 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -2229,18 +2252,40 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@base-ui/utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.2.tgz", + "integrity": "sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@floating-ui/utils": "^0.2.12", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -2802,9 +2847,9 @@ "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", "dependencies": { @@ -2862,10 +2907,20 @@ "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2884,6 +2939,12 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -2960,9 +3021,9 @@ } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "license": "MIT", "engines": { @@ -3305,9 +3366,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -3328,9 +3389,9 @@ "license": "MIT" }, "node_modules/@mui/core-downloads-tracker": { - "version": "7.3.9", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.9.tgz", - "integrity": "sha512-MOkOCTfbMJwLshlBCKJ59V2F/uaLYfmKnN76kksj6jlGUVdI25A9Hzs08m+zjBRdLv+sK7Rqdsefe8X7h/6PCw==", + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.11.tgz", + "integrity": "sha512-a7I/b/nBTdXYz2cOSlEmkQ9WWE1x8FHpqMhFPp+Y1VPFxcOw91G5ELOHARQAGSPy5V+UCgJua6K/1x70bAtQPw==", "license": "MIT", "funding": { "type": "opencollective", @@ -3338,9 +3399,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "7.3.9", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.9.tgz", - "integrity": "sha512-BT+zPJXss8Hg/oEMRmHl17Q97bPACG4ufFSfGEdhiE96jOyR5Dz1ty7ZWt1fVGR0y1p+sSgEwQT/MNZQmoWDCw==", + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.11.tgz", + "integrity": "sha512-+hz5ilwHZ3djd5es3sCErLioqe/NhZcYTsV/TNXZAMdJdb23F4xzJjqnnZdnurc3S1+ietcssRNqieOhPQLZ7Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6" @@ -3353,7 +3414,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^7.3.9", + "@mui/material": "^7.3.11", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -3364,17 +3425,17 @@ } }, "node_modules/@mui/material": { - "version": "7.3.9", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.9.tgz", - "integrity": "sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==", + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.11.tgz", + "integrity": "sha512-yq8bPc3LxOwKRWpcjRgDkYFmpM6aKlARfESTmOQcvLYFeJwtHte2tw6hJDrb8sk8wcvpDprHEHVaoUU0MslIkw==", "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.28.6", - "@mui/core-downloads-tracker": "^7.3.9", - "@mui/system": "^7.3.9", + "@mui/core-downloads-tracker": "^7.3.11", + "@mui/system": "^7.3.11", "@mui/types": "^7.4.12", - "@mui/utils": "^7.3.9", + "@mui/utils": "^7.3.11", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", @@ -3393,7 +3454,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^7.3.9", + "@mui/material-pigment-css": "^7.3.11", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -3414,13 +3475,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "7.3.9", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.9.tgz", - "integrity": "sha512-ErIyRQvsiQEq7Yvcvfw9UDHngaqjMy9P3JDPnRAaKG5qhpl2C4tX/W1S4zJvpu+feihmZJStjIyvnv6KDbIrlw==", + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.11.tgz", + "integrity": "sha512-9B+YKms0fRHbNrqp9tOT/DNbNnU5gyvJ1o3qAGXfq8GmZcbJnE3At9x07Zr/o0pkhzg4aDdwXVqe4+AcgtOCPA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", - "@mui/utils": "^7.3.9", + "@mui/utils": "^7.3.11", "prop-types": "^15.8.1" }, "engines": { @@ -3441,9 +3502,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "7.3.9", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.9.tgz", - "integrity": "sha512-JqujWt5bX4okjUPGpVof/7pvgClqh7HvIbsIBIOOlCh2u3wG/Bwp4+E1bc1dXSwkrkp9WUAoNdI5HEC+5HKvMw==", + "version": "7.3.10", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.10.tgz", + "integrity": "sha512-WxE9SiF8xskAQqGjsp0poXCkCqsoXFEsSr0HBXfApmGHR+DBnXRp+z46Vsltg4gpPM4Z96DeAQRpeAOnhNg7Ng==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", @@ -3475,17 +3536,17 @@ } }, "node_modules/@mui/system": { - "version": "7.3.9", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.9.tgz", - "integrity": "sha512-aL1q9am8XpRrSabv9qWf5RHhJICJql34wnrc1nz0MuOglPRYF/liN+c8VqZdTvUn9qg+ZjRVbKf4sJVFfIDtmg==", + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.11.tgz", + "integrity": "sha512-7izwGWdNawAKpBKcRlx7f2gFnAAjmASBWvMcyX4YYEeLOFsbfGRbUYGInvnAcUeql3rPxI7F9Ft4oY2OLRz44g==", "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.28.6", - "@mui/private-theming": "^7.3.9", - "@mui/styled-engine": "^7.3.9", + "@mui/private-theming": "^7.3.11", + "@mui/styled-engine": "^7.3.10", "@mui/types": "^7.4.12", - "@mui/utils": "^7.3.9", + "@mui/utils": "^7.3.11", "clsx": "^2.1.1", "csstype": "^3.2.3", "prop-types": "^15.8.1" @@ -3533,9 +3594,9 @@ } }, "node_modules/@mui/utils": { - "version": "7.3.9", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.9.tgz", - "integrity": "sha512-U6SdZaGbfb65fqTsH3V5oJdFj9uYwyLE2WVuNvmbggTSDBb8QHrFsqY8BN3taK9t3yJ8/BPHD/kNvLNyjwM7Yw==", + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz", + "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", @@ -3563,16 +3624,16 @@ } }, "node_modules/@mui/x-charts": { - "version": "8.28.2", - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.28.2.tgz", - "integrity": "sha512-xvQto+uVcwwWitZjzwmHaw1KZDsIju687kLSyOe3qsg4JYldgT+WWguBFQREF7Tsw7PFGAPahDAzbsTNuIkkTA==", + "version": "8.29.3", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.29.3.tgz", + "integrity": "sha512-8hF+rnf/rj258FmBMx6XwNmdFXM+gCnR9TKpd8ywJUwVPzZoZ0U/LMtsFHe16MZ9YOpOhTSdKHycq/OmiYXFlQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", "@mui/utils": "^7.3.5", - "@mui/x-charts-vendor": "8.26.0", - "@mui/x-internal-gestures": "0.4.0", - "@mui/x-internals": "8.26.0", + "@mui/x-charts-vendor": "8.29.0", + "@mui/x-internal-gestures": "0.5.0", + "@mui/x-internals": "8.29.3", "bezier-easing": "^2.1.0", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -3600,9 +3661,9 @@ } }, "node_modules/@mui/x-charts-vendor": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.26.0.tgz", - "integrity": "sha512-R//+WSWvsLJRTjTRN90EKX9sgRzAb4HQBvtUA3cTQpkGrmEjmatD4BJAm3IdRdkSagf6yKWF+ypESctyRhbwnA==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.29.0.tgz", + "integrity": "sha512-nYjgW337QxvCJXct8b0UIir9ELDjMEfWSmMVSfUQsEjn3ZD5jjK3MQVZSHSM1hP79PZcjRQIAuWI1fHkqG04ug==", "license": "MIT AND ISC", "dependencies": { "@babel/runtime": "^7.28.4", @@ -3612,13 +3673,13 @@ "@types/d3-interpolate": "^3.0.4", "@types/d3-path": "^3.1.1", "@types/d3-scale": "^4.0.9", - "@types/d3-shape": "^3.1.7", + "@types/d3-shape": "^3.1.8", "@types/d3-time": "^3.0.4", "@types/d3-time-format": "^4.0.3", "@types/d3-timer": "^3.0.2", "d3-array": "^3.2.4", "d3-color": "^3.1.0", - "d3-format": "^3.1.0", + "d3-format": "^3.1.2", "d3-interpolate": "^3.0.1", "d3-path": "^3.1.0", "d3-scale": "^4.0.2", @@ -3631,15 +3692,16 @@ } }, "node_modules/@mui/x-data-grid": { - "version": "8.28.2", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.28.2.tgz", - "integrity": "sha512-nqVhOtXOfPcKtoULuZuUp9Hdgzcn4xzqjYwqxZkTPnzKGZqkXPGaupy6n+9ssyYg7jg9ot+zNUJB0sbsvlDY/g==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-9.12.0.tgz", + "integrity": "sha512-lI7NYAPhoLIWJECB2gZuVairzWviDDOE1RpjLcPXJM+hw1mwCG+isqNfx81X+17xuHtnEEBVahCDWAJFAvCF9g==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", - "@mui/utils": "^7.3.5", - "@mui/x-internals": "8.26.0", - "@mui/x-virtualizer": "0.3.4", + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "^0.3.1", + "@mui/utils": "^9.3.0", + "@mui/x-internals": "^9.12.0", + "@mui/x-virtualizer": "0.7.0", "clsx": "^2.1.1", "prop-types": "^15.8.1", "use-sync-external-store": "^1.6.0" @@ -3654,8 +3716,8 @@ "peerDependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", - "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", - "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/material": "^7.3.0 || ^9.0.0", + "@mui/system": "^7.3.0 || ^9.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -3668,19 +3730,90 @@ } } }, + "node_modules/@mui/x-data-grid/node_modules/@mui/types": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.4.0.tgz", + "integrity": "sha512-13DH0Oniua4WmGqeYbQAZqmiN7r4nfd0zwIoGJ5QeJwyIHgEmf+LkRw/jV1HZb6AiUoHX8Oxf4xgalq3vYHcIw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/@mui/utils": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.4.0.tgz", + "integrity": "sha512-WkzY8uYtqUDyGKPShrRQRBomfMQHddlsyKu/gRRESYO24tDPD2z0xRE8CBoUsWqpBa+YrFs9KixiS7kQRjsPAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/types": "^9.4.0", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/@mui/x-internals": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.12.0.tgz", + "integrity": "sha512-rpG9EgJhH3JeOK2Q/tKaUWk5vP8C1NfL8DaXMKR1XCA7RhpZUI/xT67WKY80QtogNWu7U/BRezKyQGtiZ8soYg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "^0.3.1", + "@mui/utils": "^9.3.0", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mui/x-internal-gestures": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-0.4.0.tgz", - "integrity": "sha512-i0W6v9LoiNY8Yf1goOmaygtz/ncPJGBedhpDfvNg/i8BvzPwJcBaeW4rqPucJfVag9KQ8MSssBBrvYeEnrQmhw==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-0.5.0.tgz", + "integrity": "sha512-LTnaD8WAWld9ecW8Q5tO4Sq2ucyMWca4SKDu6Cx+0RHO3XfgMa7NxB7aBzldAYMN8QrSquBVdu8Zcf9cnp8qwQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4" } }, "node_modules/@mui/x-internals": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.26.0.tgz", - "integrity": "sha512-B9OZau5IQUvIxwpJZhoFJKqRpmWf5r0yMmSXjQuqb5WuqM755EuzWJOenY48denGoENzMLT8hQpA0hRTeU2IPA==", + "version": "8.29.3", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.29.3.tgz", + "integrity": "sha512-xJSaAXQDZ+35svja+i5N0qg7gTbZwcnAGZBgC+37Z3SoxRlTR5NwNBUlqKDm5TJwLvG17DeDYolxqWtcR93tsg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -3700,14 +3833,86 @@ } }, "node_modules/@mui/x-virtualizer": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.3.4.tgz", - "integrity": "sha512-b80Wt08bT+KVDunurdd4es76PdYaazoNGpGde3Ki7jGVaSpoljMYSRVlCtifei+4cOUD3NKJhG4e3av/czTekg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.7.0.tgz", + "integrity": "sha512-Bz5p/t78zE2q6rf/nHY3j29hr7JlWaBNT0OD8jZFcfstr8jqrPkzirGsl/Bz5EpEjEzxV73kLfZLpIToGql+rA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", - "@mui/utils": "^7.3.5", - "@mui/x-internals": "8.26.0" + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "^0.3.1", + "@mui/utils": "^9.3.0", + "@mui/x-internals": "^9.12.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-virtualizer/node_modules/@mui/types": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.4.0.tgz", + "integrity": "sha512-13DH0Oniua4WmGqeYbQAZqmiN7r4nfd0zwIoGJ5QeJwyIHgEmf+LkRw/jV1HZb6AiUoHX8Oxf4xgalq3vYHcIw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-virtualizer/node_modules/@mui/utils": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.4.0.tgz", + "integrity": "sha512-WkzY8uYtqUDyGKPShrRQRBomfMQHddlsyKu/gRRESYO24tDPD2z0xRE8CBoUsWqpBa+YrFs9KixiS7kQRjsPAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/types": "^9.4.0", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-virtualizer/node_modules/@mui/x-internals": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.12.0.tgz", + "integrity": "sha512-rpG9EgJhH3JeOK2Q/tKaUWk5vP8C1NfL8DaXMKR1XCA7RhpZUI/xT67WKY80QtogNWu7U/BRezKyQGtiZ8soYg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "^0.3.1", + "@mui/utils": "^9.3.0", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" }, "engines": { "node": ">=14.0.0" @@ -3863,27 +4068,27 @@ } }, "node_modules/@react-spring/animated": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.3.tgz", - "integrity": "sha512-7MrxADV3vaUADn2V9iYhaIL6iOWRx9nCJjYrsk2AHD2kwPr6fg7Pt0v+deX5RnCDmCKNnD6W5fasiyM8D+wzJQ==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.1.2.tgz", + "integrity": "sha512-yAsQ/bbp6+vko7WNCI1M00c6KLE9XKTGCrgRhQqS4JcK3oF5qBV4rHYrQEprvEvYXxt+1H5FsLS2eVValEPfFw==", "license": "MIT", "dependencies": { - "@react-spring/shared": "~10.0.3", - "@react-spring/types": "~10.0.3" + "@react-spring/shared": "~10.1.2", + "@react-spring/types": "~10.1.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@react-spring/core": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.0.3.tgz", - "integrity": "sha512-D4DwNO68oohDf/0HG2G0Uragzb9IA1oXblxrd6MZAcBcUQG2EHUWXewjdECMPLNmQvlYVyyBRH6gPxXM5DX7DQ==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.1.2.tgz", + "integrity": "sha512-lPGOAg0V+PV3ucopOajD+YCxoe8twALqAeG4c/+sqVjBsyUNNdx8qfz/DcNSPKA8PV3+AyQELlCxlDfap9cmBQ==", "license": "MIT", "dependencies": { - "@react-spring/animated": "~10.0.3", - "@react-spring/shared": "~10.0.3", - "@react-spring/types": "~10.0.3" + "@react-spring/animated": "~10.1.2", + "@react-spring/shared": "~10.1.2", + "@react-spring/types": "~10.1.2" }, "funding": { "type": "opencollective", @@ -3894,214 +4099,47 @@ } }, "node_modules/@react-spring/rafz": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.3.tgz", - "integrity": "sha512-Ri2/xqt8OnQ2iFKkxKMSF4Nqv0LSWnxXT4jXFzBDsHgeeH/cHxTLupAWUwmV9hAGgmEhBmh5aONtj3J6R/18wg==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.1.2.tgz", + "integrity": "sha512-KC6vSFZyPnRJ2rXqipV9QqR4SaYbYXjvKfdYKWipNK2mWuV79gr20WmpVUOsTiEHGRY3WSOdWCHN+P9Pdaqb7Q==", "license": "MIT" }, "node_modules/@react-spring/shared": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.3.tgz", - "integrity": "sha512-geCal66nrkaQzUVhPkGomylo+Jpd5VPK8tPMEDevQEfNSWAQP15swHm+MCRG4wVQrQlTi9lOzKzpRoTL3CA84Q==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.1.2.tgz", + "integrity": "sha512-47/8bNQ/o0uEmxEnPBuERlC29VSqiTn5P9ln9tUMSVkYKYxBtouYE686F5kAGj+eHRPoNCw7drxWE9nv2d1LMw==", "license": "MIT", "dependencies": { - "@react-spring/rafz": "~10.0.3", - "@react-spring/types": "~10.0.3" + "@react-spring/rafz": "~10.1.2", + "@react-spring/types": "~10.1.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@react-spring/three": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz", - "integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/core": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "@react-three/fiber": ">=6.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "three": ">=0.126" - } - }, - "node_modules/@react-spring/three/node_modules/@react-spring/animated": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", - "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", - "license": "MIT", - "dependencies": { - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/three/node_modules/@react-spring/core": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", - "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/three/node_modules/@react-spring/rafz": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", - "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", - "license": "MIT" - }, - "node_modules/@react-spring/three/node_modules/@react-spring/shared": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", - "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", - "license": "MIT", - "dependencies": { - "@react-spring/rafz": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/three/node_modules/@react-spring/types": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", - "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", - "license": "MIT" - }, "node_modules/@react-spring/types": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.0.3.tgz", - "integrity": "sha512-H5Ixkd2OuSIgHtxuHLTt7aJYfhMXKXT/rK32HPD/kSrOB6q6ooeiWAXkBy7L8F3ZxdkBb9ini9zP9UwnEFzWgQ==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.1.2.tgz", + "integrity": "sha512-G4CWowmVPz+rDG1y9QRVq/prZNxiNwQaHC0kgT/MgY6jAGgdRgTV4VThpvJDwWvUitk3xZB4soP4d36fjeQ09g==", "license": "MIT" }, "node_modules/@react-spring/web": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-10.0.3.tgz", - "integrity": "sha512-ndU+kWY81rHsT7gTFtCJ6mrVhaJ6grFmgTnENipzmKqot4HGf5smPNK+cZZJqoGeDsj9ZsiWPW4geT/NyD484A==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-10.1.2.tgz", + "integrity": "sha512-KxDB3zaDqy9qFsu7fdxjyraAxweHH4k5TW5WGT/OuMK6hKaxSDfhrQfRBzfFaSVvMBf+NghbJFanllV4ia6i7A==", "license": "MIT", "dependencies": { - "@react-spring/animated": "~10.0.3", - "@react-spring/core": "~10.0.3", - "@react-spring/shared": "~10.0.3", - "@react-spring/types": "~10.0.3" + "@react-spring/animated": "~10.1.2", + "@react-spring/core": "~10.1.2", + "@react-spring/shared": "~10.1.2", + "@react-spring/types": "~10.1.2", + "csstype": "^3.2.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@react-three/drei": { - "version": "8.20.2", - "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-8.20.2.tgz", - "integrity": "sha512-V2P/YHYg2+N58eWtIqmOJf0t5nWGTcmpT+ci4tB5gt3CsWU2vuoeQFhvBmqEF+cXwZD5ZYKPfQDf/06NzTx/lw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.2", - "@react-spring/three": "^9.3.1", - "@use-gesture/react": "^10.2.0", - "detect-gpu": "^4.0.14", - "glsl-noise": "^0.0.0", - "lodash.omit": "^4.5.0", - "lodash.pick": "^4.4.0", - "meshline": "^2.0.4", - "react-composer": "^5.0.2", - "react-merge-refs": "^1.1.0", - "stats.js": "^0.17.0", - "suspend-react": "^0.0.8", - "three-mesh-bvh": "^0.5.7", - "three-stdlib": "^2.8.9", - "troika-three-text": "^0.46.3", - "utility-types": "^3.10.0", - "zustand": "^3.5.13" - }, - "peerDependencies": { - "@react-three/fiber": ">=6.0", - "react": ">=17.0", - "react-dom": ">=17.0", - "three": ">=0.137" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/@react-three/fiber": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz", - "integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.17.8", - "@types/react-reconciler": "^0.26.7", - "@types/webxr": "*", - "base64-js": "^1.5.1", - "buffer": "^6.0.3", - "its-fine": "^1.0.6", - "react-reconciler": "^0.27.0", - "react-use-measure": "^2.1.7", - "scheduler": "^0.21.0", - "suspend-react": "^0.1.3", - "zustand": "^3.7.1" - }, - "peerDependencies": { - "expo": ">=43.0", - "expo-asset": ">=8.4", - "expo-file-system": ">=11.0", - "expo-gl": ">=11.0", - "react": ">=18 <19", - "react-dom": ">=18 <19", - "react-native": ">=0.64", - "three": ">=0.133" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - }, - "expo-asset": { - "optional": true - }, - "expo-file-system": { - "optional": true - }, - "expo-gl": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/@react-three/fiber/node_modules/suspend-react": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", - "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", - "license": "MIT", - "peerDependencies": { - "react": ">=17.0" - } - }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4201,9 +4239,9 @@ "license": "MIT" }, "node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", + "version": "0.24.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.52.tgz", + "integrity": "sha512-DNKwjDaMLKWXvjs/zCkSzA3rBzZVv4tuLCEV8ArRyHAqCtPtbUcQj0XS4h7y/CqL9pI6VpCmcH5A6oPOBAD9+Q==", "dev": true, "license": "MIT" }, @@ -4476,9 +4514,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.96.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.96.2.tgz", - "integrity": "sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==", + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.102.8.tgz", + "integrity": "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg==", "license": "MIT", "funding": { "type": "github", @@ -4486,12 +4524,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.96.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.96.2.tgz", - "integrity": "sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA==", + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.102.8.tgz", + "integrity": "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.96.2" + "@tanstack/query-core": "5.102.8" }, "funding": { "type": "github", @@ -4501,20 +4539,110 @@ "react": "^18 || ^19" } }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, "engines": { - "node": ">= 6" + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz", + "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" } }, "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", "dev": true, "license": "MIT" }, @@ -4539,10 +4667,11 @@ "dev": true, "license": "MIT" }, - "node_modules/@tweenjs/tween.js": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", - "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, "license": "MIT" }, "node_modules/@types/babel__core": { @@ -4675,9 +4804,9 @@ } }, "node_modules/@types/d3-shape": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -4701,11 +4830,15 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, - "node_modules/@types/draco3d": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", - "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", - "license": "MIT" + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } }, "node_modules/@types/eslint": { "version": "8.56.12", @@ -4718,21 +4851,10 @@ "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -4750,9 +4872,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", "dev": true, "license": "MIT", "dependencies": { @@ -4763,9 +4885,9 @@ } }, "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", "dev": true, "license": "MIT", "dependencies": { @@ -4850,23 +4972,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash.debounce": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", - "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4875,14 +4980,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", - "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", + "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~8.3.0" } }, "node_modules/@types/node-forge": { @@ -4895,12 +5000,6 @@ "@types/node": "*" } }, - "node_modules/@types/offscreencanvas": { - "version": "2019.7.3", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", - "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", - "license": "MIT" - }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -4928,9 +5027,9 @@ "license": "MIT" }, "node_modules/@types/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", "dev": true, "license": "MIT" }, @@ -4957,15 +5056,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-reconciler": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz", - "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==", - "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" } @@ -4979,16 +5070,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-window": { - "version": "1.8.8", - "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", - "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -5007,9 +5088,9 @@ "license": "MIT" }, "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==", "dev": true, "license": "MIT" }, @@ -5077,13 +5158,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/webxr": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", - "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/ws": { @@ -5356,30 +5431,12 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", "dev": true, "license": "ISC" }, - "node_modules/@use-gesture/core": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", - "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", - "license": "MIT" - }, - "node_modules/@use-gesture/react": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", - "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", - "license": "MIT", - "dependencies": { - "@use-gesture/core": "10.3.1" - }, - "peerDependencies": { - "react": ">= 16.8.0" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -5587,19 +5644,10 @@ "node": ">= 0.6" } }, - "node_modules/accessor-fn": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", - "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", "peer": true, @@ -5634,19 +5682,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -5705,9 +5740,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "peer": true, @@ -5741,9 +5776,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -5840,16 +5875,13 @@ } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -5894,13 +5926,13 @@ } }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" + "dependencies": { + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { @@ -6152,9 +6184,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.27", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", - "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", "dev": true, "funding": [ { @@ -6172,8 +6204,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001774", + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -6205,9 +6237,9 @@ } }, "node_modules/axe-core": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.2.tgz", - "integrity": "sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", "dev": true, "license": "MPL-2.0", "engines": { @@ -6500,39 +6532,10 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { - "version": "2.10.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.14.tgz", - "integrity": "sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA==", + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6555,16 +6558,6 @@ "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==", "license": "MIT" }, - "node_modules/bezier-js": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", - "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==", - "license": "MIT", - "funding": { - "type": "individual", - "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" - } - }, "node_modules/bfj": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz", @@ -6582,15 +6575,6 @@ "node": ">= 8.0.0" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -6622,9 +6606,9 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "dev": true, "license": "MIT", "dependencies": { @@ -6636,7 +6620,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.14.0", + "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -6677,9 +6661,9 @@ "license": "MIT" }, "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.4.tgz", + "integrity": "sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw==", "dev": true, "license": "MIT", "dependencies": { @@ -6695,9 +6679,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -6726,9 +6710,9 @@ "license": "BSD-2-Clause" }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -6747,11 +6731,11 @@ "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -6770,30 +6754,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6825,15 +6785,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -6931,9 +6891,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001785", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz", - "integrity": "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -6951,18 +6911,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/canvas-color-tracker": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz", - "integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==", - "license": "MIT", - "dependencies": { - "tinycolor2": "^1.6.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", @@ -6990,6 +6938,22 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -7275,9 +7239,9 @@ "license": "MIT" }, "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", "dev": true, "license": "MIT" }, @@ -7451,25 +7415,31 @@ "license": "MIT" }, "node_modules/core-js": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", - "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", "dev": true, "hasInstallScript": true, "license": "MIT", + "engines": { + "node": "*" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, "node_modules/core-js-compat": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", - "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", + "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.28.1" + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" }, "funding": { "type": "opencollective", @@ -7477,12 +7447,15 @@ } }, "node_modules/core-js-pure": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", - "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.50.0.tgz", + "integrity": "sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==", "dev": true, "hasInstallScript": true, "license": "MIT", + "engines": { + "node": "*" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -7614,15 +7587,6 @@ "postcss": "^8.4" } }, - "node_modules/css-line-break": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/css-loader": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", @@ -7785,6 +7749,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssdb": { "version": "7.11.2", "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", @@ -7983,12 +7954,6 @@ "node": ">=12" } }, - "node_modules/d3-binarytree": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", - "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", - "license": "MIT" - }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -7998,66 +7963,19 @@ "node": ">=12" } }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force-3d": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", - "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", - "license": "MIT", - "dependencies": { - "d3-binarytree": "1", - "d3-dispatch": "1 - 3", - "d3-octree": "1", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -8066,12 +7984,6 @@ "node": ">=12" } }, - "node_modules/d3-octree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", - "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", - "license": "MIT" - }, "node_modules/d3-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", @@ -8081,15 +7993,6 @@ "node": ">=12" } }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -8106,29 +8009,6 @@ "node": ">=12" } }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", @@ -8174,41 +8054,6 @@ "node": ">=12" } }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8286,9 +8131,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.20", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", - "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", "license": "MIT" }, "node_modules/debug": { @@ -8418,6 +8263,16 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -8429,15 +8284,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-gpu": { - "version": "4.0.50", - "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-4.0.50.tgz", - "integrity": "sha512-T67HE5+ONONN8rPXCBJPupyCg2QT8+l2NUUMuPxAppsMJBDPG/Jg0URLs6GyDzLm2niUE+oncIHSuy3VinoPeQ==", - "license": "MIT", - "dependencies": { - "webgl-constants": "^1.1.1" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -8563,6 +8409,13 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -8651,6 +8504,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.14.tgz", + "integrity": "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -8694,12 +8556,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/draco3d": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", - "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", - "license": "Apache-2.0" - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -8746,9 +8602,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.331", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", - "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "version": "1.5.415", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.415.tgz", + "integrity": "sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==", "dev": true, "license": "ISC" }, @@ -8793,14 +8649,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -8836,9 +8692,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -8904,6 +8760,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-array-method-boxes-properly": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", @@ -8925,23 +8800,22 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-iterator-helpers": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", - "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", + "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.24.1", + "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", @@ -8953,24 +8827,23 @@ "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", - "math-intrinsics": "^1.1.0", - "safe-array-concat": "^1.1.3" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -9010,15 +8883,18 @@ } }, "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", "dev": true, "license": "MIT", "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -9199,14 +9075,14 @@ } }, "node_modules/eslint-import-resolver-node/node_modules/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", + "is-core-module": "^2.16.2", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", @@ -9223,9 +9099,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", "dev": true, "license": "MIT", "dependencies": { @@ -9391,6 +9267,16 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -9451,14 +9337,14 @@ } }, "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", + "is-core-module": "^2.16.2", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", @@ -9612,10 +9498,20 @@ } }, "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9834,15 +9730,15 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", + "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -9861,7 +9757,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -9959,9 +9855,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", "dev": true, "funding": [ { @@ -9976,9 +9872,9 @@ "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.2.tgz", + "integrity": "sha512-UpGiiODyCGprM8EPP6JodP6jC9Rws6TCuiDOD+nn0CJhR8guI3g/ozo4ugL0vJ+Yz1UtJuuRPqvQuybVOF1VQA==", "dev": true, "license": "ISC", "dependencies": { @@ -10008,12 +9904,6 @@ "bser": "2.1.1" } }, - "node_modules/fflate": { - "version": "0.6.10", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", - "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", - "license": "MIT" - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -10078,9 +9968,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -10223,36 +10113,22 @@ } }, "node_modules/flatqueue": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/flatqueue/-/flatqueue-3.0.0.tgz", - "integrity": "sha512-y1deYaVt+lIc/d2uIcWDNd0CrdQTO5xoCjeFdhX0kSXvm2Acm0o+3bAOiYklTEoRyzwio3sv3/IiBZdusbAe2Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flatqueue/-/flatqueue-3.1.0.tgz", + "integrity": "sha512-Ia4qIYrrsEqIRx3c3XhkT+QDLQuUV5ovsr6ah1rIgKT5wclhoGK3lAMS1bWRAWxlx7wtlTBpV7QXB5d9fOSRxA==", "license": "ISC" }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, - "node_modules/float-tooltip": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", - "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", - "license": "MIT", - "dependencies": { - "d3-selection": "2 - 3", - "kapsule": "^1.16", - "preact": "10" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -10286,145 +10162,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/force-graph": { - "version": "1.51.2", - "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.2.tgz", - "integrity": "sha512-zZNdMqx8qIQGurgnbgYIUsdXxSfvhfRSIdncsKGv/twUOZpwCsk9hPHmdjdcme1+epATgb41G0rkIGHJ0Wydng==", - "license": "MIT", - "dependencies": { - "@tweenjs/tween.js": "18 - 25", - "accessor-fn": "1", - "bezier-js": "3 - 6", - "canvas-color-tracker": "^1.3", - "d3-array": "1 - 3", - "d3-drag": "2 - 3", - "d3-force-3d": "2 - 3", - "d3-scale": "1 - 4", - "d3-scale-chromatic": "1 - 3", - "d3-selection": "2 - 3", - "d3-zoom": "2 - 3", - "float-tooltip": "^1.7", - "index-array-by": "1", - "kapsule": "^1.16", - "lodash-es": "4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/form-data": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", - "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz", + "integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35" }, "engines": { @@ -10456,13 +10204,13 @@ } }, "node_modules/framer-motion": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", - "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.43.0.tgz", + "integrity": "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==", "license": "MIT", "dependencies": { - "motion-dom": "^12.38.0", - "motion-utils": "^12.36.0", + "motion-dom": "^12.43.0", + "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -10546,18 +10294,21 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -10728,13 +10479,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -10830,12 +10574,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glsl-noise": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", - "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", - "license": "MIT" - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -10975,9 +10713,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -11133,10 +10871,16 @@ "node": ">=12" } }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, "node_modules/html-webpack-plugin": { - "version": "5.6.6", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.6.tgz", - "integrity": "sha512-bLjW01UTrvoWTJQL5LsMRo1SypHW80FTm12OJRSnr3v6YHNhfe+1r0MYUZJMACxnCHURVnBWRwAsWs2yPU9Ezw==", + "version": "5.6.8", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.8.tgz", + "integrity": "sha512-MZmKQcTnhEh1SPSyMiEytIeDZDUoBZVorNHivQGXMASHf/BSGGOrKa2xQ5bGx3TCe1n109ecCt+cpww7wwWhKA==", "dev": true, "license": "MIT", "dependencies": { @@ -11154,7 +10898,7 @@ "url": "https://opencollective.com/html-webpack-plugin" }, "peerDependencies": { - "@rspack/core": "0.x || 1.x", + "@rspack/core": "0.x || 1.x || 2.x", "webpack": "^5.20.0" }, "peerDependenciesMeta": { @@ -11166,19 +10910,6 @@ } } }, - "node_modules/html2canvas": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "license": "MIT", - "dependencies": { - "css-line-break": "^2.1.0", - "text-segmentation": "^1.0.3" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -11265,9 +10996,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz", + "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11359,26 +11090,6 @@ "node": ">=4" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -11446,13 +11157,14 @@ "node": ">=0.8.19" } }, - "node_modules/index-array-by": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", - "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" } }, "node_modules/inflight": { @@ -11506,9 +11218,9 @@ } }, "node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", "dev": true, "license": "MIT", "engines": { @@ -11619,12 +11331,12 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -11684,6 +11396,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -12223,27 +11951,6 @@ "node": ">= 0.4" } }, - "node_modules/its-fine": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", - "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", - "license": "MIT", - "dependencies": { - "@types/react-reconciler": "^0.28.0" - }, - "peerDependencies": { - "react": ">=18.0" - } - }, - "node_modules/its-fine/node_modules/@types/react-reconciler": { - "version": "0.28.9", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", - "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, "node_modules/jake": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", @@ -12262,15 +11969,6 @@ "node": ">=10" } }, - "node_modules/jerrypick": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", - "integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", @@ -12947,19 +12645,6 @@ "@types/yargs-parser": "*" } }, - "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/jest-watch-typeahead/node_modules/emittery": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", @@ -13159,9 +12844,9 @@ } }, "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", "engines": { @@ -13239,9 +12924,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, "license": "MIT", "dependencies": { @@ -13352,9 +13037,9 @@ } }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13415,18 +13100,6 @@ "node": ">=4.0" } }, - "node_modules/kapsule": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", - "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", - "license": "MIT", - "dependencies": { - "lodash-es": "4" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -13488,14 +13161,14 @@ } }, "node_modules/launch-editor": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", - "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "shell-quote": "^1.8.4" } }, "node_modules/leven": { @@ -13538,20 +13211,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", @@ -13587,16 +13246,11 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, "license": "MIT" }, "node_modules/lodash.memoize": { @@ -13613,26 +13267,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.omit": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.18.0.tgz", - "integrity": "sha512-hZXIupXdHtocTnvIJ2aCd2vxKYtxex6gbiGuPvgBRnFQO9yu3AtmDAbVuCXcSsQx3INo/1g71OktlFFA/ES8Xg==", - "license": "MIT" - }, - "node_modules/lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", - "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.", - "license": "MIT" - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -13672,6 +13306,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -13765,12 +13409,6 @@ "node": ">= 4.0.0" } }, - "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "license": "MIT" - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -13798,15 +13436,6 @@ "node": ">= 8" } }, - "node_modules/meshline": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/meshline/-/meshline-2.0.4.tgz", - "integrity": "sha512-Jh6DJl/zLqA4xsKvGv5950jr2ukyXQE1wgxs8u94cImHrvL6soVIggqjP+2hVHZXGYaKnWszhtjuCbKNeQyYiw==", - "license": "MIT", - "peerDependencies": { - "three": ">=0.137" - } - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -13877,6 +13506,16 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.10.2", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", @@ -13928,6 +13567,67 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.8.0.tgz", + "integrity": "sha512-2cT9+goJfBhtMz+gJqejSf09ClgmYhPhccRb/fb0ztVbixt0BkO8mRuI26FoPPuUNkRk6iEDryWAIouc5W8eJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.3", + "terser": "^5.51.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -13942,18 +13642,18 @@ } }, "node_modules/motion-dom": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", - "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.43.0.tgz", + "integrity": "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==", "license": "MIT", "dependencies": { - "motion-utils": "^12.36.0" + "motion-utils": "^12.39.0" } }, "node_modules/motion-utils": { - "version": "12.36.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", - "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", "license": "MIT" }, "node_modules/ms": { @@ -13989,9 +13689,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -14050,9 +13750,9 @@ } }, "node_modules/node-exports-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", "dev": true, "license": "MIT", "dependencies": { @@ -14096,11 +13796,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -14152,9 +13855,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", "dev": true, "license": "MIT" }, @@ -14405,13 +14108,14 @@ } }, "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.6", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" }, @@ -14622,16 +14326,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -14729,9 +14423,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -14750,7 +14444,7 @@ "license": "MIT", "peer": true, "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -15436,9 +15130,9 @@ } }, "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "dev": true, "license": "MIT", "dependencies": { @@ -15466,9 +15160,9 @@ } }, "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "dev": true, "license": "MIT", "dependencies": { @@ -15946,9 +15640,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "peer": true, @@ -16009,9 +15703,9 @@ "license": "CC0-1.0" }, "node_modules/postcss-svgo/node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -16029,9 +15723,9 @@ } }, "node_modules/postcss-svgo/node_modules/svgo": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.2.tgz", - "integrity": "sha512-TyzE4NVGLUFy+H/Uy4N6c3G0HEeprsVfge6Lmq+0FdQQ/zqoVYB62IsBZORsiL+o96s6ff/V6/3UQo/C0cgCAA==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.4.tgz", + "integrity": "sha512-2GJ4h3rl13qYTdwllaK6QlL8tG+UrM8626V2Ylcd/yBUv2Y/EwLsYisVV6UDCRmlk+C74G8nMpxJCk0RWPdDCw==", "dev": true, "license": "MIT", "dependencies": { @@ -16073,22 +15767,6 @@ "dev": true, "license": "MIT" }, - "node_modules/potpack": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", - "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", - "license": "ISC" - }, - "node_modules/preact": { - "version": "10.29.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", - "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -16138,19 +15816,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/pretty-format/node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -16266,13 +15931,14 @@ } }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -16399,18 +16065,6 @@ "node": ">=14" } }, - "node_modules/react-composer": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz", - "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.6.0" - }, - "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -16447,6 +16101,23 @@ "node": ">=14" } }, + "node_modules/react-dev-utils/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/react-dev-utils/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -16464,6 +16135,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-dev-utils/node_modules/fork-ts-checker-webpack-plugin": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", + "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=10", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "eslint": ">= 6", + "typescript": ">= 2.7", + "vue-template-compiler": "*", + "webpack": ">= 4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/react-dev-utils/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/react-dev-utils/node_modules/loader-utils": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", @@ -16522,6 +16249,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-dev-utils/node_modules/schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/react-dev-utils/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -16536,15 +16292,6 @@ "react": "^18.3.1" } }, - "node_modules/react-dom/node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/react-error-overlay": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz", @@ -16552,70 +16299,12 @@ "dev": true, "license": "MIT" }, - "node_modules/react-force-graph-2d": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/react-force-graph-2d/-/react-force-graph-2d-1.29.1.tgz", - "integrity": "sha512-1Rl/1Z3xy2iTHKj6a0jRXGyiI86xUti81K+jBQZ+Oe46csaMikp47L5AjrzA9hY9fNGD63X8ffrqnvaORukCuQ==", - "license": "MIT", - "dependencies": { - "force-graph": "^1.51", - "prop-types": "15", - "react-kapsule": "^2.5" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": "*" - } - }, "node_modules/react-is": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", - "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", "license": "MIT" }, - "node_modules/react-kapsule": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.5.7.tgz", - "integrity": "sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==", - "license": "MIT", - "dependencies": { - "jerrypick": "^1.1.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": ">=16.13.1" - } - }, - "node_modules/react-merge-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz", - "integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/react-reconciler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", - "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.21.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^18.0.0" - } - }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -16628,9 +16317,9 @@ } }, "node_modules/react-router": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", - "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -16650,12 +16339,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", - "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", "license": "MIT", "dependencies": { - "react-router": "7.14.0" + "react-router": "7.18.2" }, "engines": { "node": ">=20.0.0" @@ -16756,47 +16445,12 @@ "react-dom": ">=16.6.0" } }, - "node_modules/react-use-measure": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", - "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.13", - "react-dom": ">=16.13" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-window": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz", - "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "memoize-one": ">=3.1.1 <6" - }, - "engines": { - "node": ">8.0.0" - }, - "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } + "license": "MIT" }, "node_modules/readable-stream": { "version": "3.6.2", @@ -16839,6 +16493,20 @@ "node": ">=6.0.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -16943,9 +16611,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", - "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -16993,6 +16661,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17006,17 +16675,18 @@ "license": "MIT" }, "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", + "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -17259,15 +16929,15 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, @@ -17408,9 +17078,9 @@ } }, "node_modules/scheduler": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", - "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -17437,9 +17107,9 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "peer": true, @@ -17496,9 +17166,9 @@ } }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -17752,9 +17422,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "dev": true, "license": "MIT", "engines": { @@ -17765,15 +17435,15 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -17785,14 +17455,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -17865,9 +17535,9 @@ } }, "node_modules/smiles-drawer": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/smiles-drawer/-/smiles-drawer-2.2.1.tgz", - "integrity": "sha512-PM49IH6DbkURkxivNPU9k6TpTlSPdyNpPmPDx/C7sbMqxvlPs3plaMVcDOH0GHnjHYdorQuYL/CJ5vkZkYKqUQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/smiles-drawer/-/smiles-drawer-2.4.1.tgz", + "integrity": "sha512-kJeD91Bp8EwLTQouq4az7hn5zUYdIaLQoyFt8XlsqriugPiZOdBFADJMUnfqDYpyu3XK9SFpBgTuDtczpbSUbA==", "license": "MIT", "dependencies": { "chroma-js": "^2.4.2" @@ -18049,12 +17719,6 @@ "escodegen": "^2.1.0" } }, - "node_modules/stats.js": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", - "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", - "license": "MIT" - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -18187,19 +17851,20 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -18209,16 +17874,16 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.2" }, "engines": { "node": ">= 0.4" @@ -18303,6 +17968,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -18428,21 +18106,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/suspend-react": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.0.8.tgz", - "integrity": "sha512-ZC3r8Hu1y0dIThzsGw0RLZplnX9yXwfItcvaIzJc2VQVi8TGyGDlu92syMB5ulybfvGLHAI5Ghzlk23UBPF8xg==", - "license": "MIT", - "peerDependencies": { - "react": ">=17.0" - } - }, "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.1.0.tgz", + "integrity": "sha512-bwLf38YmY+TDYHJw1Ex0Co8c4yeXuJAo8YnXGZrscxrvYoVVIvLeniEkV1Ks/54VteMnL4FtyN1+P+TZJdUPmQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/svgo": { "version": "1.3.2", @@ -18718,9 +18390,9 @@ } }, "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "optional": true, @@ -18736,9 +18408,9 @@ } }, "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -18809,9 +18481,9 @@ } }, "node_modules/terser": { - "version": "5.46.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", - "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -18828,9 +18500,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", - "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18850,12 +18522,39 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, "@swc/core": { "optional": true }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, "esbuild": { "optional": true }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, "uglify-js": { "optional": true } @@ -18883,15 +18582,6 @@ "node": ">=8" } }, - "node_modules/text-segmentation": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -18922,39 +18612,6 @@ "node": ">=0.8" } }, - "node_modules/three": { - "version": "0.175.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.175.0.tgz", - "integrity": "sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg==", - "license": "MIT", - "peer": true - }, - "node_modules/three-mesh-bvh": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.5.24.tgz", - "integrity": "sha512-VTIgfjz8aFoPKTQoMIQQv9jJD4ybFRZuKKE1/kqy78FQcuHQ0+iIWv7C5cSb2inlvs7bNMVY3yRx3RXGZfrvzQ==", - "license": "MIT", - "peerDependencies": { - "three": ">= 0.123.0" - } - }, - "node_modules/three-stdlib": { - "version": "2.36.1", - "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", - "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", - "license": "MIT", - "dependencies": { - "@types/draco3d": "^1.4.0", - "@types/offscreencanvas": "^2019.6.4", - "@types/webxr": "^0.5.2", - "draco3d": "^1.4.1", - "fflate": "^0.6.9", - "potpack": "^1.0.1" - }, - "peerDependencies": { - "three": ">=0.128.0" - } - }, "node_modules/throat": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", @@ -18969,21 +18626,15 @@ "dev": true, "license": "MIT" }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "license": "MIT" - }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -19011,9 +18662,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "peer": true, @@ -19093,36 +18744,6 @@ "node": ">=8" } }, - "node_modules/troika-three-text": { - "version": "0.46.4", - "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.46.4.tgz", - "integrity": "sha512-Qsv0HhUKTZgSmAJs5wvO7YlBoJSP9TGPLmrg+K9pbQq4lseQdcevbno/WI38bwJBZ/qS56hvfqEzY0zUEFzDIw==", - "license": "MIT", - "dependencies": { - "bidi-js": "^1.0.2", - "troika-three-utils": "^0.46.0", - "troika-worker-utils": "^0.46.0", - "webgl-sdf-generator": "1.1.1" - }, - "peerDependencies": { - "three": ">=0.103.0" - } - }, - "node_modules/troika-three-utils": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.46.0.tgz", - "integrity": "sha512-llHyrXAcwzr0bpg80GxsIp73N7FuImm4WCrKDJkAqcAsWmE5pfP9+Qzw+oMWK1P/AdHQ79eOrOl9NjyW4aOw0w==", - "license": "MIT", - "peerDependencies": { - "three": ">=0.103.0" - } - }, - "node_modules/troika-worker-utils": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.46.0.tgz", - "integrity": "sha512-bzOx5f2ZBxkFhXtIvDJlLn2AI3bzCkGVbCndl/2dL5QZrwHEKl45OEIilCxYQQWJG1rEbOD9O80tMjoYjw19OA==", - "license": "MIT" - }, "node_modules/tryer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", @@ -19374,18 +18995,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" }, "engines": { "node": ">= 0.4" @@ -19405,9 +19026,9 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", "peer": true, "bin": { @@ -19415,7 +19036,7 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -19445,9 +19066,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, @@ -19547,9 +19168,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "dev": true, "funding": [ { @@ -19637,15 +19258,6 @@ "dev": true, "license": "MIT" }, - "node_modules/utility-types": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -19656,19 +19268,11 @@ "node": ">= 0.4.0" } }, - "node_modules/utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "license": "MIT", - "dependencies": { - "base64-arraybuffer": "^1.0.2" - } - }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "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).", "dev": true, "license": "MIT", "bin": { @@ -19752,13 +19356,12 @@ } }, "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", "dev": true, "license": "MIT", "dependencies": { - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" }, "engines": { @@ -19775,17 +19378,6 @@ "minimalistic-assert": "^1.0.0" } }, - "node_modules/webgl-constants": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", - "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" - }, - "node_modules/webgl-sdf-generator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", - "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", - "license": "MIT" - }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -19797,38 +19389,32 @@ } }, "node_modules/webpack": { - "version": "5.105.4", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", - "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "version": "5.110.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.110.1.tgz", + "integrity": "sha512-gInQB+jxXxgnZyvPwuzT5NGQmECDqeu85oxcrjinrYHqPoBex0hCAN2SFTJVyPVrK0Pq9E44VFP+e89fAc10/w==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", - "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.20.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.7.0", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.17", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" }, "bin": { "webpack": "bin/webpack.js" @@ -19931,9 +19517,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, "license": "MIT", "engines": { @@ -20009,43 +19595,29 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">= 0.6" } }, "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -20204,14 +19776,14 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", + "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", @@ -20330,9 +19902,9 @@ } }, "node_modules/workbox-build/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "peer": true, @@ -20371,46 +19943,13 @@ "license": "MIT" }, "node_modules/workbox-build/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "deprecated": "The work that was done in this beta branch won't be included in future versions", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "whatwg-url": "^7.0.0" - }, "engines": { - "node": ">= 8" - } - }, - "node_modules/workbox-build/node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/workbox-build/node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/workbox-build/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" + "node": ">= 12" } }, "node_modules/workbox-cacheable-response": { @@ -20611,6 +20150,22 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -20632,9 +20187,9 @@ } }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "dev": true, "license": "MIT", "engines": { @@ -20694,9 +20249,9 @@ } }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", "dev": true, "license": "MIT", "dependencies": { @@ -20753,23 +20308,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zustand": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", - "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", - "license": "MIT", - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } - } } } } diff --git a/gui/src/client/package.json b/gui/src/client/package.json index 157a4f1..fb2ceb6 100644 --- a/gui/src/client/package.json +++ b/gui/src/client/package.json @@ -1,47 +1,57 @@ { "name": "retromol-gui", - "version": "0.1.0", + "version": "1.0.0-dev", "private": true, "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.5", "@mui/x-charts": "^8.17.0", - "@mui/x-data-grid": "^8.18.0", + "@mui/x-data-grid": "^9.12.0", "@react-spring/web": "^10.0.3", - "@react-three/drei": "^8.20.2", - "@react-three/fiber": "^8.13.5", "@tanstack/react-query": "^5.76.1", "@types/react": "18.3.10", "@types/react-dom": "18.3.1", "clsx": "^2.1.1", "dayjs": "^1.11.19", + "dompurify": "^3.2.4", "framer-motion": "^12.6.3", - "html2canvas": "^1.4.1", - "lodash.debounce": "^4.0.8", + "html-to-image": "^1.11.13", "react": "18.3.1", "react-dom": "18.3.1", - "react-force-graph-2d": "^1.27.1", "react-router-dom": "^7.9.5", - "react-window": "^1.8.11", "smiles-drawer": "^2.1.7", - "three": "^0.175.0", - "typescript": "4.9.5", + "typescript": "^5.4.5", "zod": "3.23.8" }, "devDependencies": { "@craco/craco": "^7.1.0", - "@types/lodash.debounce": "^4.0.9", - "@types/react-window": "^1.8.8", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/dompurify": "^3.0.5", "react-scripts": "^5.0.1" }, "scripts": { - "start": "DISABLE_ESLINT_PLUGIN=true craco start", - "build": "DISABLE_ESLINT_PLUGIN=true craco build", - "test": "craco test" + "start": "craco start", + "build": "craco build", + "test": "craco test", + "check:smiles-drawer-version": "node scripts/checkSmilesDrawerVersion.js" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "overrides": { + "react-scripts": { + "typescript": "$typescript" + } }, "browserslist": { "production": [ @@ -56,4 +66,4 @@ ] }, "proxy": "http://localhost:4000" -} \ No newline at end of file +} diff --git a/gui/src/client/scripts/checkSmilesDrawerVersion.js b/gui/src/client/scripts/checkSmilesDrawerVersion.js new file mode 100644 index 0000000..c8680df --- /dev/null +++ b/gui/src/client/scripts/checkSmilesDrawerVersion.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// Fails if DrawingAttribution.tsx's hardcoded SMILES_DRAWER_VERSION drifts from the +// version npm actually resolved in package-lock.json. Needed because smiles-drawer's +// package.json is blocked from import (its own "exports" field), so the "Drawn with +// SmilesDrawer vX" caption can't read the version at runtime the way ReactionScheme +// does for RDKit -- see DrawingAttribution.tsx for that comparison. +const fs = require("fs"); +const path = require("path"); + +const root = path.join(__dirname, ".."); + +const lockfile = JSON.parse(fs.readFileSync(path.join(root, "package-lock.json"), "utf8")); +const resolvedVersion = lockfile.packages?.["node_modules/smiles-drawer"]?.version; + +if (!resolvedVersion) { + console.error("checkSmilesDrawerVersion: could not find node_modules/smiles-drawer in package-lock.json"); + process.exit(1); +} + +const attributionPath = path.join(root, "src/components/DrawingAttribution.tsx"); +const attributionSource = fs.readFileSync(attributionPath, "utf8"); +const match = attributionSource.match(/SMILES_DRAWER_VERSION\s*=\s*"([^"]+)"/); + +if (!match) { + console.error(`checkSmilesDrawerVersion: could not find SMILES_DRAWER_VERSION in ${attributionPath}`); + process.exit(1); +} + +const hardcodedVersion = match[1]; + +if (hardcodedVersion !== resolvedVersion) { + console.error( + `checkSmilesDrawerVersion: DrawingAttribution.tsx says smiles-drawer v${hardcodedVersion}, ` + + `but package-lock.json resolves it to v${resolvedVersion}. ` + + `Update SMILES_DRAWER_VERSION in src/components/DrawingAttribution.tsx to match.` + ); + process.exit(1); +} + +console.log(`checkSmilesDrawerVersion: OK (v${resolvedVersion})`); diff --git a/gui/src/client/src/components/CopyIconButton.tsx b/gui/src/client/src/components/CopyIconButton.tsx new file mode 100644 index 0000000..8aed4e3 --- /dev/null +++ b/gui/src/client/src/components/CopyIconButton.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import Tooltip from "@mui/material/Tooltip"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import { useNotifications } from "./NotificationProvider"; +import { MinimalIconButton } from "./MinimalIconButton"; + +// Copies `text` to the clipboard on click, with a toast for success/failure -- +// same pattern UserIconDropdown's session-id copy uses, pulled out for reuse +// wherever a raw string (a SMILES, a reaction SMARTS, ...) needs a copy affordance. +export function CopyIconButton({ text, label = "value" }: { text: string; label?: string }) { + const { pushNotification } = useNotifications(); + + const handleCopy = async (event: React.MouseEvent) => { + event.stopPropagation(); + try { + await navigator.clipboard.writeText(text); + pushNotification(`Copied ${label} to clipboard`, "success"); + } catch (err) { + pushNotification(`Failed to copy ${label}`, "error"); + } + }; + + return ( + + + + + + ); +} diff --git a/gui/src/client/src/components/DrawingAttribution.tsx b/gui/src/client/src/components/DrawingAttribution.tsx new file mode 100644 index 0000000..ec95f21 --- /dev/null +++ b/gui/src/client/src/components/DrawingAttribution.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import Typography from "@mui/material/Typography"; +import type { SxProps, Theme } from "@mui/material/styles"; + +// smiles-drawer's package.json is blocked from import by its own "exports" field, so +// this is kept in sync by hand with the resolved version in package-lock.json -- +// scripts/checkSmilesDrawerVersion.js fails CI/lint if the two drift apart. +const SMILES_DRAWER_VERSION = "2.4.1"; + +type DrawingAttributionProps = + | { library: "smiles-drawer"; sx?: SxProps } + // RDKit drawings are produced server-side (see gui/src/server/routes/rules.py), so + // there's no local constant to hardcode -- the version is only known once the + // caller has actually fetched a drawing and the server reported what rendered it. + | { library: "rdkit"; version: string; sx?: SxProps }; + +// A single caption attributing one or more structure/reaction drawings above it to the +// library that rendered them. Place once per diagram -- if a diagram contains multiple +// drawings from the same library (e.g. a compound plus its reconstructions), attribute +// the whole group once rather than repeating this per drawing. +export const DrawingAttribution: React.FC = (props) => { + const label = props.library === "smiles-drawer" ? `SmilesDrawer v${SMILES_DRAWER_VERSION}` : `RDKit v${props.version}`; + return ( + + Drawn with {label} + + ); +}; diff --git a/gui/src/client/src/components/ErrorBoundary.tsx b/gui/src/client/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..6b1f526 --- /dev/null +++ b/gui/src/client/src/components/ErrorBoundary.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import Alert from "@mui/material/Alert"; +import AlertTitle from "@mui/material/AlertTitle"; +import Button from "@mui/material/Button"; +import Box from "@mui/material/Box"; + +interface ErrorBoundaryProps { + children: React.ReactNode; + /** Rendered instead of the default alert when a child throws. */ + fallback?: React.ReactNode; + /** Short label for the default fallback, e.g. "molecule structure". */ + what?: string; +} + +interface ErrorBoundaryState { + error: Error | null; +} + +// React error boundaries must be class components — there is no hook equivalent. +// Catches render/lifecycle errors in the subtree (e.g. a third-party drawing +// library choking on a malformed user-uploaded SMILES/GenBank file) so a single +// bad input can't white-screen the whole app. +export class ErrorBoundary extends React.Component { + state: ErrorBoundaryState = { error: null }; + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { error }; + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + console.error("ErrorBoundary caught an error:", error, info.componentStack); + } + + private reset = () => this.setState({ error: null }); + + render() { + if (this.state.error) { + if (this.props.fallback !== undefined) return this.props.fallback; + + return ( + + + Retry + + }> + Something went wrong{this.props.what ? ` rendering the ${this.props.what}` : ""} + {this.state.error.message || "An unexpected error occurred."} + + + ); + } + + return this.props.children; + } +} diff --git a/gui/src/client/src/components/ExportImageButton.tsx b/gui/src/client/src/components/ExportImageButton.tsx new file mode 100644 index 0000000..2b0312d --- /dev/null +++ b/gui/src/client/src/components/ExportImageButton.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import CircularProgress from "@mui/material/CircularProgress"; +import DownloadIcon from "@mui/icons-material/Download"; +import { useNotifications } from "./NotificationProvider"; +import { exportElementAsPng } from "./exportImage"; +import Button from "@mui/material/Button"; + +// Small "export this view as a PNG" affordance. Captures whatever is currently +// rendered inside `targetRef`, including any live highlight state, exactly as +// shown on screen. +export function ExportImageButton({ + targetRef, + filename, + label = "Download this view as a PNG", +}: { + targetRef: React.RefObject; + filename: string; + label?: string; +}) { + const { pushNotification } = useNotifications(); + const [exporting, setExporting] = React.useState(false); + + const handleExport = async () => { + const node = targetRef.current; + if (!node) return; + + setExporting(true); + try { + await exportElementAsPng(node, filename); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + pushNotification(`Failed to export image: ${msg}`, "error"); + } finally { + setExporting(false); + } + }; + + return ( + + ); +} diff --git a/gui/src/client/src/components/Hero.tsx b/gui/src/client/src/components/Hero.tsx index 5295b77..965ec5b 100644 --- a/gui/src/client/src/components/Hero.tsx +++ b/gui/src/client/src/components/Hero.tsx @@ -2,6 +2,7 @@ import Box from "@mui/material/Box"; import Container from "@mui/material/Container"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; +import { ServerUptime } from "./ServerUptime"; export default function Hero() { return ( @@ -74,6 +75,7 @@ export default function Hero() { Perform cross-modal retrieval between natural product compounds and BGCs. + diff --git a/gui/src/client/src/components/HomeAppBar.tsx b/gui/src/client/src/components/HomeAppBar.tsx index 363d593..87fe252 100644 --- a/gui/src/client/src/components/HomeAppBar.tsx +++ b/gui/src/client/src/components/HomeAppBar.tsx @@ -54,7 +54,7 @@ function RetrieveSession() { // Try to retrieve the session from the backend try { await getSession(sessionId); - document.cookie = `sessionId=${sessionId}; path=/; secure; samesite=strict;`; + createCookie("sessionId", sessionId); navigate("/dashboard"); } catch (err) { console.error("error retrieving session:", err); @@ -114,14 +114,6 @@ export default function HomeAppBar() { ); } - // Function to scroll the "retrieve session" section into view - const handleScrollRetrieveSession = () => { - const retrieveSessionSection = document.getElementById("retrieve-session"); - if (retrieveSessionSection) { - retrieveSessionSection.scrollIntoView({ behavior: "smooth" }); - } - } - // Function to handle the opening and closing of the drawer const toggleDrawer = (newOpen: boolean) => () => { setOpen(newOpen); diff --git a/gui/src/client/src/components/MenuContent.tsx b/gui/src/client/src/components/MenuContent.tsx index ddf07a7..76b3c9d 100644 --- a/gui/src/client/src/components/MenuContent.tsx +++ b/gui/src/client/src/components/MenuContent.tsx @@ -6,9 +6,10 @@ import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; import Stack from "@mui/material/Stack"; import ExploreIcon from "@mui/icons-material/Explore"; -import BarChartIcon from "@mui/icons-material/BarChart"; import HomeRoundedIcon from "@mui/icons-material/HomeRounded"; import UploadFileIcon from "@mui/icons-material/UploadFile"; +import RuleIcon from "@mui/icons-material/Rule"; +import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh"; import { useNavigate, useLocation } from "react-router-dom"; const mainListItems = [ @@ -28,9 +29,14 @@ const mainListItems = [ to: `/dashboard/discovery` }, { - text: "Enrichment", - icon: , - to: `/dashboard/enrichment` + text: "Generate", + icon: , + to: `/dashboard/generate` + }, + { + text: "Rules", + icon: , + to: `/dashboard/rules` }, ] diff --git a/gui/src/client/src/components/MinimalIconButton.tsx b/gui/src/client/src/components/MinimalIconButton.tsx new file mode 100644 index 0000000..217ff8c --- /dev/null +++ b/gui/src/client/src/components/MinimalIconButton.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import IconButton, { IconButtonProps } from "@mui/material/IconButton"; + +// A borderless, paddingless IconButton for inline "icon next to text" affordances +// (e.g. a rename pencil sitting right next to a label) where a normal IconButton's +// padding/hover halo would look out of place. Forwards its ref so it still works +// as the direct child of a Tooltip. +export const MinimalIconButton = React.forwardRef( + ({ sx, size = "small", ...props }, ref) => ( + + ) +); + +MinimalIconButton.displayName = "MinimalIconButton"; diff --git a/gui/src/client/src/components/MotifHoverCard.tsx b/gui/src/client/src/components/MotifHoverCard.tsx new file mode 100644 index 0000000..10bdb88 --- /dev/null +++ b/gui/src/client/src/components/MotifHoverCard.tsx @@ -0,0 +1,133 @@ +import React from "react"; +import Box from "@mui/material/Box"; +import CircularProgress from "@mui/material/CircularProgress"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; +import WarningAmberIcon from "@mui/icons-material/WarningAmber"; +import { useQuery } from "@tanstack/react-query"; +import { fetchMotifStructures } from "../features/motifs/api"; +import { MotifName } from "./MotifName"; +import SmilesDrawerContainer from "./SmilesDrawerContainer.js"; +import { DrawingAttribution } from "./DrawingAttribution"; + +const DRAWING_SIZE = 100; + +// Content of the popup -- a separate component (rather than inlined in the +// Tooltip's title) so its useQuery only actually runs once the Tooltip mounts it, +// which MUI's Popper does lazily on open. queryKey is shared across every +// instance on the page, so the name -> SMILES map is fetched once and cached. +function MotifHoverContent({ name, hint }: { name: string; hint?: string }) { + const structuresQuery = useQuery({ + queryKey: ["motifStructures"], + queryFn: ({ signal }) => fetchMotifStructures(signal), + staleTime: Infinity, + gcTime: Infinity, + }); + + const smiles = structuresQuery.data?.structures[name]; + // Some names (e.g. "glycosylation") don't identify one specific rule -- they're + // shared by many rules with different structures, so whichever one `smiles` + // shows was picked arbitrarily (see /api/motifStructures's ambiguousNames). + const isAmbiguous = structuresQuery.data?.ambiguousNames.includes(name) ?? false; + + // Must be unique per *mounted instance*, not derived from `name` alone -- + // SmilesDrawerContainer draws into a DOM node it looks up by this id, and the + // same motif name commonly appears in many cells at once (a whole column of + // an MSA, a run of identical residues). Two same-named hover cards can end up + // mounted at the same moment -- e.g. dragging the pointer straight down a + // repeated column, where the outgoing tooltip's exit transition briefly + // overlaps the incoming one's mount -- and a shared id means the lookup can + // resolve to the wrong (closing) node, leaving the one you're actually + // hovering empty until it flashes into view as the other one unmounts. + const reactId = React.useId(); + + return ( + + {smiles ? ( + <> + + + + ) : ( + + {structuresQuery.isLoading ? ( + + ) : ( + + No structure available + + )} + + )} + {isAmbiguous && ( + + + + Multiple structures share this name: the one shown is picked arbitrarily, + not necessarily the one that actually matched here. + + + )} + + + + {hint && ( + + {hint} + + )} + + ); +} + +// Wraps a motif chip/cell (primary sequence chips, the sequence editor's blocks, +// and the shared pairwise/MSA AlignmentGrid all render a bare motif name) so +// hovering it shows a small structure preview, its name, and optionally whatever +// contextual hint the caller already showed as a plain-text tooltip. +export function MotifHoverCard({ + name, + hint, + children, +}: { + name: string; + hint?: string; + children: React.ReactElement; +}) { + return ( + } + arrow + enterDelay={400} + slotProps={{ + tooltip: { + sx: { + bgcolor: "background.paper", + color: "text.primary", + border: "1px solid", + borderColor: "divider", + boxShadow: 3, + }, + }, + arrow: { + sx: { + color: "background.paper", + "&::before": { + border: "1px solid", + borderColor: "divider", + }, + }, + }, + }} + > + {children} + + ); +} diff --git a/gui/src/client/src/components/MotifName.tsx b/gui/src/client/src/components/MotifName.tsx new file mode 100644 index 0000000..fa5c191 --- /dev/null +++ b/gui/src/client/src/components/MotifName.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import Box from "@mui/material/Box"; +import { isLinkToken } from "../features/reconstruction/types"; + +// Stereo markers mxn.yml appends after "^": R/S (PK alpha/beta carbons), E/Z (PK +// alkene geometry), L/D (NRPS amino acid alpha carbons, e.g. "alanine^L"). +const STEREO_MARKER = /^\^[A-Za-z]+$/; + +// Renders a monomer name like "A2^R" or "C^E2" or "alanine^L", superscripting the +// trailing stereo marker(s). The link token (joining two merged primary sequence +// paths, see LINK_TOKEN) isn't a real building block -- render it as a muted glyph +// instead of a name, everywhere a monomer name would otherwise be rendered (sequence +// chips, alignment grid cells, ...). +export function MotifName({ name }: { name: string }) { + if (isLinkToken(name)) { + return ( + + ⋮ + + ); + } + + const parts = name.split(/(\^[A-Za-z]+)/g); + + return ( + <> + {parts.map((part, i) => { + if (STEREO_MARKER.test(part)) { + return ( + + {part.slice(1)} + + ); + } + + return {part}; + })} + + ); +} diff --git a/gui/src/client/src/components/NotificationDrawer.tsx b/gui/src/client/src/components/NotificationDrawer.tsx index de63f64..f6742ba 100644 --- a/gui/src/client/src/components/NotificationDrawer.tsx +++ b/gui/src/client/src/components/NotificationDrawer.tsx @@ -10,13 +10,13 @@ import ClearAllIcon from "@mui/icons-material/ClearAll"; import { styled, useTheme } from "@mui/material/styles"; import { useNotifications } from "./NotificationProvider"; -// Custom styling for the drawer +// Custom styling for the drawer paper (the visible panel). Deliberately does NOT set +// width/etc. on the Drawer's own root -- that root is the fixed, full-viewport-covering +// container MUI uses internally to compute how far off-screen the panel starts before +// sliding in; shrinking it to the panel's own width corrupts that calculation and makes +// the panel appear to enter from the left instead of the right. const drawerWidth = 400; const Drawer = styled(MuiDrawer)({ - width: drawerWidth, - flexShrink: 0, - boxSizing: "border-box", - mt: 10, [`& .${drawerClasses.paper}`]: { width: drawerWidth, boxSizing: "border-box", diff --git a/gui/src/client/src/components/ScoreBar.tsx b/gui/src/client/src/components/ScoreBar.tsx index 028d7ce..ee4cdc0 100644 --- a/gui/src/client/src/components/ScoreBar.tsx +++ b/gui/src/client/src/components/ScoreBar.tsx @@ -30,7 +30,6 @@ export const ScoreBar: React.FC = ({ { - const t = theme.vars || theme; return { height: height, width: width, diff --git a/gui/src/client/src/components/ServerUptime.tsx b/gui/src/client/src/components/ServerUptime.tsx new file mode 100644 index 0000000..e5eece9 --- /dev/null +++ b/gui/src/client/src/components/ServerUptime.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import Stack from "@mui/material/Stack"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import { useQuery } from "@tanstack/react-query"; +import { getServerStartup } from "../features/server/api"; +import { formatUptime } from "../features/server/utils"; + +// The startup epoch this reads (see /api/startup) is stored in Redis, the +// same store sessions live in -- it only resets when Redis itself was +// restarted/flushed, not on every backend process restart. So a short uptime +// here is a real signal that older sessions may be gone, not just noise. +export function ServerUptime() { + const { data, isLoading, isError } = useQuery({ + queryKey: ["serverStartup"], + queryFn: ({ signal }) => getServerStartup(signal), + refetchInterval: 60_000, + retry: false, + }); + + if (isLoading || isError || !data) return null; + + return ( + + + + + Server up for {formatUptime(data.uptime)} + + + + ); +} diff --git a/gui/src/client/src/components/SmilesDrawerContainer.js b/gui/src/client/src/components/SmilesDrawerContainer.js index 2145961..cfba9e4 100644 --- a/gui/src/client/src/components/SmilesDrawerContainer.js +++ b/gui/src/client/src/components/SmilesDrawerContainer.js @@ -1,12 +1,18 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import SmilesDrawer from 'smiles-drawer'; -import { Box } from '@mui/material'; +import { Box, Typography } from '@mui/material'; import { useColorScheme } from '@mui/material/styles'; class CustomSvgDrawer extends SmilesDrawer.SvgDrawer { - constructor(options) { + constructor(options, showIsotopes = false, orientationTags = null) { super(options); + this.showIsotopes = showIsotopes; + // { startTags, endTags }: isotope tags (see retromol.chem.tagging) of the + // first and last blocks in a generated sequence -- when given, the drawing + // is mirrored horizontally (if needed) so the start block ends up on the + // left, matching reading order of the primary sequence shown alongside it. + this.orientationTags = orientationTags; const themeOverrides = { light: { @@ -75,6 +81,9 @@ class CustomSvgDrawer extends SmilesDrawer.SvgDrawer { for (const vertex of vertices) { vertex.position.x = 2 * centerX - vertex.position.x; } + for (const ring of this.preprocessor?.rings ?? []) { + ring.center.x = 2 * centerX - ring.center.x; + } } // 2) replace Sn with wildcard atom @@ -85,24 +94,51 @@ class CustomSvgDrawer extends SmilesDrawer.SvgDrawer { atom.bracket = null; } + orientSequenceLeftToRight(startTags, endTags) { + const graph = this.preprocessor?.graph; + if (!graph || !startTags?.length || !endTags?.length) return; + + const vertices = graph.vertices.filter((v) => v.position); + if (!vertices.length) return; + + const avgX = (tags) => { + const matched = vertices.filter((v) => v.value?.bracket && tags.includes(v.value.bracket.isotope)); + if (!matched.length) return null; + return matched.reduce((sum, v) => sum + v.position.x, 0) / matched.length; + }; + + const startX = avgX(startTags); + const endX = avgX(endTags); + if (startX === null || endX === null || startX <= endX) return; + + const xs = vertices.map((v) => v.position.x); + const centerX = (Math.min(...xs) + Math.max(...xs)) / 2; + for (const vertex of vertices) { + vertex.position.x = 2 * centerX - vertex.position.x; + } + for (const ring of this.preprocessor?.rings ?? []) { + ring.center.x = 2 * centerX - ring.center.x; + } + } + drawAtomHighlights(highlights) { this.prepareTinAsRightWildcard('Sn'); + if (this.orientationTags) { + this.orientSequenceLeftToRight(this.orientationTags.startTags, this.orientationTags.endTags); + } let preprocessor = this.preprocessor; - let opts = preprocessor.opts; let graph = preprocessor.graph; - let rings = preprocessor.rings; - let svgWrapper = this.svgWrapper; // highlighted atom ids const highlightedAtomIds = []; const atomIdToHighlight = {}; - for (var i = 0; i < graph.vertices.length; i++) { + for (let i = 0; i < graph.vertices.length; i++) { let vertex = graph.vertices[i]; let atom = vertex.value; - for (var j = 0; j < preprocessor.highlight_atoms.length; j++) { + for (let j = 0; j < preprocessor.highlight_atoms.length; j++) { let highlight = preprocessor.highlight_atoms[j] // if atom.bracket !== null, then it is a bracket atom, and we continue @@ -117,7 +153,7 @@ class CustomSvgDrawer extends SmilesDrawer.SvgDrawer { }; // loop over edges - for (var i = 0; i < graph.edges.length; i++) { + for (let i = 0; i < graph.edges.length; i++) { let edge = graph.edges[i]; // if edge.sourceId and edge.targetId in highlightedAtomIds, then draw bond highlight, they also need to have same highlight color if (highlightedAtomIds.includes(edge.sourceId) && highlightedAtomIds.includes(edge.targetId)) { @@ -129,11 +165,15 @@ class CustomSvgDrawer extends SmilesDrawer.SvgDrawer { }; }; - // loop over all atoms and set atom.bracket to null - for (var i = 0; i < graph.vertices.length; i++) { + // loop over all atoms and, unless isotopes were asked to be kept (used to show + // R-group numbers, e.g. [1*]/[2*], in the rules browser and motif hover card), + // clear atom.bracket so isotope/charge annotations don't clutter the drawing. + for (let i = 0; i < graph.vertices.length; i++) { let vertex = graph.vertices[i]; let atom = vertex.value; - atom.bracket = null; + if (!this.showIsotopes) { + atom.bracket = null; + } // make sure COOH is drawn fully instead of displayed with text if (atom.element === 'C') { @@ -152,7 +192,7 @@ class CustomSvgDrawer extends SmilesDrawer.SvgDrawer { }; // loop over all bonds - for (var i = 0; i < graph.edges.length; i++) { + for (let i = 0; i < graph.edges.length; i++) { let edge = graph.edges[i]; // if aromatic bond if (edge.isPartOfAromaticRing) { @@ -173,29 +213,74 @@ class CustomSvgDrawer extends SmilesDrawer.SvgDrawer { * @property {number} size width & height of the drawing * @property {HighlightAtom[]} [highlightAtoms] array of `[atomNumber, color]` * @property {string} [themeOverride] force “light” or “dark” drawing theme + * @property {boolean} [showIsotopes] draw isotope numbers (e.g. R-group + * tags like [1*]/[2*]) instead of hiding them + * @property {{startTags: number[], endTags: number[]}} [orientationTags] + * isotope tags of the first/last blocks in a + * generated sequence -- mirrors the drawing + * horizontally (if needed) so the start block + * ends up on the left */ /** * @param {Props} props */ -const SmilesDrawerContainer = ({ identifier, smiles, size, highlightAtoms = [], themeOverride = '' }) => { - // create a new drawer instance - let drawer = new CustomSvgDrawer({ width: size, height: size }); - - const { mode, systemMode, setMode } = useColorScheme(); +const SmilesDrawerContainer = ({ identifier, smiles, size, highlightAtoms = [], themeOverride = '', showIsotopes = false, orientationTags = null }) => { + const { mode, systemMode } = useColorScheme(); + const [error, setError] = useState(null); // draw the molecule when the component is mounted useEffect(() => { + setError(null); + let target = `structure-svg-${identifier}` let themeName = themeOverride !== '' ? themeOverride : (systemMode !== undefined) ? systemMode : mode; let weights = null; let infoOnly = false; let weightsNormalized = false; - SmilesDrawer.parse(smiles, function (tree) { - drawer.draw(tree, target, themeName, weights, infoOnly, highlightAtoms, weightsNormalized); - }); - }, [smiles, highlightAtoms, size]); + // A fresh drawer instance per draw call, since it isn't safe to reuse + // once it holds a graph for a previous (possibly differently-sized) SMILES. + // + // padding is bumped above the library default (10) because that default + // only leaves room for atom labels -- it doesn't account for the extra + // radius a highlight circle (customDrawAtomHighlight, r = bondLength / 3) + // draws around a highlighted atom, so a highlighted atom sitting at the + // very edge of the layout (as the first/last block often does once + // orientationTags pins it there) gets its highlight clipped by the SVG's + // own viewBox. + let drawer = new CustomSvgDrawer({ width: size, height: size, padding: 24 }, showIsotopes, orientationTags); + + try { + SmilesDrawer.parse( + smiles, + function (tree) { + try { + drawer.draw(tree, target, themeName, weights, infoOnly, highlightAtoms, weightsNormalized); + } catch (drawErr) { + console.error('SmilesDrawerContainer: draw failed', drawErr); + setError('Could not render this structure.'); + } + }, + function (parseErr) { + console.error('SmilesDrawerContainer: parse failed', parseErr); + setError('Could not parse this SMILES.'); + } + ); + } catch (err) { + // Some malformed input can throw synchronously instead of hitting the error callback + console.error('SmilesDrawerContainer: unexpected error', err); + setError('Could not render this structure.'); + } + }, [identifier, smiles, highlightAtoms, size, themeOverride, showIsotopes, orientationTags, mode, systemMode]); + + if (error) { + return ( + + {error} + + ); + } return ( diff --git a/gui/src/client/src/components/SvgViewer.test.tsx b/gui/src/client/src/components/SvgViewer.test.tsx new file mode 100644 index 0000000..dd460ff --- /dev/null +++ b/gui/src/client/src/components/SvgViewer.test.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { SvgViewer } from "./SvgViewer"; + +describe("SvgViewer", () => { + it("strips '; + + const { container } = render(); + + // This asserts the *absence* of dangerous DOM nodes/attributes, which + // has no accessible-role equivalent to query by — direct node access is + // the correct tool here, not a shortcut around Testing Library. + /* eslint-disable testing-library/no-container, testing-library/no-node-access */ + expect(container.querySelector("script")).toBeNull(); + expect(container.querySelector("svg")?.hasAttribute("onload")).toBe(false); + // legitimate markup still survives sanitization + expect(container.querySelector("circle")).not.toBeNull(); + /* eslint-enable testing-library/no-container, testing-library/no-node-access */ + }); +}); diff --git a/gui/src/client/src/components/SvgViewer.tsx b/gui/src/client/src/components/SvgViewer.tsx index 10528dc..7da9dc4 100644 --- a/gui/src/client/src/components/SvgViewer.tsx +++ b/gui/src/client/src/components/SvgViewer.tsx @@ -1,4 +1,5 @@ import React from "react"; +import DOMPurify from "dompurify"; import Box from "@mui/material/Box"; import Paper from "@mui/material/Paper"; import Slider from "@mui/material/Slider"; @@ -45,17 +46,25 @@ export const SvgViewer: React.FC = ({ const containerRef = React.useRef(null); - const handleFit = () => { + // Server-provided SVG markup is untrusted input (derived from user-uploaded + // compounds/gene clusters) and gets injected raw via dangerouslySetInnerHTML, + // so it must be sanitized first to prevent stored/reflected XSS. + const sanitizedSvg = React.useMemo( + () => DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } }), + [svg] + ); + + const handleFit = React.useCallback(() => { // naive fit: reset zoom and pan setZoom(initialZoom); setPan({ x: 0, y: 0 }); setPanOrigin({ x: 0, y: 0 }); - } + }, [initialZoom]); // Reset zoom and pan whenever SVG changes React.useEffect(() => { handleFit(); - }, [svg, initialZoom]); + }, [svg, handleFit]); // Notify parent about zoom changes React.useEffect(() => { @@ -114,7 +123,9 @@ export const SvgViewer: React.FC = ({ } const handleDownload = () => { - const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" }); + // Download the sanitized markup too — a raw SVG opened directly in a + // browser tab can still execute embedded