diff --git a/.github/workflows/test-frontend.yaml b/.github/workflows/test-frontend.yaml new file mode 100644 index 0000000..a244616 --- /dev/null +++ b/.github/workflows/test-frontend.yaml @@ -0,0 +1,85 @@ +name: Test frontend + +on: + pull_request: + +defaults: + run: + working-directory: frontend + +jobs: + test-build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install packages + run: bun install + + - name: SSH debug + if: runner.debug == '1' + uses: mxschmitt/action-tmate@v3 + + - name: Run test + run: bun run build + + test-types: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install packages + run: bun install + + - name: SSH debug + if: runner.debug == '1' + uses: mxschmitt/action-tmate@v3 + + - name: Run test + run: bun run test:types + + test-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install packages + run: bun install + + - name: SSH debug + if: runner.debug == '1' + uses: mxschmitt/action-tmate@v3 + + - name: Run test + run: bun run test:lint + + test-format: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install packages + run: bun install + + - name: SSH debug + if: runner.debug == '1' + uses: mxschmitt/action-tmate@v3 + + - name: Run test + run: bun run test:format diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..1ac2203 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2206, The Krishnan Lab at CU Denver Anschutz Medical Campus. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/backend/launch_api.sh b/backend/launch_api.sh index 28c5516..88f2490 100755 --- a/backend/launch_api.sh +++ b/backend/launch_api.sh @@ -24,5 +24,5 @@ if [ "$DJANGO_DEBUG" = "1" ] ; then ./manage.py runserver 0.0.0.0:8000 else echo "* Serving via gunicorn (production mode)" - gunicorn meta2onto.wsgi:application --bind 0.0.0.0:8000 --workers 3 + gunicorn meta2onto.wsgi:application --bind 0.0.0.0:8000 --workers ${WEB_WORKERS:-4} --timeout ${WORKER_TIMEOUT:-60} fi diff --git a/backend/src/api/management/commands/compute_global_facets.py b/backend/src/api/management/commands/compute_global_facets.py new file mode 100644 index 0000000..8f724ba --- /dev/null +++ b/backend/src/api/management/commands/compute_global_facets.py @@ -0,0 +1,206 @@ +""" +Populates the Facet model using the currenty-loaded GEOSeries, GEOPlatforms, +and GEOSamples models. These facets are returned by the study search endpoint +instead of being dynamically computed at query time. + +See the Facet model for more details on the structure of the facet data and +the rationale for precomputing it. +""" + +from django.core.management.base import BaseCommand + +from django.db import models, connection, transaction + +from api.models import Facet, FacetEntry, GEOSeries, GEOPlatform, GEOSeriesToGEOPlatforms, GEOSample +from api.utils.timing import timed + +def populate_study_size(): + # find the minimum and maximum number of samples + # associated with any series in the database + min_size = ( + GEOSeries.objects.annotate(sample_count=models.Count("samples")) + .aggregate(min_size=models.Min("sample_count")) + .get("min_size", 0) + ) + max_size = ( + GEOSeries.objects.annotate(sample_count=models.Count("samples")) + .aggregate(max_size=models.Max("sample_count")) + .get("max_size", 0) + ) + + Facet.objects.create( + name="Study Size", + min=min_size, + max=max_size + ) + +def populate_confidence(): + # hardcoded to 0-100, but we might change it to the min/max confidence + # over the dataset at some point + Facet.objects.create( + name="Confidence", + min=0, + max=100 + ) + +def populate_platforms(): + # populates the Plaforms facet with each unique platform + # and the number of studies overall associated with that platform + facet = Facet.objects.create( + name="Platforms" + ) + + qs = GEOPlatform.objects.raw(""" + SELECT + PL.gpl, + SUM(array_length(GEPL.platforms, 1)) AS total + FROM api_geoplatform AS PL + INNER JOIN api_geoseriestogeoplatforms GEPL + ON PL.gpl = ANY(GEPL.platforms) + GROUP BY PL.gpl + """) + + for platform in qs: + FacetEntry.objects.create( + facet=facet, + name=platform.gpl, + count=platform.total + ) + +def populate_technologies(): + """ + Populates the Technologies facet with each unique technology and the number + of studies overall associated with that technology. + + Note that we can't query via GEOPlatform as we do in populate_platforms(), + since we're not returning GEOPlatform primary keys. Instead we opt for a + regular raw query. + """ + with transaction.atomic(): + facet = Facet.objects.create(name="Technologies") + + with connection.cursor() as cursor: + cursor.execute(""" + SELECT + PL.technology, + SUM(array_length(GEPL.platforms, 1)) AS total + FROM api_geoplatform AS PL + INNER JOIN api_geoseriestogeoplatforms GEPL + ON PL.gpl = ANY(GEPL.platforms) + GROUP BY PL.technology + """) + + rows = cursor.fetchall() + + FacetEntry.objects.bulk_create([ + FacetEntry( + facet=facet, + name=technology, + count=total, + ) + for technology, total in rows + ]) + +def populate_databases(): + """ + Populates the Databases facet with each unique database and the number of + studies overall associated with that database. + + Note that we can't query via GEOPlatform as we do in populate_platforms(), + since we're not returning GEOPlatform primary keys. Instead we opt for a + regular raw query. + """ + with transaction.atomic(): + facet = Facet.objects.create(name="Databases") + + with connection.cursor() as cursor: + cursor.execute(""" + SELECT + database, + COUNT(DISTINCT series_id) AS total + FROM ( + SELECT + database, + series_id + FROM api_externaldbrefs + WHERE database IS NOT NULL AND database <> '' + + UNION + + SELECT + database, + series_id + FROM api_geoseriesdatabase + WHERE database IS NOT NULL AND database <> '' + ) AS combined + GROUP BY database + """) + + rows = cursor.fetchall() + + FacetEntry.objects.bulk_create([ + FacetEntry( + facet=facet, + name=database, + count=total, + ) + for database, total in rows + ]) + +def populate_organisms(): + """ + Populates the Organisms facet with each unique organism and the number of + studies overall associated with that organism. + """ + with transaction.atomic(): + facet = Facet.objects.create(name="Organisms") + + with connection.cursor() as cursor: + cursor.execute(""" + SELECT organism, COUNT(*) AS total + FROM api_geoseries_organism + GROUP BY organism + """) + + rows = cursor.fetchall() + + FacetEntry.objects.bulk_create([ + FacetEntry( + facet=facet, + name=organism, + count=total, + ) + for organism, total in rows + ]) + +# definitions of which facets we want to compute +# - the key in this dict is the name of the facet, the type +# (minmax or categorical) determines which attributes of the Facet +# model are populated. +# - 'method' is a function that computes the facet values and yields dicts for +# each facet value +FACETS = [ + ("Study Size", "minmax", populate_study_size), + ("Confidence", "minmax", populate_confidence), + ("Platforms", "categorical", populate_platforms), + ("Technologies", "categorical", populate_technologies), + ("Databases", "categorical", populate_databases), + ("Organisms", "categorical", populate_organisms), +] + +class Command(BaseCommand): + help = "Compute Facet model contents based on GEOSeries, Samples, and Platforms models." + + def add_arguments(self, parser): + pass + + def handle(self, *args, **opts): + # drop all the entries in the Facet table before recomputing + # (this implicitly drops all the associated FacetEntry rows via a + # cascade delete) + Facet.objects.all().delete() + + for facet_name, facet_type, method in FACETS: + with timed(label="- Elapsed", print_method=self.stdout.write): + self.stdout.write(f"Computing facet: {facet_name} ({facet_type})") + method() diff --git a/backend/src/api/management/commands/import_external_db_refs.py b/backend/src/api/management/commands/import_external_db_refs.py new file mode 100644 index 0000000..aceecfa --- /dev/null +++ b/backend/src/api/management/commands/import_external_db_refs.py @@ -0,0 +1,173 @@ +""" +Populates the model ExternalDbRefs with relations from GEO series to external dbs, using the following files: +data/expression_db_references/archs4_*.(txt|parquet) +data/expression_db_references/recount3_*.(txt|parquet) +data/expression_db_references/refinebio_*.(txt|parquet) +""" + +import math +from pathlib import Path + +from tqdm import tqdm + +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction, connection + +import pyarrow.parquet as pq + +from api.models import ExternalDbRefs + +# ============================================================================ +# === Utilities +# ============================================================================ + + +def _total_batches(pf: pq.ParquetFile, batch_size: int) -> int: + total_rows = sum( + pf.metadata.row_group(i).num_rows for i in range(pf.num_row_groups) + ) + return math.ceil(total_rows / batch_size) + + +# ============================================================================ +# === Importers +# ============================================================================ + +# size of batches to process at a time +DEFAULT_BATCH_SIZE = 5000 + + +# @transaction.atomic +def import_external_db_refs(db_name: str, path: Path, batch_size: int = DEFAULT_BATCH_SIZE, gse_col: str = "geo"): + """ + *_predictions.parquet files have the following columns: + term, ID (), confidence, related_words + """ + + inserted = 0 + + # check if path ends with .txt or .parquet, and read accordingly + if path.suffix == ".txt": + # the archs4 file, provided as text, appears to consist of + # one or more GSE IDs, comma-delimited, per line. presumably + # these are all in archs4, so we just collect them into a set and + # insert a relation to archs4 per GSE ID + with open(path, "r") as f: + gse_ids = set() + + for line in f: + gse_ids.update(line.strip().split(",")) + + print(f"Processed {len(gse_ids)} unique GSE IDs", end="\r") + + inserted += len(ExternalDbRefs.objects.bulk_create( + [ + ExternalDbRefs( + series_id=gse_id, + database=db_name, + external_id=None + ) + for gse_id in gse_ids + ], + ignore_conflicts=True, + )) + + elif path.suffix == ".parquet": + # the parquet files consist of rows with columns (geo, accession), where + # geo is the GSE ID and accession is the external db ID. we insert a + # relation for each row. + pf = pq.ParquetFile(path) + + for batch in tqdm( + pf.iter_batches(batch_size=batch_size), + total=_total_batches(pf, batch_size), + desc=f"Importing {db_name} into ExternalDbRefs", + ): + with transaction.atomic(): + rows = batch.to_pylist() + + # presume the GEOSeries we're linking to exists, and if it doesn't skip insertion + + # now, bulk create ExternalDbRefs entries + inserted += len(ExternalDbRefs.objects.bulk_create( + [ + ExternalDbRefs( + series_id=row[gse_col], + database=db_name, + external_id=row.get("accession", None), + ) + for row in rows + ], + ignore_conflicts=True, + )) + + return inserted + +# ============================================================================ +# === entrypoint +# ============================================================================ + + +class Command(BaseCommand): + help = "Import external database references (e.g., archs4, recount3, refine.bio) into ExternalDbRefs." + + def add_arguments(self, parser): + parser.add_argument( + "--root", + required=True, + help="Directory containing the external db reference parquet/txt files", + ) + + # add an argument to delete all existing data before import? + parser.add_argument( + "--clear-existing", + action="store_true", + help="Clear existing search data before import.", + ) + + def handle(self, *args, **opts): + root = Path(opts["root"]).expanduser().resolve() + if not root.exists(): + raise CommandError(f"Root folder not found: {root}") + + self.stdout.write(self.style.MIGRATE_HEADING("Starting ExternalDbRefs import")) + + if opts["clear_existing"]: + with transaction.atomic(): + self.stdout.write(self.style.WARNING("Clearing existing ExternalDbRefs data...")) + + models_to_clear = ( + ExternalDbRefs, + ) + + with connection.cursor() as cursor: + cursor.execute( + "TRUNCATE TABLE {} RESTART IDENTITY CASCADE;".format( + ", ".join(f'"{t._meta.db_table}"' for t in models_to_clear) + ) + ) + + for external_db in ('archs4', 'recount3', 'refinebio'): + # find a file under root that matches _*.txt or _*.parquet + matching_files = list(root.glob(f"{external_db}_*.txt")) + list(root.glob(f"{external_db}_*.parquet")) + + # map 'refinebio' to the canonical 'refine.bio', even though the filename doesn't include it + db_name = 'refine.bio' if external_db == 'refinebio' else external_db + # map the gse_col based on the external_db; for archs4 and recount3 it's "geo", for refinebio it's "gse" + gse_col = "geo" if external_db in ('archs4', 'recount3') else "gse" + + if not matching_files: + self.stdout.write(self.style.WARNING(f"No file found for {external_db}, skipping...")) + continue + elif len(matching_files) > 1: + self.stdout.write(self.style.WARNING(f"Multiple files found for {external_db}, using the first one: {matching_files[0]}")) + + file_to_import = matching_files[0] + + # import that file; import_external_db_refs will determine if it's parquet or txt based on the extension and import it appropriately + self.stdout.write(self.style.HTTP_INFO(f"Importing: {file_to_import}")) + inserted = import_external_db_refs(db_name=db_name, path=file_to_import, batch_size=500, gse_col=gse_col) + self.stdout.write(self.style.SUCCESS(f"✓ {inserted} {db_name} reference(s) imported")) + + + self.stdout.write(self.style.MIGRATE_HEADING("Import complete")) diff --git a/backend/src/api/management/commands/import_geo_parquet.py b/backend/src/api/management/commands/import_geo_parquet.py index b0da804..d5b14d1 100644 --- a/backend/src/api/management/commands/import_geo_parquet.py +++ b/backend/src/api/management/commands/import_geo_parquet.py @@ -181,7 +181,7 @@ def import_ids_level_sample(path: Path, batch_size: int = DEFAULT_BATCH_SIZE): # check if the GEOSeries exists; create if not if row["series"] not in series_set: series_set.add(row["series"]) - GEOSeries.objects.get_or_create(series_id=row["series"]) + GEOSeries.objects.get_or_create(gse=row["series"]) OrganismForPairing.objects.bulk_create( [ diff --git a/backend/src/api/management/commands/import_search_parquet.py b/backend/src/api/management/commands/import_search_parquet.py index 6fe903b..0a17402 100644 --- a/backend/src/api/management/commands/import_search_parquet.py +++ b/backend/src/api/management/commands/import_search_parquet.py @@ -12,7 +12,7 @@ import pyarrow.parquet as pq -from api.models import SearchTerm, GEOSeries, OntologyTermRating +from api.models import SearchTerm, GEOSeries, OntologyTermRating, PositiveStudyAnnotation # ============================================================================ # === Utilities @@ -106,6 +106,37 @@ def import_eval_terms(path: Path, batch_size: int = DEFAULT_BATCH_SIZE): return inserted +def import_positive_annotations(path: Path, batch_size: int = DEFAULT_BATCH_SIZE): + """ + positive_study_annotations.parquet files have the following columns: + term, ID (GSE) + """ + + pf = pq.ParquetFile(path) + inserted = 0 + + for batch in tqdm( + pf.iter_batches(batch_size=batch_size), + total=_total_batches(pf, batch_size), + desc="Importing Positive Study Annotations", + ): + with transaction.atomic(): + rows = batch.to_pylist() + + # now, bulk create PositiveStudyAnnotation entries + inserted += len(PositiveStudyAnnotation.objects.bulk_create( + [ + PositiveStudyAnnotation( + term=row["term"], + series_id=row["ID"], + ) + for row in rows + ], + ignore_conflicts=True, + )) + + return inserted + # ============================================================================ # === entrypoint # ============================================================================ @@ -146,6 +177,11 @@ def add_arguments(self, parser): action="store_true", help="Skip importing eval terms.", ) + parser.add_argument( + "--skip-positive-annotations", + action="store_true", + help="Skip importing positive study annotations.", + ) # add an argument to delete all existing data before import? parser.add_argument( @@ -171,6 +207,7 @@ def handle(self, *args, **opts): models_to_clear = ( SearchTerm, OntologyTermRating, + PositiveStudyAnnotation, ) with connection.cursor() as cursor: @@ -208,4 +245,16 @@ def handle(self, *args, **opts): else: self.stdout.write(self.style.WARNING("Skipping eval terms import")) + # import positive study annotations if provided + if not opts["skip_positive_annotations"]: + positive_annotations_path = root / "positive_study_annotations.parquet" + if positive_annotations_path.exists(): + self.stdout.write(self.style.HTTP_INFO(f"Importing: {positive_annotations_path}")) + positive_annotations_inserted = import_positive_annotations(positive_annotations_path, batch_size=500) + self.stdout.write(self.style.SUCCESS(f"✓ {positive_annotations_inserted} positive study annotation(s) imported")) + else: + self.stdout.write(self.style.WARNING(f"Positive study annotations file not found, skipping: {positive_annotations_path}")) + else: + self.stdout.write(self.style.WARNING("Skipping positive study annotations import")) + self.stdout.write(self.style.MIGRATE_HEADING("Import complete")) diff --git a/backend/src/api/management/commands/import_series_databases.py b/backend/src/api/management/commands/import_series_databases.py index bf60b24..26ad4a0 100644 --- a/backend/src/api/management/commands/import_series_databases.py +++ b/backend/src/api/management/commands/import_series_databases.py @@ -1,43 +1,23 @@ """ -Import GEOSeriesDatabase Parquet dumps into normalized Django models. +Import GEOSeriesDatabase Parquet dump into normalized Django models. -Expected input files (Parquet format): - - ids__level-sample.parquet +Expected input file (Parquet format): - ids__level-series.parquet - - corpus__level-sample.parquet - - corpus__level-series.parquet These should all be under the 'root' data folder specified as the one required CLI argument. - -There are three main entities represented: -- GEOSample (GSM) -- GEOSeries (GSE) -- GEOPlatform (GPL) - -The "ids__level-*.parquet" files contain structured metadata and relationships -between these entities, while the "corpus__level-*.parquet" files contain -textual documents associated with each GEOSample or GEOSeries for indexing/search. - -GEOPlatforms and Organisms are created as needed. GEOSeries-GEOPlatform relationships, -GEOSample-GEOSeries memberships, GEOSeries-GEOSeries relations, and external links are also -created based on the data in the Parquet files. """ -import json import math -import re from pathlib import Path -from typing import Iterable, Tuple, Optional +from typing import Iterable, Optional from tqdm import tqdm from django.core.management.base import BaseCommand, CommandError from django.db import transaction, connection -import pyarrow as pa import pyarrow.parquet as pq -from django.utils.dateparse import parse_datetime from api.models import GEOSeriesDatabase @@ -79,114 +59,6 @@ def _total_batches(pf: pq.ParquetFile, batch_size: int) -> int: DEFAULT_BATCH_SIZE = 5000 -@transaction.atomic -def import_corpus_level_sample(path: Path, batch_size: int = DEFAULT_BATCH_SIZE): - """ - corpus__level-sample.parquet - columns: index (sample_id), docs - """ - - pf = pq.ParquetFile(path) - - for batch in tqdm( - pf.iter_batches(batch_size=batch_size), - total=_total_batches(pf, batch_size), - desc="Importing GEOSample", - ): - rows = batch.to_pylist() - GEOSample.objects.bulk_create( - [GEOSample(sample_id=row["index"], doc=row["doc"]) for row in rows], - ignore_conflicts=True, - ) - - -@transaction.atomic -def import_corpus_level_series(path: Path, batch_size: int = DEFAULT_BATCH_SIZE): - """ - corpus__level-series.parquet - columns: index (series_id), docs - """ - - pf = pq.ParquetFile(path) - - for batch in tqdm( - pf.iter_batches(batch_size=batch_size), - total=_total_batches(pf, batch_size), - desc="Importing GEOSeries", - ): - rows = batch.to_pylist() - GEOSeries.objects.bulk_create( - [GEOSeries(series_id=row["index"], doc=row["doc"]) for row in rows], - ignore_conflicts=True, - ) - - -def import_ids_level_sample(path: Path, batch_size: int = DEFAULT_BATCH_SIZE): - """ - ids__level-sample.parquet - columns: sample, series, platform, organism, status - - Column details: - - "sample": GSM ID - - "series": GSE ID - - "platform": GPL ID - - "organism": organism name - - "status": e.g., 'Public on YYYY-MM-DD' - """ - - pf = pq.ParquetFile(path) - - # store refs to already-created organisms to avoid redundant queries - organism_cache = {} - # store IDs of already-created platforms to avoid redundant queries - platform_set = set() - sample_set = set() - series_set = set() - - for batch in tqdm( - pf.iter_batches(batch_size=batch_size), - total=_total_batches(pf, batch_size), - desc="Importing OrganismForPairing", - ): - with transaction.atomic(): - rows = batch.to_pylist() - - for row in rows: - # check if the organism exists; create if not and cache the model instance - if row["organism"] not in organism_cache: - organism, _ = Organism.objects.get_or_create(name=row["organism"]) - organism_cache[row["organism"]] = organism - - # check if the platform exists; create if not - if row["platform"] not in platform_set: - platform_set.add(row["platform"]) - # use get_or_create rather than create just in case it's already there - GEOPlatform.objects.get_or_create(platform_id=row["platform"]) - - # check if the sample exists; create if not - if row["sample"] not in sample_set: - sample_set.add(row["sample"]) - GEOSample.objects.get_or_create(sample_id=row["sample"]) - - # check if the GEOSeries exists; create if not - if row["series"] not in series_set: - series_set.add(row["series"]) - GEOSeries.objects.get_or_create(series_id=row["series"]) - - OrganismForPairing.objects.bulk_create( - [ - OrganismForPairing( - sample_id=row["sample"], - series_id=row["series"], - platform_id=row["platform"], - organism=organism_cache[row["organism"]], - ) - for row in rows - ], - ignore_conflicts=True, - ) - - def import_series_databases(path: Path, batch_size: int = DEFAULT_BATCH_SIZE): """ ids__level-series.parquet @@ -239,9 +111,39 @@ def import_series_databases(path: Path, batch_size: int = DEFAULT_BATCH_SIZE): if rel_type in db_relations: # create GEOSeriesRelations entry GEOSeriesDatabase.objects.get_or_create( - series_id=series_id, database_name=rel_type, url=rel_value + series_id=series_id, database=rel_type, url=rel_value ) +def import_geoseries_bioprojects(path: Path, batch_size: int = DEFAULT_BATCH_SIZE): + """ + geo_series.parquet + columns: accession, bioprojects, ... + + Column details: + - "accession" is the series ID (GSE). + - "bioprojects" is a list of BioProject IDs associated with the series + """ + + pf = pq.ParquetFile(path) + + for batch in tqdm( + pf.iter_batches(batch_size=batch_size), + total=_total_batches(pf, batch_size), + desc="Importing BioProject GEOSeriesRelations from geo_series.parquet", + ): + with transaction.atomic(): + rows = batch.to_pylist() + + for row in rows: + relations = row["bioprojects"] if row["bioprojects"] is not None else [] + + for rel in relations: + # create GEOSeriesRelations entry + GEOSeriesDatabase.objects.get_or_create( + series_id=row["accession"], + database="BioProject", + url=f" https://www.ncbi.nlm.nih.gov/bioproject/{rel}" + ) # ============================================================================ # === entrypoint @@ -258,6 +160,7 @@ def add_arguments(self, parser): help="Directory containing the ids__level-series.parquet file.", ) parser.add_argument("--ids-series", default="ids__level-series.parquet") + parser.add_argument("--geo-series", default="geo_series.parquet") # add an argument to delete all existing data before import? parser.add_argument( @@ -302,4 +205,9 @@ def handle(self, *args, **opts): import_series_databases(ids_series, batch_size=50) self.stdout.write(self.style.SUCCESS("✓ ids__level-series imported")) + geo_series = root / opts["geo_series"] + self.stdout.write(self.style.HTTP_INFO(f"Importing: {geo_series}")) + import_geoseries_bioprojects(geo_series, batch_size=50) + self.stdout.write(self.style.SUCCESS("✓ geo_series imported")) + self.stdout.write(self.style.MIGRATE_HEADING("Import complete")) diff --git a/backend/src/api/management/commands/normalize_database_names.py b/backend/src/api/management/commands/normalize_database_names.py new file mode 100644 index 0000000..bfb6cbd --- /dev/null +++ b/backend/src/api/management/commands/normalize_database_names.py @@ -0,0 +1,48 @@ +from django.core.management.base import BaseCommand +from django.db import connection, transaction + + +class Command(BaseCommand): + help = "Normalize database names to this set: Refine.bio, ARCHS4, Recount3, GEO, SRA, BioStudies, BioProject, ArrayExpress, Peptidome" + + def handle(self, *args, **options): + with transaction.atomic(), connection.cursor() as cursor: + for table in ('api_geoseriesdatabase', 'api_externaldbrefs'): + # Update the database names in the studies table + self.stdout.write(f"Normalizing database names in table: {table}") + cursor.execute(f""" + UPDATE {table} + SET database = CASE + WHEN database ILIKE 'refine.bio' THEN 'Refine.bio' + WHEN database ILIKE 'archs4' THEN 'ARCHS4' + WHEN database ILIKE 'recount3' THEN 'Recount3' + WHEN database ILIKE 'geo' THEN 'GEO' + WHEN database ILIKE 'sra' THEN 'SRA' + WHEN database ILIKE 'biostudies' THEN 'BioStudies' + WHEN database ILIKE 'bioproject' THEN 'BioProject' + WHEN database ILIKE 'arrayexpress' THEN 'ArrayExpress' + WHEN database ILIKE 'peptidome' THEN 'Peptidome' + ELSE database + END + """) + + # update facet databases entries as well + with transaction.atomic(), connection.cursor() as cursor: + self.stdout.write("Normalizing database facet names in table: api_facetentry") + cursor.execute(""" + UPDATE api_facetentry + SET name = CASE + WHEN name ILIKE 'refine.bio' THEN 'Refine.bio' + WHEN name ILIKE 'archs4' THEN 'ARCHS4' + WHEN name ILIKE 'recount3' THEN 'Recount3' + WHEN name ILIKE 'geo' THEN 'GEO' + WHEN name ILIKE 'sra' THEN 'SRA' + WHEN name ILIKE 'biostudies' THEN 'BioStudies' + WHEN name ILIKE 'bioproject' THEN 'BioProject' + WHEN name ILIKE 'arrayexpress' THEN 'ArrayExpress' + WHEN name ILIKE 'peptidome' THEN 'Peptidome' + ELSE name + END + """) + + self.stdout.write(self.style.SUCCESS("Database names normalized successfully.")) diff --git a/backend/src/api/management/commands/populate_site_statistics.py b/backend/src/api/management/commands/populate_site_statistics.py new file mode 100644 index 0000000..96c5558 --- /dev/null +++ b/backend/src/api/management/commands/populate_site_statistics.py @@ -0,0 +1,49 @@ +from pprint import pprint + +from django.db.models import ( + OuterRef, + F, + Exists, +) + +from django.core.management.base import BaseCommand + +from api.utils.query import ArrayAnyEquals + +from api.models import GEOPlatform, GEOSample, SearchTerm, SiteStatistic +from api.utils.timing import timed + +class Command(BaseCommand): + help = "Populate the SiteStatistic table with precomputed stats for the database." + + def handle(self, *args, **options): + with timed("Populating SiteStatistic table"): + search_term_series = SearchTerm.objects.filter( + ArrayAnyEquals( + F("series_id"), + OuterRef("series_set"), + ) + ) + + samples = GEOSample.objects.filter( + Exists(search_term_series) + ) + + stats = { + "tissues": SearchTerm.objects.exclude(term__startswith="MONDO:").values("term").distinct().count(), + "diseases": SearchTerm.objects.filter(term__startswith="MONDO:").values("term").distinct().count(), + "studies": SearchTerm.objects.values("series_id").distinct().count(), + "samples": samples.count(), + "species": samples.values("organism_ch1" ).distinct().count(), + "technologies": GEOPlatform.objects.values("technology").distinct().count(), + } + + pprint(stats) + + for name, value in stats.items(): + SiteStatistic.objects.update_or_create( + name=name, + defaults={"value": value}, + ) + + self.stdout.write(self.style.SUCCESS("Stats table populate completed.")) diff --git a/backend/src/api/migrations/0037_externaldbrefs.py b/backend/src/api/migrations/0037_externaldbrefs.py new file mode 100644 index 0000000..1d0f64f --- /dev/null +++ b/backend/src/api/migrations/0037_externaldbrefs.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.7 on 2026-06-19 17:40 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0036_dedupe_search_onto'), + ] + + operations = [ + migrations.CreateModel( + name='ExternalDbRefs', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('database', models.CharField()), + ('external_id', models.CharField(blank=True, help_text='ID of the series in the external database, if available', null=True)), + ('series', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='external_db_refs', to='api.geoseries')), + ], + options={ + 'indexes': [models.Index(fields=['series'], name='api_externa_series__994a4d_idx'), models.Index(fields=['database'], name='api_externa_databas_81871d_idx')], + 'unique_together': {('series', 'database')}, + }, + ), + ] diff --git a/backend/src/api/migrations/0038_facet_facetentry.py b/backend/src/api/migrations/0038_facet_facetentry.py new file mode 100644 index 0000000..ec1ac76 --- /dev/null +++ b/backend/src/api/migrations/0038_facet_facetentry.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.7 on 2026-06-22 21:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0037_externaldbrefs'), + ] + + operations = [ + migrations.CreateModel( + name='Facet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256, unique=True)), + ('min', models.IntegerField(blank=True, null=True)), + ('max', models.IntegerField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='FacetEntry', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256)), + ('count', models.IntegerField()), + ('facet', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='api.facet')), + ], + options={ + 'indexes': [models.Index(fields=['name'], name='api_faceten_name_7368d5_idx')], + }, + ), + ] diff --git a/backend/src/api/migrations/0039_remove_geoseriesdatabase_api_geoseri_databas_9f2673_idx_and_more.py b/backend/src/api/migrations/0039_remove_geoseriesdatabase_api_geoseri_databas_9f2673_idx_and_more.py new file mode 100644 index 0000000..d6dbbc5 --- /dev/null +++ b/backend/src/api/migrations/0039_remove_geoseriesdatabase_api_geoseri_databas_9f2673_idx_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2026-06-22 23:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0038_facet_facetentry'), + ] + + operations = [ + migrations.RemoveIndex( + model_name='geoseriesdatabase', + name='api_geoseri_databas_9f2673_idx', + ), + migrations.RenameField( + model_name='geoseriesdatabase', + old_name='database_name', + new_name='database', + ), + migrations.AddIndex( + model_name='geoseriesdatabase', + index=models.Index(fields=['database'], name='api_geoseri_databas_b1f8de_idx'), + ), + ] diff --git a/backend/src/api/migrations/0040_externaldbrefs_api_externa_databas_e626f4_idx_and_more.py b/backend/src/api/migrations/0040_externaldbrefs_api_externa_databas_e626f4_idx_and_more.py new file mode 100644 index 0000000..3fa09e8 --- /dev/null +++ b/backend/src/api/migrations/0040_externaldbrefs_api_externa_databas_e626f4_idx_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.7 on 2026-06-22 23:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0039_remove_geoseriesdatabase_api_geoseri_databas_9f2673_idx_and_more'), + ] + + operations = [ + migrations.AddIndex( + model_name='externaldbrefs', + index=models.Index(fields=['database', 'series_id'], name='api_externa_databas_e626f4_idx'), + ), + migrations.AddIndex( + model_name='geoseriesdatabase', + index=models.Index(fields=['database', 'series_id'], name='api_geoseri_databas_49f223_idx'), + ), + ] diff --git a/backend/src/api/migrations/0041_alter_feedback_unique_together.py b/backend/src/api/migrations/0041_alter_feedback_unique_together.py new file mode 100644 index 0000000..a3289b7 --- /dev/null +++ b/backend/src/api/migrations/0041_alter_feedback_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.7 on 2026-06-23 16:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0040_externaldbrefs_api_externa_databas_e626f4_idx_and_more'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='feedback', + unique_together={('series_id', 'user_id')}, + ), + ] diff --git a/backend/src/api/migrations/0042_geosample_api_geosamp_organis_8c91f4_idx.py b/backend/src/api/migrations/0042_geosample_api_geosamp_organis_8c91f4_idx.py new file mode 100644 index 0000000..451424b --- /dev/null +++ b/backend/src/api/migrations/0042_geosample_api_geosamp_organis_8c91f4_idx.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.7 on 2026-06-23 17:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0041_alter_feedback_unique_together'), + ] + + operations = [ + migrations.AddIndex( + model_name='geosample', + index=models.Index(fields=['organism_ch1'], name='api_geosamp_organis_8c91f4_idx'), + ), + ] diff --git a/backend/src/api/migrations/0043_geosample_series_set_and_more.py b/backend/src/api/migrations/0043_geosample_series_set_and_more.py new file mode 100644 index 0000000..2a75968 --- /dev/null +++ b/backend/src/api/migrations/0043_geosample_series_set_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2026-06-24 23:36 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0042_geosample_api_geosamp_organis_8c91f4_idx'), + ] + + operations = [ + migrations.AddField( + model_name='geosample', + name='series_set', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(), blank=True, default=list, help_text='List of GSE IDs this sample belongs to', null=True, size=None), + ), + migrations.AddIndex( + model_name='geosample', + index=models.Index(fields=['series_set'], name='api_geosamp_series__c62dec_idx'), + ), + ] diff --git a/backend/src/api/migrations/0043_geosample_series_set_and_more_squashed_0046_add_geosample_series_set_gin.py b/backend/src/api/migrations/0043_geosample_series_set_and_more_squashed_0046_add_geosample_series_set_gin.py new file mode 100644 index 0000000..1ab2ec9 --- /dev/null +++ b/backend/src/api/migrations/0043_geosample_series_set_and_more_squashed_0046_add_geosample_series_set_gin.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.7 on 2026-06-29 18:12 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [('api', '0043_geosample_series_set_and_more'), ('api', '0044_populate_series_set'), ('api', '0045_search_onto_nolimit'),] + + dependencies = [ + ('api', '0042_geosample_api_geosamp_organis_8c91f4_idx'), + ] + + operations = [ + migrations.AddField( + model_name='geosample', + name='series_set', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(), blank=True, default=list, help_text='List of GSE IDs this sample belongs to', null=True, size=None), + ), + migrations.RunSQL( + sql="\n UPDATE api_geosample SET series_set = array_remove(regexp_split_to_array(trim(series_id), '\\s*;\\s*'), '')\n WHERE series_id IS NOT NULL;\n ", + reverse_sql='\n UPDATE api_geosample SET series_set = NULL;\n ', + ), + migrations.RunSQL( + sql="\nDROP FUNCTION IF EXISTS search_onto(text, integer);\n\nCREATE FUNCTION search_onto(\n query text,\n max_results integer DEFAULT 50\n)\nRETURNS TABLE (\n id varchar, name varchar, ontology varchar, type varchar,\n synonym varchar, scope varchar,\n sim real, scope_weight real, overall_rank real, is_exact boolean\n)\nSET pg_trgm.similarity_threshold = 0.2\nAS $$\n SELECT\n d.id, d.name, d.ontology, d.type,\n d.synonym, d.scope,\n d.sim, d.scope_weight, d.overall_rank, d.is_exact\n FROM (\n SELECT DISTINCT ON (q.id)\n q.id, q.name, q.ontology, q.type,\n q.synonym, q.scope,\n q.sim, q.scope_weight, q.overall_rank, q.is_exact\n FROM (\n -- Branch 1: exact match on term id or name.\n -- No similarity calculation; wins all ranking.\n SELECT\n t.id, t.name, t.ontology, t.type,\n NULL::varchar AS synonym,\n NULL::varchar AS scope,\n 1.0::real AS sim,\n 1.0::real AS scope_weight,\n 1.0::real AS overall_rank,\n TRUE AS is_exact\n FROM api_ontologyterms t\n WHERE (t.id = query OR t.name = query)\n AND EXISTS (\n SELECT 1 FROM api_searchterm st WHERE st.term = t.id\n )\n\n UNION ALL\n\n -- Branch 2: exact match on a synonym.\n -- Carries scope weight but no similarity cost.\n SELECT\n t.id, t.name, t.ontology, t.type,\n s.synonym,\n s.scope,\n 1.0::real AS sim,\n CASE s.scope\n WHEN 'EXACT' THEN 1.5\n WHEN 'NARROW' THEN 1.3\n WHEN 'BROAD' THEN 1.1\n WHEN 'RELATED' THEN 0.9\n ELSE 1.0\n END::real AS scope_weight,\n CASE s.scope\n WHEN 'EXACT' THEN 1.5\n WHEN 'NARROW' THEN 1.3\n WHEN 'BROAD' THEN 1.1\n WHEN 'RELATED' THEN 0.9\n ELSE 1.0\n END::real AS overall_rank,\n TRUE AS is_exact\n FROM api_ontologyterms t\n JOIN api_ontologysynonyms s ON s.term_id = t.id\n WHERE s.synonym = query\n AND t.id <> query\n AND t.name <> query\n AND EXISTS (\n SELECT 1 FROM api_searchterm st WHERE st.term = t.id\n )\n\n UNION ALL\n\n -- Branch 3: fuzzy name match.\n -- Uses the trigram GIN index on api_ontologyterms.name via the %\n -- operator (controlled by the pg_trgm.similarity_threshold = 0.2 SET\n -- clause above, which replaces the previous > 0.2 threshold).\n SELECT\n t.id, t.name, t.ontology, t.type,\n NULL::varchar AS synonym,\n NULL::varchar AS scope,\n similarity(t.name, query)::real AS sim,\n 1.0::real AS scope_weight,\n similarity(t.name, query)::real AS overall_rank,\n FALSE AS is_exact\n FROM api_ontologyterms t\n WHERE t.name % query\n AND t.name <> query\n AND t.id <> query\n AND EXISTS (\n SELECT 1 FROM api_searchterm st WHERE st.term = t.id\n )\n\n UNION ALL\n\n -- Branch 4: fuzzy synonym match.\n -- Uses the trigram GIN index on api_ontologysynonyms.synonym via %.\n SELECT\n t.id, t.name, t.ontology, t.type,\n s.synonym,\n s.scope,\n similarity(s.synonym, query)::real AS sim,\n CASE s.scope\n WHEN 'EXACT' THEN 1.5\n WHEN 'NARROW' THEN 1.3\n WHEN 'BROAD' THEN 1.1\n WHEN 'RELATED' THEN 0.9\n ELSE 1.0\n END::real AS scope_weight,\n (similarity(s.synonym, query) * CASE s.scope\n WHEN 'EXACT' THEN 1.5\n WHEN 'NARROW' THEN 1.3\n WHEN 'BROAD' THEN 1.1\n WHEN 'RELATED' THEN 0.9\n ELSE 1.0\n END)::real AS overall_rank,\n FALSE AS is_exact\n FROM api_ontologyterms t\n JOIN api_ontologysynonyms s ON s.term_id = t.id\n WHERE s.synonym % query\n AND s.synonym <> query\n AND EXISTS (\n SELECT 1 FROM api_searchterm st WHERE st.term = t.id\n )\n ) q\n ORDER BY\n q.id,\n q.is_exact DESC,\n q.overall_rank DESC,\n q.scope_weight DESC,\n q.sim DESC,\n q.synonym NULLS LAST\n ) d\n ORDER BY\n d.is_exact DESC,\n d.overall_rank DESC,\n d.scope_weight DESC,\n d.sim DESC,\n d.id\n LIMIT CASE\n WHEN max_results IS NULL THEN NULL\n ELSE max_results\n END;\n$$ LANGUAGE sql;\n\nalter function search_onto(text, integer) owner to meta2onto;\n", + reverse_sql="\nDROP FUNCTION IF EXISTS search_onto(text, integer);\n\nCREATE FUNCTION search_onto(\n query text,\n max_results integer DEFAULT 50\n)\nRETURNS TABLE (\n id varchar, name varchar, ontology varchar, type varchar,\n synonym varchar, scope varchar,\n sim real, scope_weight real, overall_rank real, is_exact boolean\n)\nSET pg_trgm.similarity_threshold = 0.2\nAS $$\n SELECT\n d.id, d.name, d.ontology, d.type,\n d.synonym, d.scope,\n d.sim, d.scope_weight, d.overall_rank, d.is_exact\n FROM (\n SELECT DISTINCT ON (q.id)\n q.id, q.name, q.ontology, q.type,\n q.synonym, q.scope,\n q.sim, q.scope_weight, q.overall_rank, q.is_exact\n FROM (\n -- Branch 1: exact match on term id or name.\n -- No similarity calculation; wins all ranking.\n SELECT\n t.id, t.name, t.ontology, t.type,\n NULL::varchar AS synonym,\n NULL::varchar AS scope,\n 1.0::real AS sim,\n 1.0::real AS scope_weight,\n 1.0::real AS overall_rank,\n TRUE AS is_exact\n FROM api_ontologyterms t\n WHERE (t.id = query OR t.name = query)\n AND EXISTS (\n SELECT 1 FROM api_searchterm st WHERE st.term = t.id\n )\n\n UNION ALL\n\n -- Branch 2: exact match on a synonym.\n -- Carries scope weight but no similarity cost.\n SELECT\n t.id, t.name, t.ontology, t.type,\n s.synonym,\n s.scope,\n 1.0::real AS sim,\n CASE s.scope\n WHEN 'EXACT' THEN 1.5\n WHEN 'NARROW' THEN 1.3\n WHEN 'BROAD' THEN 1.1\n WHEN 'RELATED' THEN 0.9\n ELSE 1.0\n END::real AS scope_weight,\n CASE s.scope\n WHEN 'EXACT' THEN 1.5\n WHEN 'NARROW' THEN 1.3\n WHEN 'BROAD' THEN 1.1\n WHEN 'RELATED' THEN 0.9\n ELSE 1.0\n END::real AS overall_rank,\n TRUE AS is_exact\n FROM api_ontologyterms t\n JOIN api_ontologysynonyms s ON s.term_id = t.id\n WHERE s.synonym = query\n AND t.id <> query\n AND t.name <> query\n AND EXISTS (\n SELECT 1 FROM api_searchterm st WHERE st.term = t.id\n )\n\n UNION ALL\n\n -- Branch 3: fuzzy name match.\n -- Uses the trigram GIN index on api_ontologyterms.name via the %\n -- operator (controlled by the pg_trgm.similarity_threshold = 0.2 SET\n -- clause above, which replaces the previous > 0.2 threshold).\n SELECT\n t.id, t.name, t.ontology, t.type,\n NULL::varchar AS synonym,\n NULL::varchar AS scope,\n similarity(t.name, query)::real AS sim,\n 1.0::real AS scope_weight,\n similarity(t.name, query)::real AS overall_rank,\n FALSE AS is_exact\n FROM api_ontologyterms t\n WHERE t.name % query\n AND t.name <> query\n AND t.id <> query\n AND EXISTS (\n SELECT 1 FROM api_searchterm st WHERE st.term = t.id\n )\n\n UNION ALL\n\n -- Branch 4: fuzzy synonym match.\n -- Uses the trigram GIN index on api_ontologysynonyms.synonym via %.\n SELECT\n t.id, t.name, t.ontology, t.type,\n s.synonym,\n s.scope,\n similarity(s.synonym, query)::real AS sim,\n CASE s.scope\n WHEN 'EXACT' THEN 1.5\n WHEN 'NARROW' THEN 1.3\n WHEN 'BROAD' THEN 1.1\n WHEN 'RELATED' THEN 0.9\n ELSE 1.0\n END::real AS scope_weight,\n (similarity(s.synonym, query) * CASE s.scope\n WHEN 'EXACT' THEN 1.5\n WHEN 'NARROW' THEN 1.3\n WHEN 'BROAD' THEN 1.1\n WHEN 'RELATED' THEN 0.9\n ELSE 1.0\n END)::real AS overall_rank,\n FALSE AS is_exact\n FROM api_ontologyterms t\n JOIN api_ontologysynonyms s ON s.term_id = t.id\n WHERE s.synonym % query\n AND s.synonym <> query\n AND EXISTS (\n SELECT 1 FROM api_searchterm st WHERE st.term = t.id\n )\n ) q\n ORDER BY\n q.id,\n q.is_exact DESC,\n q.overall_rank DESC,\n q.scope_weight DESC,\n q.sim DESC,\n q.synonym NULLS LAST\n ) d\n ORDER BY\n d.is_exact DESC,\n d.overall_rank DESC,\n d.scope_weight DESC,\n d.sim DESC,\n d.id\n LIMIT max_results;\n$$ LANGUAGE sql;\n\nalter function search_onto(text, integer) owner to meta2onto;\n", + ), + ] diff --git a/backend/src/api/migrations/0044_populate_series_set.py b/backend/src/api/migrations/0044_populate_series_set.py new file mode 100644 index 0000000..7cf7c59 --- /dev/null +++ b/backend/src/api/migrations/0044_populate_series_set.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2026-06-24 23:40 + +from django.db import migrations + +from django.db.migrations import RunSQL + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0043_geosample_series_set_and_more'), + ] + + operations = [ + RunSQL( + """ + UPDATE api_geosample SET series_set = array_remove(regexp_split_to_array(trim(series_id), '\s*;\s*'), '') + WHERE series_id IS NOT NULL; + """, + reverse_sql=""" + UPDATE api_geosample SET series_set = NULL; + """ + ), + ] diff --git a/backend/src/api/migrations/0045_search_onto_nolimit.py b/backend/src/api/migrations/0045_search_onto_nolimit.py new file mode 100644 index 0000000..df3d5c1 --- /dev/null +++ b/backend/src/api/migrations/0045_search_onto_nolimit.py @@ -0,0 +1,317 @@ +# Generated by Django 5.2.7 on 2026-06-27 02:14 + +from django.db import migrations +from django.db.migrations import RunSQL + +# when null is passed for max_results, the function will return all results (no limit) + +FUNC_DEFN = """ +DROP FUNCTION IF EXISTS search_onto(text, integer); + +CREATE FUNCTION search_onto( + query text, + max_results integer DEFAULT 50 +) +RETURNS TABLE ( + id varchar, name varchar, ontology varchar, type varchar, + synonym varchar, scope varchar, + sim real, scope_weight real, overall_rank real, is_exact boolean +) +SET pg_trgm.similarity_threshold = 0.2 +AS $$ + SELECT + d.id, d.name, d.ontology, d.type, + d.synonym, d.scope, + d.sim, d.scope_weight, d.overall_rank, d.is_exact + FROM ( + SELECT DISTINCT ON (q.id) + q.id, q.name, q.ontology, q.type, + q.synonym, q.scope, + q.sim, q.scope_weight, q.overall_rank, q.is_exact + FROM ( + -- Branch 1: exact match on term id or name. + -- No similarity calculation; wins all ranking. + SELECT + t.id, t.name, t.ontology, t.type, + NULL::varchar AS synonym, + NULL::varchar AS scope, + 1.0::real AS sim, + 1.0::real AS scope_weight, + 1.0::real AS overall_rank, + TRUE AS is_exact + FROM api_ontologyterms t + WHERE (t.id = query OR t.name = query) + AND EXISTS ( + SELECT 1 FROM api_searchterm st WHERE st.term = t.id + ) + + UNION ALL + + -- Branch 2: exact match on a synonym. + -- Carries scope weight but no similarity cost. + SELECT + t.id, t.name, t.ontology, t.type, + s.synonym, + s.scope, + 1.0::real AS sim, + CASE s.scope + WHEN 'EXACT' THEN 1.5 + WHEN 'NARROW' THEN 1.3 + WHEN 'BROAD' THEN 1.1 + WHEN 'RELATED' THEN 0.9 + ELSE 1.0 + END::real AS scope_weight, + CASE s.scope + WHEN 'EXACT' THEN 1.5 + WHEN 'NARROW' THEN 1.3 + WHEN 'BROAD' THEN 1.1 + WHEN 'RELATED' THEN 0.9 + ELSE 1.0 + END::real AS overall_rank, + TRUE AS is_exact + FROM api_ontologyterms t + JOIN api_ontologysynonyms s ON s.term_id = t.id + WHERE s.synonym = query + AND t.id <> query + AND t.name <> query + AND EXISTS ( + SELECT 1 FROM api_searchterm st WHERE st.term = t.id + ) + + UNION ALL + + -- Branch 3: fuzzy name match. + -- Uses the trigram GIN index on api_ontologyterms.name via the % + -- operator (controlled by the pg_trgm.similarity_threshold = 0.2 SET + -- clause above, which replaces the previous > 0.2 threshold). + SELECT + t.id, t.name, t.ontology, t.type, + NULL::varchar AS synonym, + NULL::varchar AS scope, + similarity(t.name, query)::real AS sim, + 1.0::real AS scope_weight, + similarity(t.name, query)::real AS overall_rank, + FALSE AS is_exact + FROM api_ontologyterms t + WHERE t.name % query + AND t.name <> query + AND t.id <> query + AND EXISTS ( + SELECT 1 FROM api_searchterm st WHERE st.term = t.id + ) + + UNION ALL + + -- Branch 4: fuzzy synonym match. + -- Uses the trigram GIN index on api_ontologysynonyms.synonym via %. + SELECT + t.id, t.name, t.ontology, t.type, + s.synonym, + s.scope, + similarity(s.synonym, query)::real AS sim, + CASE s.scope + WHEN 'EXACT' THEN 1.5 + WHEN 'NARROW' THEN 1.3 + WHEN 'BROAD' THEN 1.1 + WHEN 'RELATED' THEN 0.9 + ELSE 1.0 + END::real AS scope_weight, + (similarity(s.synonym, query) * CASE s.scope + WHEN 'EXACT' THEN 1.5 + WHEN 'NARROW' THEN 1.3 + WHEN 'BROAD' THEN 1.1 + WHEN 'RELATED' THEN 0.9 + ELSE 1.0 + END)::real AS overall_rank, + FALSE AS is_exact + FROM api_ontologyterms t + JOIN api_ontologysynonyms s ON s.term_id = t.id + WHERE s.synonym % query + AND s.synonym <> query + AND EXISTS ( + SELECT 1 FROM api_searchterm st WHERE st.term = t.id + ) + ) q + ORDER BY + q.id, + q.is_exact DESC, + q.overall_rank DESC, + q.scope_weight DESC, + q.sim DESC, + q.synonym NULLS LAST + ) d + ORDER BY + d.is_exact DESC, + d.overall_rank DESC, + d.scope_weight DESC, + d.sim DESC, + d.id + LIMIT CASE + WHEN max_results IS NULL THEN NULL + ELSE max_results + END; +$$ LANGUAGE sql; + +alter function search_onto(text, integer) owner to meta2onto; +""" + +# reverse: the version from 0031_optimize_search_onto.py +OLD_FUNC_DEFN = """ +DROP FUNCTION IF EXISTS search_onto(text, integer); + +CREATE FUNCTION search_onto( + query text, + max_results integer DEFAULT 50 +) +RETURNS TABLE ( + id varchar, name varchar, ontology varchar, type varchar, + synonym varchar, scope varchar, + sim real, scope_weight real, overall_rank real, is_exact boolean +) +SET pg_trgm.similarity_threshold = 0.2 +AS $$ + SELECT + d.id, d.name, d.ontology, d.type, + d.synonym, d.scope, + d.sim, d.scope_weight, d.overall_rank, d.is_exact + FROM ( + SELECT DISTINCT ON (q.id) + q.id, q.name, q.ontology, q.type, + q.synonym, q.scope, + q.sim, q.scope_weight, q.overall_rank, q.is_exact + FROM ( + -- Branch 1: exact match on term id or name. + -- No similarity calculation; wins all ranking. + SELECT + t.id, t.name, t.ontology, t.type, + NULL::varchar AS synonym, + NULL::varchar AS scope, + 1.0::real AS sim, + 1.0::real AS scope_weight, + 1.0::real AS overall_rank, + TRUE AS is_exact + FROM api_ontologyterms t + WHERE (t.id = query OR t.name = query) + AND EXISTS ( + SELECT 1 FROM api_searchterm st WHERE st.term = t.id + ) + + UNION ALL + + -- Branch 2: exact match on a synonym. + -- Carries scope weight but no similarity cost. + SELECT + t.id, t.name, t.ontology, t.type, + s.synonym, + s.scope, + 1.0::real AS sim, + CASE s.scope + WHEN 'EXACT' THEN 1.5 + WHEN 'NARROW' THEN 1.3 + WHEN 'BROAD' THEN 1.1 + WHEN 'RELATED' THEN 0.9 + ELSE 1.0 + END::real AS scope_weight, + CASE s.scope + WHEN 'EXACT' THEN 1.5 + WHEN 'NARROW' THEN 1.3 + WHEN 'BROAD' THEN 1.1 + WHEN 'RELATED' THEN 0.9 + ELSE 1.0 + END::real AS overall_rank, + TRUE AS is_exact + FROM api_ontologyterms t + JOIN api_ontologysynonyms s ON s.term_id = t.id + WHERE s.synonym = query + AND t.id <> query + AND t.name <> query + AND EXISTS ( + SELECT 1 FROM api_searchterm st WHERE st.term = t.id + ) + + UNION ALL + + -- Branch 3: fuzzy name match. + -- Uses the trigram GIN index on api_ontologyterms.name via the % + -- operator (controlled by the pg_trgm.similarity_threshold = 0.2 SET + -- clause above, which replaces the previous > 0.2 threshold). + SELECT + t.id, t.name, t.ontology, t.type, + NULL::varchar AS synonym, + NULL::varchar AS scope, + similarity(t.name, query)::real AS sim, + 1.0::real AS scope_weight, + similarity(t.name, query)::real AS overall_rank, + FALSE AS is_exact + FROM api_ontologyterms t + WHERE t.name % query + AND t.name <> query + AND t.id <> query + AND EXISTS ( + SELECT 1 FROM api_searchterm st WHERE st.term = t.id + ) + + UNION ALL + + -- Branch 4: fuzzy synonym match. + -- Uses the trigram GIN index on api_ontologysynonyms.synonym via %. + SELECT + t.id, t.name, t.ontology, t.type, + s.synonym, + s.scope, + similarity(s.synonym, query)::real AS sim, + CASE s.scope + WHEN 'EXACT' THEN 1.5 + WHEN 'NARROW' THEN 1.3 + WHEN 'BROAD' THEN 1.1 + WHEN 'RELATED' THEN 0.9 + ELSE 1.0 + END::real AS scope_weight, + (similarity(s.synonym, query) * CASE s.scope + WHEN 'EXACT' THEN 1.5 + WHEN 'NARROW' THEN 1.3 + WHEN 'BROAD' THEN 1.1 + WHEN 'RELATED' THEN 0.9 + ELSE 1.0 + END)::real AS overall_rank, + FALSE AS is_exact + FROM api_ontologyterms t + JOIN api_ontologysynonyms s ON s.term_id = t.id + WHERE s.synonym % query + AND s.synonym <> query + AND EXISTS ( + SELECT 1 FROM api_searchterm st WHERE st.term = t.id + ) + ) q + ORDER BY + q.id, + q.is_exact DESC, + q.overall_rank DESC, + q.scope_weight DESC, + q.sim DESC, + q.synonym NULLS LAST + ) d + ORDER BY + d.is_exact DESC, + d.overall_rank DESC, + d.scope_weight DESC, + d.sim DESC, + d.id + LIMIT max_results; +$$ LANGUAGE sql; + +alter function search_onto(text, integer) owner to meta2onto; +""" + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0044_populate_series_set'), + ] + + operations = [ + RunSQL( + FUNC_DEFN, + reverse_sql=OLD_FUNC_DEFN, + ), + ] diff --git a/backend/src/api/migrations/0046_add_geosample_series_set_gin.py b/backend/src/api/migrations/0046_add_geosample_series_set_gin.py new file mode 100644 index 0000000..e3acc98 --- /dev/null +++ b/backend/src/api/migrations/0046_add_geosample_series_set_gin.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2026-06-27 02:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('api', '0045_search_onto_nolimit'), + ] + + operations = [ + migrations.RunSQL( + sql=""" + CREATE INDEX CONCURRENTLY IF NOT EXISTS geosample_series_set_gin + ON api_geosample + USING GIN (series_set); + """, + reverse_sql=""" + DROP INDEX CONCURRENTLY IF EXISTS geosample_series_set_gin; + """, + ), + ] diff --git a/backend/src/api/migrations/0047_geoseries_submission_date_idx.py b/backend/src/api/migrations/0047_geoseries_submission_date_idx.py new file mode 100644 index 0000000..5677d7d --- /dev/null +++ b/backend/src/api/migrations/0047_geoseries_submission_date_idx.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2026-07-01 14:12 + +import django.contrib.postgres.indexes +from django.db import migrations, models + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('api', '0046_add_geosample_series_set_gin'), + ] + + operations = [ + migrations.RunSQL( + sql=""" + CREATE INDEX CONCURRENTLY IF NOT EXISTS api_geoseri_submiss_a549eb_idx + ON api_geoseries (submission_date); + """, + reverse_sql=""" + DROP INDEX CONCURRENTLY IF EXISTS api_geoseri_submiss_a549eb_idx; + """, + ), + ] diff --git a/backend/src/api/migrations/0048_geoseries_samples_ct_and_more.py b/backend/src/api/migrations/0048_geoseries_samples_ct_and_more.py new file mode 100644 index 0000000..94ba56c --- /dev/null +++ b/backend/src/api/migrations/0048_geoseries_samples_ct_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2026-07-01 14:15 + +import django.contrib.postgres.indexes +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0047_geoseries_submission_date_idx'), + ] + + operations = [ + migrations.AddField( + model_name='geoseries', + name='samples_ct', + field=models.IntegerField(blank=True, null=True), + ), + migrations.RunSQL( + sql=""" + UPDATE api_geoseries SET samples_ct = (SELECT COUNT(*) FROM api_geosample WHERE series_set @> ARRAY[api_geoseries.gse]::varchar[]); + """ + ), + ] diff --git a/backend/src/api/migrations/0049_geosample_geosample_series_set_gin_and_more.py b/backend/src/api/migrations/0049_geosample_geosample_series_set_gin_and_more.py new file mode 100644 index 0000000..8272e8b --- /dev/null +++ b/backend/src/api/migrations/0049_geosample_geosample_series_set_gin_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2026-07-01 14:19 + +import django.contrib.postgres.indexes +from django.db import migrations, models + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('api', '0048_geoseries_samples_ct_and_more'), + ] + + operations = [ + migrations.RunSQL( + sql=""" + CREATE INDEX CONCURRENTLY IF NOT EXISTS api_geoseri_samples_ct_a549eb_idx + ON api_geoseries (samples_ct); + """, + reverse_sql=""" + DROP INDEX CONCURRENTLY IF EXISTS api_geoseri_samples_ct_a549eb_idx; + """, + ), + ] diff --git a/backend/src/api/migrations/0050_geoseries_submission_date_convert_and_more.py b/backend/src/api/migrations/0050_geoseries_submission_date_convert_and_more.py new file mode 100644 index 0000000..6f61bcb --- /dev/null +++ b/backend/src/api/migrations/0050_geoseries_submission_date_convert_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2026-07-01 14:54 + +import django.contrib.postgres.indexes +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0049_geosample_geosample_series_set_gin_and_more'), + ] + + operations = [ + migrations.RemoveIndex( + model_name='geoseries', + name='api_geoseri_gse_01d11b_idx', + ), + migrations.AlterField( + model_name='geoseries', + name='submission_date', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/backend/src/api/migrations/0051_mark_series_set_submission_date_idxes.py b/backend/src/api/migrations/0051_mark_series_set_submission_date_idxes.py new file mode 100644 index 0000000..c7936f6 --- /dev/null +++ b/backend/src/api/migrations/0051_mark_series_set_submission_date_idxes.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2026-07-01 17:13 + +import django.contrib.postgres.indexes +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_geoseries_submission_date_convert_and_more'), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[], + state_operations=[ + migrations.AddIndex( + model_name='geosample', + index=django.contrib.postgres.indexes.GinIndex( + fields=['series_set'], + name='geosample_series_set_gin', + ), + ), + migrations.AddIndex( + model_name='geoseries', + index=models.Index( + fields=['submission_date'], + name='api_geoseri_submiss_a549eb_idx', + ), + ), + ], + ), + ] diff --git a/backend/src/api/migrations/0052_positivestudyannotation.py b/backend/src/api/migrations/0052_positivestudyannotation.py new file mode 100644 index 0000000..203f95b --- /dev/null +++ b/backend/src/api/migrations/0052_positivestudyannotation.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.7 on 2026-07-02 18:27 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_mark_series_set_submission_date_idxes'), + ] + + operations = [ + migrations.CreateModel( + name='PositiveStudyAnnotation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('term', models.CharField(db_index=True, max_length=256)), + ('series', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='positive_annotations', to='api.geoseries')), + ], + options={ + 'indexes': [models.Index(fields=['term', 'series'], name='api_positiv_term_e17b5e_idx')], + }, + ), + ] diff --git a/backend/src/api/migrations/0053_searchseriesorganism.py b/backend/src/api/migrations/0053_searchseriesorganism.py new file mode 100644 index 0000000..443014e --- /dev/null +++ b/backend/src/api/migrations/0053_searchseriesorganism.py @@ -0,0 +1,67 @@ +# Generated by Django 5.2.7 on 2026-07-02 20:37 + +from django.db import migrations, models + +CREATE_MATERIALIZED_VIEW = """ +CREATE MATERIALIZED VIEW api_geoseries_organism AS +WITH series_organisms AS ( + SELECT DISTINCT + ag.gse AS series_id, + btrim(o.organism_raw) AS organism + FROM api_geosample AS gs + CROSS JOIN LATERAL unnest(gs.series_set) AS g(gse_raw) + JOIN api_geoseries AS ag + ON ag.gse = btrim(g.gse_raw) + CROSS JOIN LATERAL regexp_split_to_table( + gs.organism_ch1, + E'\\r?\\n' + ) AS o(organism_raw) + WHERE gs.organism_ch1 IS NOT NULL + AND btrim(o.organism_raw) <> '' +) +SELECT + row_number() OVER ( + ORDER BY series_id, organism + )::bigint AS id, + series_id, + organism +FROM series_organisms; + +REFRESH MATERIALIZED VIEW api_geoseries_organism; +""" + +CREATE_UNIQUE_INDEX = """ +CREATE UNIQUE INDEX api_geoseries_organism_gse_organism_uidx + ON api_geoseries_organism (series_id, organism); +""" + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_positivestudyannotation'), + ] + + operations = [ + migrations.RunSQL( + sql=CREATE_MATERIALIZED_VIEW, + reverse_sql="DROP MATERIALIZED VIEW api_geoseries_organism;", + ), + migrations.RunSQL( + sql=CREATE_UNIQUE_INDEX, + reverse_sql=( + "DROP INDEX api_geoseries_organism_gse_organism_uidx;" + ), + ), + migrations.CreateModel( + name='SearchSeriesOrganism', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('organism', models.CharField()), + ], + options={ + 'db_table': 'api_geoseries_organism', + 'managed': False, + }, + ), + ] diff --git a/backend/src/api/migrations/0054_searchseriestechnology.py b/backend/src/api/migrations/0054_searchseriestechnology.py new file mode 100644 index 0000000..2cf56c4 --- /dev/null +++ b/backend/src/api/migrations/0054_searchseriestechnology.py @@ -0,0 +1,70 @@ +# Generated by Django 5.2.7 on 2026-07-02 23:59 + +from django.db import migrations, models + + +CREATE_MATERIALIZED_VIEW = """ +CREATE MATERIALIZED VIEW api_geoseries_technology AS +WITH series_technologies AS ( + SELECT DISTINCT + btrim(series_value.gse_raw) AS gse, + btrim(platform.technology) AS technology + FROM api_geosample AS sample + CROSS JOIN LATERAL unnest(sample.series_set) AS series_value(gse_raw) + JOIN api_geoseries AS series + ON series.gse = btrim(series_value.gse_raw) + JOIN api_geoplatform AS platform + ON platform.gpl = sample.gpl + WHERE series_value.gse_raw IS NOT NULL + AND btrim(series_value.gse_raw) <> '' + AND platform.technology IS NOT NULL + AND btrim(platform.technology) <> '' +) +SELECT + row_number() OVER ( + ORDER BY gse, technology + )::bigint AS id, + gse, + technology +FROM series_technologies; + +REFRESH MATERIALIZED VIEW api_geoseries_technology; +""" + +CREATE_UNIQUE_INDEX = """ +CREATE UNIQUE INDEX api_geoseries_technology_id_uidx + ON api_geoseries_technology (id); + +CREATE UNIQUE INDEX api_geoseries_technology_gse_technology_uidx + ON api_geoseries_technology (gse, technology); +""" + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_searchseriesorganism'), + ] + + operations = [ + migrations.RunSQL( + sql=CREATE_MATERIALIZED_VIEW, + reverse_sql="DROP MATERIALIZED VIEW api_geoseries_technology;", + ), + migrations.RunSQL( + sql=CREATE_UNIQUE_INDEX, + reverse_sql=( + "DROP INDEX api_geoseries_technology_gse_technology_uidx;" + ), + ), + migrations.CreateModel( + name='SearchSeriesTechnology', + fields=[ + ('id', models.BigIntegerField(primary_key=True, serialize=False)), + ('technology', models.CharField()), + ], + options={ + 'db_table': 'api_geoseries_technology', + 'managed': False, + }, + ), + ] diff --git a/backend/src/api/migrations/0055_searchseriesplatform.py b/backend/src/api/migrations/0055_searchseriesplatform.py new file mode 100644 index 0000000..bd6506d --- /dev/null +++ b/backend/src/api/migrations/0055_searchseriesplatform.py @@ -0,0 +1,69 @@ +# Generated by Django 5.2.7 on 2026-07-07 18:08 + +from django.db import migrations, models + +CREATE_MATERIALIZED_VIEW = """ +CREATE MATERIALIZED VIEW api_geoseries_platform AS +WITH series_platforms AS ( + SELECT DISTINCT + btrim(series_value.gse_raw) AS gse, + btrim(platform.gpl) AS gpl + FROM api_geosample AS sample + CROSS JOIN LATERAL unnest(sample.series_set) AS series_value(gse_raw) + JOIN api_geoseries AS series + ON series.gse = btrim(series_value.gse_raw) + JOIN api_geoplatform AS platform + ON platform.gpl = sample.gpl + WHERE series_value.gse_raw IS NOT NULL + AND btrim(series_value.gse_raw) <> '' + AND platform.gpl IS NOT NULL + AND btrim(platform.gpl) <> '' +) +SELECT + row_number() OVER ( + ORDER BY gse, gpl + )::bigint AS id, + gse, + gpl AS platform +FROM series_platforms; + +REFRESH MATERIALIZED VIEW api_geoseries_platform; +""" + +CREATE_UNIQUE_INDEX = """ +CREATE UNIQUE INDEX api_geoseries_platform_id_uidx + ON api_geoseries_platform (id); + +CREATE UNIQUE INDEX api_geoseries_platform_gse_gpl_uidx + ON api_geoseries_platform (gse, platform); +""" + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_searchseriestechnology'), + ] + + operations = [ + migrations.RunSQL( + sql=CREATE_MATERIALIZED_VIEW, + reverse_sql="DROP MATERIALIZED VIEW api_geoseries_platform;", + ), + migrations.RunSQL( + sql=CREATE_UNIQUE_INDEX, + reverse_sql=( + "DROP INDEX api_geoseries_platform_gse_gpl_uidx;" + ), + ), + migrations.CreateModel( + name='SearchSeriesPlatform', + fields=[ + ('id', models.BigIntegerField(primary_key=True, serialize=False)), + ('platform', models.CharField()), + ], + options={ + 'db_table': 'api_geoseries_platform', + 'managed': False, + }, + ), + ] diff --git a/backend/src/api/migrations/0056_sitestatistic.py b/backend/src/api/migrations/0056_sitestatistic.py new file mode 100644 index 0000000..727b208 --- /dev/null +++ b/backend/src/api/migrations/0056_sitestatistic.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.7 on 2026-07-09 21:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_searchseriesplatform'), + ] + + operations = [ + migrations.CreateModel( + name='SiteStatistic', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256, unique=True)), + ('value', models.IntegerField()), + ], + ), + ] diff --git a/backend/src/api/models.py b/backend/src/api/models.py index a7c1654..bcc698a 100644 --- a/backend/src/api/models.py +++ b/backend/src/api/models.py @@ -2,25 +2,22 @@ from django.db import models from django.db.models import ( + F, Q, Case, + Exists, + OuterRef, TextField, When, Value, CharField, - IntegerField, - FloatField, - Count, - OuterRef, - Subquery, ) -from django.db.models.functions import Coalesce -from django.db.models.sql.constants import INNER +from django.db.models.functions import Cast +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField from django.core.validators import MinValueValidator, MaxValueValidator from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.search import TrigramSimilarity, SearchVector -from django.contrib.postgres.fields import ArrayField -from django_cte import CTE, with_cte -from django_cte.raw import raw_cte_sql +ORGANISM_ARRAY_FIELD = ArrayField(TextField()) class TimeStampedModel(models.Model): @@ -56,56 +53,59 @@ def __str__(self): # =========================================================================== class GEOSeriesManager(models.Manager): - def with_samples_count(self, queryset=None): + def with_facet_buckets(self, queryset=None, confidence_levels=True, study_sizes=True): """ - Annotate each GEOSeries row with samples_ct using a Subquery so the - queryset stays one-row-per-series. + Annotate a queryset with: + - confidence_level, derived from prob + - study_size, derived from samples_ct + Assumes prob and samples_ct are already present or may be null. """ if queryset is None: queryset = self.get_queryset() - return queryset.annotate( - samples_ct=Coalesce( - Subquery( - GEOSample.objects.filter(series_id=OuterRef("gse")) - .values("series_id") - .annotate(c=Count("gsm")) - .values("c")[:1], - output_field=IntegerField(), + # return queryset if neither confidence_levels nor study_sizes are requested + result = queryset + + if confidence_levels: + result = result.annotate( + confidence_level=Case( + When(prob__gte=0.8, then=Value("high")), + When(prob__gte=0.5, then=Value("medium")), + When(prob__lt=0.5, then=Value("low")), + default=Value("unknown"), + output_field=CharField(), + ) + ) + + if study_sizes: + result = result.annotate( + study_size=Case( + When(samples_ct__lt=10, then=Value("small")), + When(samples_ct__gte=10, samples_ct__lte=50, then=Value("medium")), + When(samples_ct__gt=50, then=Value("large")), + default=Value("unknown"), + output_field=CharField(), ), - Value(0), - output_field=IntegerField(), ) - ) + + return result - def with_facet_buckets(self, queryset=None): - """ - Annotate a queryset with: - - confidence_level, derived from prob - - study_size, derived from samples_ct - Assumes prob and samples_ct are already present or may be null. - """ + def with_organisms(self, queryset=None): if queryset is None: queryset = self.get_queryset() return queryset.annotate( - confidence_level=Case( - When(prob__gte=0.8, then=Value("high")), - When(prob__gte=0.5, then=Value("medium")), - When(prob__lt=0.5, then=Value("low")), - default=Value("unknown"), - output_field=CharField(), - ), - study_size=Case( - When(samples_ct__lt=10, then=Value("small")), - When(samples_ct__gte=10, samples_ct__lte=50, then=Value("medium")), - When(samples_ct__gt=50, then=Value("large")), - default=Value("unknown"), - output_field=CharField(), - ), + organism_names=Cast( + ArrayAgg( + "organisms__organism", + distinct=True, + filter=Q(organisms__organism__isnull=False), + ), + output_field=ORGANISM_ARRAY_FIELD, + ) ) - - def search_gse_with_prob(self, query: str, limit: int = 50): + + def search_gse_with_prob(self, query: str): """ Returns a queryset of GEOSeries joined to a CTE containing: (series_id, prob) @@ -113,73 +113,61 @@ def search_gse_with_prob(self, query: str, limit: int = 50): 'query' should be an ontology ID from api_searchterm, e.g. 'MONDO:0000270'. """ - hits = CTE( - raw_cte_sql( - """ - SELECT - st.series_id AS series_id, - st.confidence AS prob, - st.related_words AS keywords - FROM api_searchterm st - WHERE st.term = %s - LIMIT %s - """, - [query, limit], - { - "series_id": CharField(), - "prob": FloatField(), - }, - ), - name="hits", + positive_annotations = PositiveStudyAnnotation.objects.filter( + series_id=OuterRef("pk"), + term=query, ) - - qs = hits.join( - self.get_queryset(), - gse=hits.col.series_id, - _join_type=INNER, - ).annotate(prob=hits.col.prob) - - # we need to join against api_searchterm and retrieve related_words for each GSE - # on the following fields: - # - series_id=the GSE ID - # - term=the original query term - qs = qs.annotate( - keywords=Subquery( - SearchTerm.objects.filter( - series_id=OuterRef("gse"), - term=query, - ).values("related_words")[:1] + + return ( + self.get_queryset() + .filter(search_terms__term=query) + .annotate( + prob=F("search_terms__confidence"), + keywords=F("search_terms__related_words"), + classification=Case( + When( + Exists(positive_annotations), # noqa: F821 + then=Value("Positive"), + ), + default=Value("Unlabeled"), + output_field=CharField(), + ), ) ) - - return with_cte(hits, select=qs) - - def search(self, query: str, max_results: int = 50, order_by: str = "relevance"): + + def order_by_custom(self, queryset=None, order_by="relevance"): """ - Search GEOSeries and return a stable queryset annotated with: - - prob - - samples_ct + Order a queryset of GEOSeries by the given order_by parameter. """ - qs = self.search_gse_with_prob(query=query, limit=max_results) - qs = self.with_samples_count(qs) + if queryset is None: + queryset = self.get_queryset() if order_by == "relevance": - qs = qs.order_by("-prob", "gse") + return queryset.order_by("-prob", "gse") elif order_by == "-relevance": - qs = qs.order_by("prob", "gse") + return queryset.order_by("prob", "gse") elif order_by == "date": - qs = qs.order_by("-submission_date", "gse") + return queryset.order_by("-submission_date", "gse") elif order_by == "-date": - qs = qs.order_by("submission_date", "gse") + return queryset.order_by("submission_date", "gse") elif order_by == "samples": - qs = qs.order_by("-samples_ct", "gse") + return queryset.order_by("-samples_ct", "gse") elif order_by == "-samples": - qs = qs.order_by("samples_ct", "gse") + return queryset.order_by("samples_ct", "gse") else: - qs = qs.order_by("gse") + return queryset.order_by("gse") - return qs + def search(self, query: str, max_results: int | None = 50, order_by: str = "relevance"): + qs = self.search_gse_with_prob(query=query) + qs = self.order_by_custom(qs, order_by=order_by) + # add in organisms as 'organism_names' array field + qs = self.with_organisms(qs) + + if max_results is not None: + qs = qs[:max_results] + + return qs class GEOSeries(models.Model): """ @@ -193,7 +181,8 @@ class GEOSeries(models.Model): title = models.TextField(null=True, blank=True) gse = models.CharField(primary_key=True) status = models.CharField(null=True, blank=True) - submission_date = models.CharField(null=True, blank=True) + # submission_date = models.CharField(null=True, blank=True) + submission_date = models.DateField(null=True, blank=True) last_update_date = models.CharField(null=True, blank=True) pubmed_id = models.BigIntegerField(null=True, blank=True) summary = models.TextField(null=True, blank=True) @@ -210,27 +199,18 @@ class GEOSeries(models.Model): doc = models.TextField(blank=True, null=True) + samples_ct = models.IntegerField(null=True, blank=True) + # runtime annotations prob: float | None = None keywords: str | None = None - samples_ct: int | None = None + classification: str | None = None confidence_level: str | None = None study_size: str | None = None - @property - def database(self): - inlined_db = getattr(self, "_database", None) - if inlined_db is not None: - return inlined_db - return list( - GEOSeriesDatabase.objects.filter(series_id=self.gse).values_list( - "database_name", flat=True - ) - ) - class Meta: indexes = [ - models.Index(fields=["gse"]), + models.Index(fields=["submission_date"]), ] def __str__(self): @@ -292,23 +272,20 @@ class GEOSample(models.Model): blank=True, ) - # # the platform to which this sample belongs - # platform = models.ForeignKey( - # "Platform", - # # to_field="gpl", - # db_column="gpl", - # related_name="samples", - # on_delete=models.DO_NOTHING, - # db_constraint=False, - # null=True, - # blank=True, - # ) + # since 'series' can be a semicolon-delimited list, this column + # breaks it out into a list of GSE IDs for easier querying and filtering + series_set = ArrayField( + models.CharField(), default=list, blank=True, null=True, + help_text="List of GSE IDs this sample belongs to" + ) class Meta: indexes = [ models.Index(fields=["gsm"]), models.Index(fields=["series"]), models.Index(fields=["gpl_raw"]), + models.Index(fields=["organism_ch1"]), + GinIndex(fields=["series_set"], name="geosample_series_set_gin"), ] def __str__(self): @@ -407,9 +384,6 @@ class GEOSeriesDatabase(models.Model): """ Database source for a GEOSeries, e.g., GEO, ArrayExpress, SRA. - We'll eventually use ExternalRelation for this, but for now it's faster to - have a dedicated table that just lists databases. - This model is populated by the management command import_series_databases which takes ids__level-series.parquet as an input, specifically its "relations" column. @@ -423,17 +397,53 @@ class GEOSeriesDatabase(models.Model): on_delete=models.DO_NOTHING, db_constraint=False, ) - database_name = models.CharField() + database = models.CharField() url = models.CharField(null=True, blank=True) class Meta: indexes = [ models.Index(fields=["series"]), - models.Index(fields=["database_name"]), + models.Index(fields=["database"]), + models.Index(fields=["database", "series_id"]), + ] + + def __str__(self): + return f"{self.series.series_id} in {self.database}" + + +class ExternalDbRefs(models.Model): + """ + External database references for a GEOSeries, e.g., ARCHS4, Recount3, refine.bio. + + This model is populated by the management command import_external_db_refs + which takes the following files as input: + - data/expression_db_references/archs4_studies_*.txt + - data/expression_db_references/recount3_studies_*.parquet + - data/expression_db_references/refinebio_studies_*.parquet + """ + + series = models.ForeignKey( + GEOSeries, + related_name="external_db_refs", + null=True, + blank=True, + on_delete=models.DO_NOTHING, + db_constraint=False, + ) + database = models.CharField() + external_id = models.CharField(null=True, blank=True, help_text="ID of the series in the external database, if available") + + class Meta: + indexes = [ + models.Index(fields=["series"]), + models.Index(fields=["database"]), + models.Index(fields=["database", "series_id"]), ] + # make (series, database) unique together to avoid duplicates + unique_together = ("series", "database") def __str__(self): - return f"{self.series.series_id} in {self.database_name}" + return f"{self.series.series_id} in {self.database} {' (ID: ' + self.external_id + ')' if self.external_id else ''}" # =========================================================================== @@ -464,7 +474,9 @@ class SearchTerm(models.Model): backend.src.api.views.ontology_search for how the actual fetching of matching GEOSeries is performed. - Originates from disease_predictions.parquet and tissue_predictions.parquet + Loaded from the following files in management/commands/import_search_parquet.py: + - data/search_tables/disease_predictions.parquet + - data/search_tables/tissue_predictions.parquet """ objects = SearchTermManager() @@ -503,18 +515,198 @@ class OntologyTermRating(models.Model): (Note that, while not required, as of 2026-06-05, SearchTerm has as many unique values of 'term' as there rows in this table.) + + Loaded from the following file in management/commands/import_search_parquet.py: + - data/search_tables/eval.parquet """ term = models.CharField(max_length=256, db_index=True) performance = models.CharField(max_length=64) type = models.CharField(max_length=64) +class PositiveStudyAnnotation(models.Model): + """ + Positive study annotations for each ontology term. + + Loaded from the following file in management/commands/import_search_parquet.py: + - data/search_tables/positive_study_annotations.parquet + """ + term = models.CharField(max_length=256, db_index=True) + series = models.ForeignKey( + GEOSeries, + related_name="positive_annotations", + null=True, + blank=True, + on_delete=models.DO_NOTHING, + db_constraint=False, + ) + + class Meta: + indexes = [ + models.Index(fields=["term", "series"]), + ] + +class FacetEntry(models.Model): + """ + Individual facet value for a categorical facet. + + This is used to store the individual values and counts for a categorical facet, + e.g. "Platform" or "Technology". + """ + + facet = models.ForeignKey( + "Facet", + related_name="entries", + null=True, + blank=True, + on_delete=models.CASCADE + ) + name = models.CharField(max_length=256) + count = models.IntegerField() + + class Meta: + indexes = [ + models.Index(fields=["name"]), + ] + + def __str__(self): + return f"{self.name}: {self.count}" + +class Facet(models.Model): + """ + Global facet values for the entire dataset, used to populate the sidebar filters. + + If the facet has min and max defined, that's passed verbatim to the frontend. + Otherwise, we return the values of FacetEntry associated with this facet. + + (On why global facets were introduced: normally, facets are computed on the + current query results and updated as the user selects additional filtering + options. Unfotunately, running these queries on the fly for the current + search is too slow, and caching, the natural choice for speeding up slow + queries, is complicated by the introduction of user-supplied feedback to the + series response.) + """ + + name = models.CharField(max_length=256, unique=True) + min = models.IntegerField(null=True, blank=True) + max = models.IntegerField(null=True, blank=True) + + def __str__(self): + return f"{self.name}: {self.value}" + +class SiteStatistic(models.Model): + """ + Global statistics for the entire dataset, used to populate the homepage's summary statistics. + """ + + name = models.CharField(max_length=256, unique=True) + value = models.IntegerField() + + def __str__(self): + return f"{self.name}: {self.value}" + +# --------------------------------------------------------------------------- +# --- search-related materialized views +# --------------------------------------------------------------------------- + +# TBC: create views/migrations for the following: +# - (series_id, organism_ch1) via series.gse <-(series_set)- samples.organism_ch1 +# - (series_id, technology) via series.gse <-(series_set)- samples -(gpl)-> platforms.technology + +class SearchSeriesOrganism(models.Model): + """ + Materialized view of (series_id, organism_ch1) + via series.gse <-(series_set)- samples.organism_ch1 + """ + # note that this is generated by the view and thus unstable. it's only to + # satisfy Django's requirement for a primary key, and should not be used in + # queries + id = models.BigIntegerField(primary_key=True) + + series = models.ForeignKey( + GEOSeries, + related_name="organisms", + null=True, + blank=True, + on_delete=models.DO_NOTHING, + db_constraint=False, + ) + organism = models.CharField() + + class Meta: + managed = False + db_table = "api_geoseries_organism" + unique_together = (("series", "organism"),) + + def __str__(self): + return f"{self.series.series_id}: {self.organism}" + +class SearchSeriesTechnology(models.Model): + """ + Materialized view of (series_id, technology) + via series.gse <-(series_set)- samples -(gpl)-> platforms.technology + """ + # note that, like SearchSeriesOrganism, this is generated by the view and + # thus unstable. it's only to satisfy Django's requirement for a primary + # key, and should not be used in queries + id = models.BigIntegerField(primary_key=True) + + series = models.ForeignKey( + GEOSeries, + related_name="technologies", + db_column="gse", + to_field="gse", + null=True, + blank=True, + on_delete=models.DO_NOTHING, + db_constraint=False, + ) + technology = models.CharField() + + class Meta: + managed = False + db_table = "api_geoseries_technology" + unique_together = (("series", "technology"),) + + def __str__(self): + return f"{self.series.series_id}: {self.technology}" + +class SearchSeriesPlatform(models.Model): + """ + Materialized view of (series_id, platform) + via series.gse <-(series_set)- samples -(gpl)-> platforms.gpl + """ + # note that, like SearchSeriesOrganism, this is generated by the view and + # thus unstable. it's only to satisfy Django's requirement for a primary + # key, and should not be used in queries + id = models.BigIntegerField(primary_key=True) + + series = models.ForeignKey( + GEOSeries, + related_name="platforms", + db_column="gse", + to_field="gse", + null=True, + blank=True, + on_delete=models.DO_NOTHING, + db_constraint=False, + ) + platform = models.CharField() + + class Meta: + managed = False + db_table = "api_geoseries_platform" + unique_together = (("series", "platform"),) + + def __str__(self): + return f"{self.series.series_id}: {self.platform}" + # =========================================================================== # === Ontology search terms from meta-hq # =========================================================================== class OntologySearchResultsManager(models.Manager): - def search(self, query: str, max_results: int = 5000): + def search(self, query: str, max_results: int | None = 5000): """ Perform a search for the given query string across ontology terms + synonyms. @@ -530,7 +722,7 @@ def search(self, query: str, max_results: int = 5000): ) return qs - def search_series(self, query: str, max_results: int = 5000): + def search_series(self, query: str, max_results: int | None = 5000): """ Perform a search for the given query string across ontology terms; joins the ontology terms against api_searchterm to get associated GEOSeries. @@ -729,5 +921,8 @@ class Feedback(TimeStampedModel): keywords = models.JSONField(null=True, blank=True) elaborate = models.TextField(null=True, blank=True) + class Meta: + unique_together = ("series_id", "user_id") + def __str__(self): return f"Feedback from {self.name or 'anonymous'} ({self.email or 'no email'}) at {self.created_at}" diff --git a/backend/src/api/serializers.py b/backend/src/api/serializers.py index c5e4258..b1d5e25 100644 --- a/backend/src/api/serializers.py +++ b/backend/src/api/serializers.py @@ -1,13 +1,13 @@ from rest_framework import serializers +from django.db.models import Avg, Count, Sum, Q + from .models import ( + Feedback, GEOSample, GEOSeries, - GEOSeriesToGEOPlatforms, Organism, GEOPlatform, SearchTerm, - GEOSeries, - GEOSample, OntologySearchResults, OntologySearchDocs, OntologySynonyms, @@ -154,23 +154,43 @@ def get_confidence(self, obj): label = "low" return {"name": label, "value": obj.prob} - database = serializers.ListField(child=serializers.CharField(), read_only=True) + database = serializers.SerializerMethodField() + + def get_database(self, obj): + series_dbs = { + item.database: { + "url": item.url.strip() if item.url else item.url, + } + for item in obj.databases.all() + } + + external_refs = { + item.database: { + "external_id": ( + item.external_id.strip() + if item.external_id else item.external_id + ), + } + for item in obj.external_db_refs.all() + } + + return {**series_dbs, **external_refs} # FIXME: renames to support frontend changes; i'm probably going to # keep the db layer the same to ease imports and just rename fields at the # serializer layer. id = serializers.CharField(source="gse", read_only=True) name = serializers.CharField(source="title", read_only=True) - submitted_at = serializers.DateTimeField(source="submission_date", read_only=True) + submitted_at = serializers.DateField(source="submission_date", read_only=True) description = serializers.CharField(source="summary", read_only=True) # platform = serializers.CharField(source="database", read_only=True) platform = serializers.SerializerMethodField() - def get_platform(self, obj): + def get_platform(self, obj) -> list[str] | None: """Get the platform name associated with this series.""" - gse_obj = GEOSeriesToGEOPlatforms.objects.filter(gse=obj.gse).first() - return str(gse_obj.platforms) if gse_obj else "" + platforms = getattr(obj, "prefetched_platforms", []) + return [x.platform for x in platforms] if platforms else [] keywords = serializers.SerializerMethodField() @@ -180,12 +200,44 @@ def get_keywords(self, obj): return [kw.strip() for kw in obj.keywords.split(",")] return [] - classification = serializers.SerializerMethodField() + feedback = serializers.SerializerMethodField() + + def get_feedback(self, obj) -> dict[str, int | float]: + """Returns aggregate rating and number of votes for this series. + + In the Feedback model, "likes" have a rating of 1 and "dislikes" have a rating of -1. + + """ + feedback = ( + Feedback.objects.filter(series_id=obj) + .aggregate( + vote_count=Count('id'), + likes=Count('id', filter=Q(rating=1)), + dislikes=Count('id', filter=Q(rating=-1)), + ) + ) + + return feedback if feedback else {"avg_rating": 0, "vote_count": 0, "sum_rating": 0, "likes": 0, "dislikes": 0} + + organisms = serializers.SerializerMethodField() - def get_classification(self, obj): - """Returns values Positive or Negative; supposed to represent 'Classification of study in model training'?""" - # FIXME: figure out how to actually determine this - return "Positive" + def get_organisms(self, obj) -> list[str]: + sentinel = object() + annotated_organisms = getattr(obj, "organism_names", sentinel) + + if annotated_organisms is not sentinel: + return annotated_organisms or [] + + return [ + organism.organism + for organism in obj.organisms.all() + ] + + # technologies = serializers.SlugRelatedField( + # many=True, + # read_only=True, + # slug_field="technology", + # ) class Meta: model = GEOSeries @@ -216,11 +268,14 @@ class Meta: # from joining with api_sample count "sample_count", # FIXME: review if samples_ct can be remapped to this # from joining with api_seriesdatabase - "database", # FIXME: review if still used + "database", "platform", + "organisms", + # "technologies", "keywords", "classification", + "feedback" ] class GEOSampleSerializer(serializers.ModelSerializer): @@ -235,6 +290,22 @@ class Meta: fields = "__all__" +# =========================================================================== +# === Database statistics +# =========================================================================== + +class DatabaseStatsSerializer(serializers.Serializer): + """Serializer for database statistics returned from /api/stats/ endpoint.""" + + tissues = serializers.IntegerField() + diseases = serializers.IntegerField() + studies = serializers.IntegerField() + samples = serializers.IntegerField() + species = serializers.IntegerField() + technologies = serializers.IntegerField() + feedback = serializers.IntegerField() + + # =========================================================================== # === Cart server-side state diff --git a/backend/src/api/urls.py b/backend/src/api/urls.py index 1a6dae2..c55701c 100644 --- a/backend/src/api/urls.py +++ b/backend/src/api/urls.py @@ -1,3 +1,5 @@ +from collections import OrderedDict + from django.urls import path, include from rest_framework.routers import DefaultRouter @@ -7,29 +9,59 @@ OrganismViewSet, GEOPlatformViewSet, SearchTermViewSet, - GEOSeriesViewSet, GEOSampleViewSet, - # OrganismForPairingViewSet, - # GEOSeriesRelationsViewSet, - # ExternalRelationViewSet, ontology_search, + database_statistics, ) -# Create a router and register our viewsets with it -router = DefaultRouter() +class APIRouter(DefaultRouter): + ''' + Customized Default Router to include non-viewset views on root page + ''' + single_views:list + def __init__(self, single_views:list, *args, **kwargs): + self.single_views = single_views + self.trailing_slash = '/?' + super().__init__(*args, **kwargs) + + def get_api_root_view(self, api_urls=None): + """ + Return a basic root view. + """ + api_root_dict = OrderedDict() + list_name = self.routes[0].name + for prefix, viewset, basename in self.registry: + api_root_dict[prefix] = list_name.format(basename=basename) + for single_view in self.single_views: + sanitized_route = single_view['route'].rstrip('/?').rstrip('/') + api_root_dict[sanitized_route] = single_view['name'] + return self.APIRootView.as_view(api_root_dict=api_root_dict) + +single_views = [ + { + "route": "ontology/search", + "view": ontology_search, + "name": "ontology-search" + }, + { + "route": "stats", + "view": database_statistics, + "name": "database-statistics" + }, +] + +router = APIRouter(single_views=single_views) + router.register(r"organisms", OrganismViewSet, basename="organism") router.register(r"platforms", GEOPlatformViewSet, basename="platform") router.register(r"study", GEOSeriesViewSet, basename="study") router.register(r"samples", GEOSampleViewSet, basename="sample") -# router.register(r'organism-pairings', OrganismForPairingViewSet, basename='organism-pairing') -# router.register(r'series-relations', GEOSeriesRelationsViewSet, basename='series-relations') -# router.register(r'external-relations', ExternalRelationViewSet, basename='external-relation') router.register(r"search-terms", SearchTermViewSet, basename="search-term") router.register(r"cart", CartViewSet, basename="cart") -# The API URLs are now determined automatically by the router urlpatterns = [ path("", include(router.urls)), path("ontology/search/", ontology_search, name="ontology-search"), + path("stats/", database_statistics, name="database-statistics"), # path('cart/download/', download_cart, name='cart-download'), ] diff --git a/backend/src/api/utils/query.py b/backend/src/api/utils/query.py new file mode 100644 index 0000000..d62e5ec --- /dev/null +++ b/backend/src/api/utils/query.py @@ -0,0 +1,62 @@ +""" +Functions that extend Django's ORM to support, e.g., postgres-specific operations. +""" + +from django.db.models import ( + BooleanField, + Count, + F, + Func, + IntegerField, + OuterRef, + Subquery, + Value, + CharField, +) + +from django.db.models.functions import Coalesce +from django.conf import settings + +from django.core.cache import cache + +from api.utils.timing import timed + +class ArrayAnyEquals(Func): + """ + Compile: + + lhs = ANY(rhs) + + Example: + + OuterRef("gse") = ANY(GEOSample.series_set) + """ + + output_field = BooleanField() + + def __init__(self, lhs, rhs, **extra): + super().__init__(lhs, rhs, **extra) + + def as_sql(self, compiler, connection, **extra_context): + lhs_sql, lhs_params = compiler.compile(self.source_expressions[0]) + rhs_sql, rhs_params = compiler.compile(self.source_expressions[1]) + + sql = f"{lhs_sql} = ANY({rhs_sql})" + params = [*lhs_params, *rhs_params] + + return sql, params + + +class Array(Func): + """ + Compile: + + ARRAY[expr] + + Example: + + ARRAY[api_geoseries.gse]::varchar[] + """ + + template = "ARRAY[%(expressions)s]::varchar[]" + output_field = CharField() diff --git a/backend/src/api/utils/timing.py b/backend/src/api/utils/timing.py new file mode 100644 index 0000000..d81df7e --- /dev/null +++ b/backend/src/api/utils/timing.py @@ -0,0 +1,21 @@ +from contextlib import contextmanager +from time import perf_counter + +import sys + +@contextmanager +def timed(label="Elapsed", print_method=print, flush=True): + start = perf_counter() + try: + yield + finally: + elapsed = perf_counter() - start + print_method(f"{label}: {elapsed:.3f} seconds") + + if flush: + # if flush exists on print_method, call it to ensure immediate output + if hasattr(print_method, "flush"): + print_method.flush() + # if it's just normal print, flush the standard output + elif print_method == print: + sys.stdout.flush() diff --git a/backend/src/api/views.py b/backend/src/api/views.py index d338d02..2e17d85 100644 --- a/backend/src/api/views.py +++ b/backend/src/api/views.py @@ -1,24 +1,22 @@ import csv -from collections import Counter +import re from django.conf import settings from django.db import models, transaction from django.db.models import ( - Case, - When, + Prefetch, Value, Count, OuterRef, Subquery, - IntegerField, CharField, F, Func, + Exists, ) -from django.db.models.functions import Coalesce +from django.db.models.functions import Cast from django.http import HttpResponse from django.utils.decorators import method_decorator -from django.views.decorators.cache import cache_page from django.views.decorators.csrf import csrf_exempt from rest_framework import status, viewsets from rest_framework.decorators import action, api_view, permission_classes @@ -28,19 +26,24 @@ from rest_framework.response import Response from .models import ( + ORGANISM_ARRAY_FIELD, Cart, CartItem, GEOPlatform, GEOSample, GEOSeries, GEOSeriesToGEOPlatforms, + GEOSeriesDatabase, + SearchSeriesPlatform, + ExternalDbRefs, OntologySearchResults, Organism, - GEOPlatform, SearchTerm, + OntologyTerms, OntologyTermRating, - GEOSeries, Feedback, + Facet, + SiteStatistic, ) from .serializers import ( CartSerializer, @@ -50,7 +53,7 @@ OrganismSerializer, GEOPlatformSerializer, SearchTermSerializer, - GEOSeriesSerializer, + DatabaseStatsSerializer, ) from .utils.auth import CsrfExemptSessionAuthentication @@ -135,111 +138,107 @@ class GEOSeriesViewSet(viewsets.ReadOnlyModelViewSet): ordering = ["gse"] pagination_class = GEOSeriesSearchPagination - def _with_samples_count(self, queryset): - """ - Keep queryset at one row per GEOSeries while annotating sample counts. - """ - return queryset.annotate( - samples_ct=Coalesce( - Subquery( - GEOSample.objects.filter(series_id=OuterRef("gse")) - .values("series_id") - .annotate(c=Count("gsm")) - .values("c")[:1], - output_field=IntegerField(), - ), - Value(0), - output_field=IntegerField(), - ) - ) - def _with_facet_buckets(self, queryset): """ Add stable bucket annotations used by facets and filters. Assumes prob may or may not be present, and samples_ct is present. """ - return queryset.annotate( - confidence_level=Case( - When(prob__gte=0.8, then=Value("high")), - When(prob__gte=0.5, then=Value("medium")), - When(prob__lt=0.5, then=Value("low")), - default=Value("unknown"), - output_field=CharField(), - ), - study_size=Case( - When(samples_ct__lt=10, then=Value("small")), - When(samples_ct__gte=10, samples_ct__lte=50, then=Value("medium")), - When(samples_ct__gt=50, then=Value("large")), - default=Value("unknown"), - output_field=CharField(), - ), - ) + return GEOSeries.objects.with_facet_buckets(queryset, confidence_levels=True, study_sizes=False) def _build_facets(self, queryset): """Compute facets for the search result set.""" - annotated_qs = self._with_samples_count(queryset) - annotated_qs = self._with_facet_buckets(annotated_qs).order_by() + if settings.COMPUTE_FACETS_DYNAMICALLY: + annotated_qs = self._with_facet_buckets(queryset) - # confidence facet - confidence_counts = annotated_qs.values("confidence_level").annotate( - count=Count("gse") - ) + # # confidence facet + # removed b/c we now hardcode the response + # confidence_counts = annotated_qs.values("confidence_level").annotate( + # count=Count("gse") + # ) - # study size facet - study_size_counts = annotated_qs.values("study_size").annotate( - count=Count("gse") - ) + # study size facet + study_size_counts = annotated_qs.values("study_size").annotate( + count=Count("gse") + ) - # platform facet - gse_list = list(queryset.values_list("gse", flat=True)) + # platform facet + gse_list = list(queryset.values_list("gse", flat=True)) - platform_counts_qs = ( - GEOSeriesToGEOPlatforms.objects - .filter(gse__in=gse_list) - .annotate( - gpl=Func(F("platforms"), function="unnest", output_field=CharField()) + platform_counts_qs = ( + GEOSeriesToGEOPlatforms.objects + .filter(gse__in=gse_list) + .annotate( + gpl=Func(F("platforms"), function="unnest", output_field=CharField()) + ) + .values("gse", "gpl") + .distinct() ) - .values("gse", "gpl") - .distinct() - ) - # platform facets are a little different; we need to return the ID - # for display, but we use the gpl field as the key for searches - platform_counts = ( - GEOPlatform.objects - .filter(gpl__in=platform_counts_qs.values("gpl")) - .values("gpl") - .annotate(count=Count("gpl", distinct=True)) - ) + # platform facets are a little different; we need to return the ID + # for display, but we use the gpl field as the key for searches + platform_counts = ( + GEOPlatform.objects + .filter(gpl__in=platform_counts_qs.values("gpl")) + .values("gpl") + .annotate(count=Count("gpl", distinct=True)) + ) - # also facet by technology - technology_counts = ( - GEOPlatform.objects - .filter(gpl__in=platform_counts_qs.values("gpl")) - .values("technology") - .annotate(count=Count("gpl", distinct=True)) - ) + # also facet by technology + technology_counts = ( + GEOPlatform.objects + .filter(gpl__in=platform_counts_qs.values("gpl")) + .values("technology") + .annotate(count=Count("gpl", distinct=True)) + ) + + # final facet results + return { + "Study Size": { + entry["study_size"]: entry["count"] for entry in study_size_counts + }, + "Confidence": { + # entry["confidence_level"]: entry["count"] for entry in confidence_counts + "label": "Confidence", + "min": 0, + "max": 100, + }, + "Platforms": { + (row["gpl"] or "unknown"): row["count"] for row in platform_counts + }, + "Technologies": { + (row["technology"] or "unknown"): row["count"] for row in technology_counts + }, + } + else: + # query Facet and FacetEntry tables for precomputed facet values + facets = {} + + for facet in Facet.objects.prefetch_related("entries").all(): + if facet.min is not None and facet.max is not None: + # min/max facet + facets[facet.name] = { + "label": facet.name, + "min": facet.min, + "max": facet.max, + } + else: + # categorical facet + facets[facet.name] = { + entry.name: entry.count + for entry in ( + facet.entries + .filter(count__gte=1) + .order_by("-count") + )[:20] + } + + return facets - # final facet results - return { - "Study Size": { - entry["study_size"]: entry["count"] for entry in study_size_counts - }, - "Confidence": { - entry["confidence_level"]: entry["count"] for entry in confidence_counts - }, - "Platforms": { - (row["gpl"] or "unknown"): row["count"] for row in platform_counts - }, - "Technologies": { - (row["technology"] or "unknown"): row["count"] for row in technology_counts - }, - } # search by ontology ID (e.g., MONDO:0000270), which consults SearchTerm for # series matching the term - @method_decorator(cache_page(settings.LONGTERM_CACHE_TIMEOUT)) + # @method_decorator(cache_page(settings.LONGTERM_CACHE_TIMEOUT)) @action( detail=False, methods=["get"], url_path="search", permission_classes=[AllowAny] ) @@ -270,16 +269,29 @@ def search(self, request): results = GEOSeries.objects.search(query, max_results=max_results, order_by=ordering) # adds annotations used for building facets - results = self._with_samples_count(results) results = self._with_facet_buckets(results) # Build facets BEFORE applying facet filters, so facets describe the full # searched result set facets = self._build_facets(results) + # prefetch models we join against to avoid N+1 queries + results = results.prefetch_related( + "technologies", + "databases", + "external_db_refs", + ) + + results = results.prefetch_related( + Prefetch( + "platforms", # replace with the actual related_name + queryset=SearchSeriesPlatform.objects.all(), + to_attr="prefetched_platforms", + ) + ) # --------------------------------------------------------------- - # --- apply faceting options from request + # --- apply faceting filter options from request # --------------------------------------------------------------- # if confidence is provided, filter by confidence bucket @@ -293,6 +305,16 @@ def search(self, request): results = results.filter(prob__lt=0.5) elif confidence == "unknown": results = results.filter(prob__isnull=True) + elif re.match(r"^[0-9]+-[0-9]+$", confidence or ""): + # check if confidence can be interpreted as a range like "70-90" and filter accordingly + try: + low, high = tuple(int(x) for x in confidence.split("-")) + if 0 <= low <= 100: + results = results.filter(prob__gte=low / 100) + if 0 <= high <= 100: + results = results.filter(prob__lte=high / 100) + except ValueError: + pass # ignore invalid confidence values # if study size is provided, filter by samples_ct bucket study_size = request.query_params.get("Study Size") @@ -303,6 +325,16 @@ def search(self, request): results = results.filter(samples_ct__gte=10, samples_ct__lte=50) elif study_size == "large": results = results.filter(samples_ct__gt=50) + elif re.match(r"^[0-9]+-[0-9]+$", study_size or ""): + # check if study size can be interpreted as a range like "10-50" and filter accordingly + try: + low, high = tuple(int(x) for x in study_size.split("-")) + if low >= 0: + results = results.filter(samples_ct__gte=low) + if high >= 0: + results = results.filter(samples_ct__lte=high) + except ValueError: + pass # ignore invalid study size values # if platforms is provided: # 1. get GPLs whose technology is in requested platform technologies @@ -326,45 +358,82 @@ def search(self, request): results = results.filter(gse__in=Subquery(gse_values)) - # if Technologies is provided, do the following: - # 1. find GPLs whose technology is in requested technologies - # 2. find GSEs whose platforms array overlaps those GPLs - # 3. filter results to those GSEs - technologies = request.query_params.getlist("Technologies") + # if Technologies is provided, use the api_geoseries_technology + # materialized view to find GSEs whose technologies array overlaps the + # requested technologies + technologies = [ + technology.strip() + for technology in request.query_params.getlist("Technologies") + if technology.strip() + ] + if technologies: - tech_gpls = list( - GEOPlatform.objects.filter(technology__in=technologies).values_list( - "gpl", flat=True + for technology in set(technologies): + results = results.filter( + technologies__technology=technology ) - ) - tech_gse_values = ( - GEOSeriesToGEOPlatforms.objects.filter(platforms__overlap=tech_gpls) - .values_list("gse", flat=True) - .distinct() + results = results.distinct() + + + # if Databases is provided, filter results to those GSEs whose database + # field matches the requested databases in either of our two tables for + # databases, GEOSeriesDatabase or ExternalDbRefs + # (which, on writing this out, i realized we should probably merge) + databases = request.query_params.getlist("Databases") + if databases: + for db in databases: + if db in GEOSeriesDatabase.objects.values_list("database", flat=True): + in_geo_series_database = GEOSeriesDatabase.objects.filter( + series_id=OuterRef("gse"), + database=db, + ) + else: + in_geo_series_database = GEOSeriesDatabase.objects.none() + + if db in ExternalDbRefs.objects.values_list("database", flat=True): + in_external_refs = ExternalDbRefs.objects.filter( + series_id=OuterRef("gse"), + database=db, + ) + else: + in_external_refs = ExternalDbRefs.objects.none() + + results = results.filter( + Exists(in_geo_series_database) | Exists(in_external_refs) + ) + + # if Organisms is provided, filter results to those GSEs whose samples have the requested organisms + organisms = [ + organism.strip() + for organism in request.query_params.getlist("Organisms") + if organism.strip() + ] + + if organisms: + results = results.filter( + organism_names__contains=Cast( + Value(organisms), + output_field=ORGANISM_ARRAY_FIELD, + ) ) - results = results.filter(gse__in=Subquery(tech_gse_values)) + # filter on Classification + classifications = [ + classification.strip() + for classification in request.query_params.getlist("Classification") + if classification.strip() + ] + if classifications: + results = results.filter( + classification__in=classifications + ) # --------------------------------------------------------------- - # --- apply ordering, limit options from request + # --- apply limit options from request, prepare for final render # --------------------------------------------------------------- - # apply ordering again after facet filters if needed - if ordering == "relevance": - results = results.order_by("-prob", "gse") - elif ordering == "-relevance": - results = results.order_by("prob", "gse") - elif ordering == "date": - results = results.order_by("-submission_date", "gse") - elif ordering == "-date": - results = results.order_by("submission_date", "gse") - elif ordering == "samples": - results = results.order_by("-samples_ct", "gse") - elif ordering == "-samples": - results = results.order_by("samples_ct", "gse") - # paginate the response if limit is not None: self.pagination_class.limit = limit @@ -377,12 +446,24 @@ def search(self, request): # --- build final result set, either paginated or not # --------------------------------------------------------------- + + try: + rec_type, rec_name = OntologyTerms.objects.filter(id=query).values_list("type", "name").first() + except TypeError: + rec_type, rec_name = "", "" + + performance_row = ( + OntologyTermRating.objects + .filter(term=query).values("performance").first() + ) meta = { "term": query, + "name": rec_name, + "type": rec_type, "performance": ( - OntologyTermRating.objects - .filter(term=query).values("performance").first() - .get("performance", "unknown") + performance_row.get("performance", "unknown") + if performance_row else + "unknown" ) } @@ -429,7 +510,18 @@ def lookup(self, request): ) def samples(self, request, pk=None): series = self.get_object() - samples = GEOSample.objects.filter(series_id=series.gse).all() + samples = GEOSample.objects.filter(series_set__contains=[series.gse]).all() + + # if query was provided, search within the samples for that series + query = request.query_params.get("query") + if query: + samples = samples.filter( + models.Q(gsm__icontains=query) + | models.Q(title__icontains=query) + | models.Q(data_processing__icontains=query) + | models.Q(description__icontains=query) + ).distinct() + page = self.paginate_queryset(samples) if page is not None: serializer = GEOSampleSerializer(page, many=True) @@ -443,18 +535,20 @@ def samples(self, request, pk=None): detail=False, methods=["post"], url_path="feedback", permission_classes=[AllowAny] ) def feedback(self, request): - candidate = Feedback( - series_id=GEOSeries.objects.get(gse=request.data.get("id")), - user_id=request.headers.get("X-User-UUID", ""), - name=request.data.get("user", {}).get("name", ""), - email=request.data.get("user", {}).get("email", ""), - rating=request.data.get("rating"), - qualities=request.data.get("qualities", []), - keywords=request.data.get("keywords", {}), - elaborate=request.data.get("elaborate", ""), - ) try: - candidate.save() + Feedback.objects.update_or_create( + series_id=GEOSeries.objects.get(gse=request.data.get("id")), + user_id=request.headers.get("X-User-UUID", ""), + defaults={ + "name": request.data.get("user", {}).get("name", ""), + "email": request.data.get("user", {}).get("email", ""), + "rating": request.data.get("rating"), + "qualities": request.data.get("qualities", []), + "keywords": request.data.get("keywords", {}), + "elaborate": request.data.get("elaborate", ""), + } + ) + return Response({"status": "success"}, status=status.HTTP_201_CREATED) except Exception as e: return Response( @@ -530,6 +624,32 @@ def ontology_search(request): serializer = OntologySearchResultsSerializer(results, many=True) return Response(serializer.data) +# =========================================================================== +# === Database-wide statistics +# =========================================================================== + +@api_view(["GET"]) +@permission_classes([AllowAny]) +def database_statistics(request): + """ + API endpoint for getting statistics about the database. + Accessible at /api/stats/ + + This endpoint mostly returns the contents of SiteStatistic, which is + populated by the populate_site_statistics management command. + """ + + serializer = DatabaseStatsSerializer({ + "tissues": SiteStatistic.objects.get(name="tissues").value, + "diseases": SiteStatistic.objects.get(name="diseases").value, + "studies": SiteStatistic.objects.get(name="studies").value, + "samples": SiteStatistic.objects.get(name="samples").value, + "species": SiteStatistic.objects.get(name="species").value, + "technologies": SiteStatistic.objects.get(name="technologies").value, + "feedback": Feedback.objects.count(), + }, many=False) + return Response(serializer.data) + # =========================================================================== # === Cart share, download views @@ -587,7 +707,7 @@ def create(self, request, *args, **kwargs): series_id = series_data["id"] added_at = series_data.get("added") try: - series = GEOSeries.objects.get(series_id=series_id) + series = GEOSeries.objects.get(gse=series_id) CartItem.objects.create(series=series, added_at=added_at, cart=cart) except GEOSeries.DoesNotExist: continue # skip invalid series ids diff --git a/backend/src/meta2onto/settings.py b/backend/src/meta2onto/settings.py index 0b8868d..5de5989 100644 --- a/backend/src/meta2onto/settings.py +++ b/backend/src/meta2onto/settings.py @@ -38,7 +38,7 @@ def is_truthy(value): DOMAIN = os.environ.get("DOMAIN", "localhost") -ALLOWED_HOSTS = ["localhost", DOMAIN] +ALLOWED_HOSTS = ["localhost", DOMAIN, "meta2onto.org"] CORS_ALLOWED_ORIGINS = [ "http://localhost:3050", @@ -46,6 +46,7 @@ def is_truthy(value): "http://localhost:8051", f"http://{DOMAIN}", f"https://{DOMAIN}", + "https://meta2onto.org", ] CORS_ALLOWED_ORIGIN_REGEXES = [ @@ -186,6 +187,25 @@ def is_truthy(value): DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +VERBOSE_DB_LOGGING = is_truthy(os.environ.get("VERBOSE_DB_LOGGING", "0")) + +if VERBOSE_DB_LOGGING: + LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + }, + }, + "loggers": { + "django.db.backends": { + "handlers": ["console"], + "level": "DEBUG", + }, + }, + } + # add django rest framework settings REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, @@ -207,7 +227,20 @@ def is_truthy(value): # app-specific settings # cache timeout for search results, which are relatively static and can be cached longer-term -# (default currently set to 30 days in seconds) -LONGTERM_CACHE_TIMEOUT = int(os.environ.get("LONGTERM_CACHE_TIMEOUT", str(60 * 60 * 24 * 30))) +# (default currently set to slightly less than 30 days in seconds) +# (note that >= 30 days, the timeout is interpreted as a UNIX timestamp and not a duration from now) +LONGTERM_CACHE_TIMEOUT = int(os.environ.get("LONGTERM_CACHE_TIMEOUT", str(60 * 60 * 24 * 29))) # maximum number of search results to return, which can be overridden by environment variable -SEARCH_MAX_RESULTS = int(os.environ.get("SEARCH_MAX_RESULTS", "1000")) +# (-1 means no limit, which is the default) +SEARCH_MAX_RESULTS = int(os.environ.get("SEARCH_MAX_RESULTS", "-1")) +if SEARCH_MAX_RESULTS < 0: + SEARCH_MAX_RESULTS = None + +# if true, uses django's caching mechanism to cache search results for 30 days (or until memcached is cleared) +# if false, performs search queries directly against the database each time +USE_SEARCH_CACHE = is_truthy(os.environ.get("USE_SEARCH_CACHE", "0")) + +# if true, computes facets per request +# if false, consults the global Facet and FacetEntry tables for precomputed facet values +# over the entire dataset +COMPUTE_FACETS_DYNAMICALLY = is_truthy(os.environ.get("COMPUTE_FACETS_DYNAMICALLY", "0")) diff --git a/compose-envs/docker-compose.dev.yml b/compose-envs/docker-compose.dev.yml index 6371f22..4f36642 100644 --- a/compose-envs/docker-compose.dev.yml +++ b/compose-envs/docker-compose.dev.yml @@ -6,6 +6,7 @@ services: - ./backend/src/:/app/src/ environment: - DJANGO_DEBUG=1 + - VERBOSE_DB_LOGGING=0 frontend: ports: diff --git a/compose-envs/docker-compose.proxied.yml b/compose-envs/docker-compose.proxied.yml index 07ea894..a7f0135 100644 --- a/compose-envs/docker-compose.proxied.yml +++ b/compose-envs/docker-compose.proxied.yml @@ -22,6 +22,7 @@ services: - backend_static:/opt/static environment: - DJANGO_DEBUG=0 + - WORKER_TIMEOUT=120 frontend: command: sh -c "bun install && bun run build --emptyOutDir --outDir /tmp/dist && rm -rf /opt/dist/* && mv /tmp/dist /opt/dist" diff --git a/db-exports/host_load_db.sh b/db-exports/host_load_db.sh index a08649a..c1f4568 100755 --- a/db-exports/host_load_db.sh +++ b/db-exports/host_load_db.sh @@ -1,15 +1,22 @@ #!/usr/bin/env bash -# this script is intended to be run from the host machine, not inside a container +# this script is used to load the database from a dump file on the host machine. +# it uses optimized settings for loading the database, which are different from +# the settings used for running the database in production. -# it does the following: +# the script does the following: # 1. brings down any running containers in the stack # 2. starts the database container with a load-optimized configuration # 3. loads meta2onto_latest.dump from the /db-exports/ directory # 4. brings down the database container +# this script is intended to be run from the host machine, not inside a container + set -euo pipefail +# if 1, skips interactive prompts +NONINTERACTIVE=${NONINTERACTIVE:-"0"} + # check if we're in a container and abort if so if [ -f "/.dockerenv" ]; then echo "Error: This script should be run from the host, not inside a container." @@ -20,11 +27,57 @@ fi SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) cd "${SCRIPT_DIR}/.." +# ================================================================ +# == obtain database dump to initialize db +# ================================================================ + +# Converts bytes value to human-readable string [$1: bytes value] +# from https://unix.stackexchange.com/a/259254 +bytesToHumanReadable() { + local i=${1:-0} d="" s=0 S=("Bytes" "KiB" "MiB" "GiB" "TiB" "PiB" "EiB") + while ((i > 1024 && s < ${#S[@]}-1)); do + printf -v d ".%02d" $((i % 1024 * 100 / 1024)) + i=$((i / 1024)) + s=$((s + 1)) + done + echo "$i$d ${S[$s]}" +} + +LATEST_DUMP_URL="https://storage.googleapis.com/cu-dbmi-meta2onto/meta2onto_latest.dump" + +LAST_DUMP_FILENAME=$( basename "${LATEST_DUMP_URL}" ) +TARGET_DB_DUMP="./db-exports/${LAST_DUMP_FILENAME}" + +REMOTE_FILE_SIZE=$( curl -sI "${LATEST_DUMP_URL}" | grep -i '^Content-Length:' | awk '{print $2}' | tr -d '\r\n' ) +LOCAL_FILE_SIZE=$(wc -c < "$TARGET_DB_DUMP" 2>/dev/null || echo 0) + +# if the local dump either doesn't exist or doesn't match the remote's file size, download it +if [ ! -f "${TARGET_DB_DUMP}" ] || [ "${REMOTE_FILE_SIZE}" -ne "${LOCAL_FILE_SIZE}" ]; then + # if we're in interactive mode, confirm before downloading + if [ "${NONINTERACTIVE}" -eq 0 ]; then + read -p "* A new database dump is available; it will require $( bytesToHumanReadable ${REMOTE_FILE_SIZE} ) of space. Download it now? (y/n) " yn + case $yn in + [Yy]* ) + echo "* Downloading latest database dump from ${LATEST_DUMP_URL}..." + mkdir -p ./db-exports/ + time curl -o "./db-exports/${LAST_DUMP_FILENAME}" -L "${LATEST_DUMP_URL}" + ;; + * ) + echo "* Skipping database dump download" + esac + else + echo "* Non-interactive mode: downloading latest database dump from ${LATEST_DUMP_URL}..." + mkdir -p ./db-exports/ + time curl -o "./db-exports/${LAST_DUMP_FILENAME}" -L "${LATEST_DUMP_URL}" + fi +fi + # bring down any running containers docker compose down # purge the database volume docker volume rm meta2onto_postgres_data 2>/dev/null || true + # start the database container with a load-optimized configuration # (this will block until the load is complete) time ( @@ -36,6 +89,8 @@ time ( export PGCONFIG_PATH="./services/postgres/configs/postgresql_load.conf" fi + echo "* Loading database from dump file using config: ${PGCONFIG_PATH}" + docker compose run --rm -it \ -v ${PGCONFIG_PATH}:/opt/postgresql.conf \ db \ @@ -44,3 +99,5 @@ time ( # when it's done, bring down the stack again docker compose down + +echo "* Database load complete. You can now start the stack with ./run_stack.sh" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d802980..18ccd25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,7 @@ services: - backend memcached: + command: ["memcached", "-m", "1024", "-I", "5m"] image: memcached:latest db: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/frontend/.env b/frontend/.env index 4ca55a5..6bcdcec 100755 --- a/frontend/.env +++ b/frontend/.env @@ -1,5 +1,8 @@ VITE_TITLE=Meta2Onto VITE_DESCRIPTION=Discover Human Transcriptomics Data. Search millions of annotated samples across major databases. -VITE_URL=https://meta2onto.cu-dbmi.dev -VITE_API=https://meta2onto.cu-dbmi.dev/api -VITE_LAB=Krishnan Lab \ No newline at end of file +VITE_URL=https://meta2onto.org +VITE_API=https://meta2onto.org/api +VITE_LAB=Krishnan Lab +VITE_TXT2ONTO=https://academic.oup.com/bib/article/26/1/bbae652/7930339 +VITE_REPO=https://github.com/krishnanlab/meta2onto +VITE_EMAIL=arjun.krishnan@cuanschutz.edu diff --git a/frontend/.prettierrc b/frontend/.prettierrc index 61b1143..3f05b01 100755 --- a/frontend/.prettierrc +++ b/frontend/.prettierrc @@ -1,4 +1,5 @@ { + "htmlWhitespaceSensitivity": "strict", "overrides": [ { "files": "*.css", @@ -31,8 +32,7 @@ "^../" ], "importOrderParserPlugins": ["typescript", "jsx", "importAssertions"], - "jsdocCapitalizeDescription": false, - "htmlWhitespaceSensitivity": "strict" + "jsdocCapitalizeDescription": false } } ] diff --git a/frontend/Dockerfile b/frontend/Dockerfile index b3bdfd4..cd8129b 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -5,7 +5,7 @@ FROM oven/bun:1 AS base WORKDIR /app # Copy package management files -COPY package.json bun.lock ./ +COPY package.json ./ # Install dependencies using Bun RUN bun install diff --git a/frontend/bun.lock b/frontend/bun.lock index 38d8015..a210869 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -4,175 +4,122 @@ "workspaces": { "": { "dependencies": { - "@base-ui/react": "^1.1.0", + "@base-ui/react": "^1.6.0", "@fontsource-variable/outfit": "^5.2.8", "@fontsource-variable/sometype-mono": "^5.2.7", - "@reactuses/core": "^6.1.11", - "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-query": "^5.90.19", + "@reactuses/core": "^6.3.3", + "@tailwindcss/vite": "^4.3.1", + "@tanstack/react-query": "^5.101.1", "@tanstack/react-table": "^8.21.3", "clsx": "^2.1.1", - "gsap": "^3.14.2", - "highlight.js": "^11.11.1", - "javascript-time-ago": "^2.5.12", - "jotai": "^2.16.2", - "lodash": "^4.17.21", - "lucide-react": "^0.562.0", - "react": "^19.2.3", + "gsap": "^3.15.0", + "javascript-time-ago": "^2.6.4", + "jotai": "^2.20.1", + "lodash": "^4.18.1", + "lucide-react": "^1.21.0", + "react": "^19.2.7", "react-children-utilities": "^2.10.0", - "react-dom": "^19.2.3", + "react-dom": "^19.2.7", "react-ga4": "^3.0.1", "react-highlight-words": "^0.21.0", - "react-router": "^7.12.0", - "react-time-ago": "^7.3.5", + "react-router": "^8.0.1", + "react-time-ago": "^7.4.4", "seedrandom": "^3.0.5", - "tailwindcss": "^4.1.18", - "zod": "^4.3.6", + "tailwindcss": "^4.3.1", + "zod": "^4.4.3", }, "devDependencies": { - "@eslint/js": "^9.39.2", - "@ianvs/prettier-plugin-sort-imports": "^4.7.0", - "@types/lodash": "^4.17.23", - "@types/react": "^19.2.9", + "@eslint/js": "^10.0.1", + "@ianvs/prettier-plugin-sort-imports": "^4.7.1", + "@types/eslint-plugin-jsx-a11y": "^6.10.1", + "@types/lodash": "^4.17.24", + "@types/node": "^26.0.0", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/react-highlight-words": "^0.20.1", "@types/seedrandom": "^3.0.8", - "@vitejs/plugin-react": "^5.1.2", - "eslint": "^9.39.2", + "@vitejs/plugin-react": "^6.0.3", + "eslint": "^10.5.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-better-tailwindcss": "^4.0.1", + "eslint-plugin-better-tailwindcss": "^4.6.0", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-prettier": "^5.5.5", - "eslint-plugin-react-hooks": "^7.0.1", - "globals": "^17.0.0", - "msw": "^2.12.7", - "prettier": "^3.8.0", - "prettier-plugin-jsdoc": "^1.8.0", - "prettier-plugin-tailwindcss": "^0.7.2", - "type-fest": "^5.4.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.53.1", - "vite": "^7.3.1", - "vite-plugin-svgr": "^4.5.0", + "eslint-plugin-prettier": "^5.5.6", + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.7.0", + "prettier": "^3.8.4", + "prettier-plugin-jsdoc": "^1.8.1", + "prettier-plugin-tailwindcss": "^0.8.0", + "type-fest": "^5.7.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.62.0", + "vite": "^8.1.0", }, }, }, "packages": { - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + "@base-ui/react": ["@base-ui/react@1.6.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.3.1", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw=="], - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + "@base-ui/utils": ["@base-ui/utils@0.3.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "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" }, "optionalPeers": ["@types/react"] }, "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg=="], - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - "@base-ui/react": ["@base-ui/react@1.4.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.2.8", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - "@base-ui/utils": ["@base-ui/utils@0.2.8", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], - "@eslint/css-tree": ["@eslint/css-tree@4.0.2", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-eqSkC3mka2tiqOuPZKqvxNJoRzpxMss3Np3Yqi4sW7nTTRCpTKB2hzrY4JRsi0ZP3QbVfp23sgEm7VCoOjesmw=="], + "@eslint/css-tree": ["@eslint/css-tree@4.0.4", "", { "dependencies": { "mdn-data": "2.28.1", "source-map-js": "^1.2.1" } }, "sha512-nxMparyhqVWQvadx9x8dIfubfIPOE+X2b2waua8fzdnM9vdp9rgVtwEZlG0TmCwEUz/d/f40fzvO/eqBwdxz0A=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], - "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="], "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], @@ -198,16 +145,6 @@ "@ianvs/prettier-plugin-sort-imports": ["@ianvs/prettier-plugin-sort-imports@4.7.1", "", { "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "semver": "^7.5.2" }, "peerDependencies": { "@prettier/plugin-oxc": "^0.0.4 || ^0.1.0", "@vue/compiler-sfc": "2.7.x || 3.x", "content-tag": "^4.0.0", "prettier": "2 || 3 || ^4.0.0-0", "prettier-plugin-ember-template-tag": "^2.1.0" }, "optionalPeers": ["@prettier/plugin-oxc", "@vue/compiler-sfc", "content-tag", "prettier-plugin-ember-template-tag"] }, "sha512-jmTNYGlg95tlsoG3JLCcuC4BrFELJtLirLAkQW/71lXSyOhVt/Xj7xWbbGcuVbNq1gwWgSyMrPjJc9Z30hynVw=="], - "@inquirer/ansi": ["@inquirer/ansi@2.0.5", "", {}, "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw=="], - - "@inquirer/confirm": ["@inquirer/confirm@6.0.12", "", { "dependencies": { "@inquirer/core": "^11.1.9", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og=="], - - "@inquirer/core": ["@inquirer/core@11.1.9", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg=="], - - "@inquirer/figures": ["@inquirer/figures@2.0.5", "", {}, "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ=="], - - "@inquirer/type": ["@inquirer/type@4.0.5", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -220,145 +157,93 @@ "@microsoft/fetch-event-source": ["@microsoft/fetch-event-source@2.0.1", "", {}, "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="], - "@mswjs/interceptors": ["@mswjs/interceptors@0.41.6", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-qmDvJIjcNsZ6tXWy2G9yuCgMPTTn35GMA3dPpSLm7QJVpbQzYdw0ALy1bKoivXnEM3U93/OrK+/M719b+fg84Q=="], - - "@open-draft/deferred-promise": ["@open-draft/deferred-promise@3.0.0", "", {}, "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA=="], - - "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], - "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@oxc-project/types": ["@oxc-project/types@0.137.0", "", {}, "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA=="], - "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], + "@pkgr/core": ["@pkgr/core@0.3.6", "", {}, "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA=="], - "@reactuses/core": ["@reactuses/core@6.3.1", "", { "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", "js-cookie": "^3.0.5", "lodash-es": "^4.17.21", "screenfull": "^5.0.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "qrcode": "^1.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["qrcode"] }, "sha512-UOEzmpFwatGroooV0VW9ygr138qX9MnplY0XeLBPTpAUGEo2lUrPz6qtURnLT64KYu0FwOCV6lwkMkMwrcP42g=="], + "@reactuses/core": ["@reactuses/core@6.3.3", "", { "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", "js-cookie": "^3.0.5", "lodash-es": "^4.17.21", "screenfull": "^5.0.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "qrcode": "^1.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["qrcode"] }, "sha512-BrbySILdZqCAY38Zy2Ly76Wp36CzXsgZR8anGX9p7URUjAObci2vivyIzyQNLfKRgrA+MT5V4F1YYgY5o2g2Gg=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.2", "", { "os": "android", "cpu": "arm64" }, "sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA=="], - "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.2", "", { "os": "android", "cpu": "arm64" }, "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.2", "", { "os": "linux", "cpu": "arm" }, "sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.2", "", { "os": "none", "cpu": "arm64" }, "sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.2", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.5" }, "cpu": "none" }, "sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.1" } }, "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.1", "@tailwindcss/oxide-darwin-arm64": "4.3.1", "@tailwindcss/oxide-darwin-x64": "4.3.1", "@tailwindcss/oxide-freebsd-x64": "4.3.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", "@tailwindcss/oxide-linux-x64-musl": "4.3.1", "@tailwindcss/oxide-wasm32-wasi": "4.3.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.1", "", { "os": "android", "cpu": "arm64" }, "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.2", "", { "os": "none", "cpu": "arm64" }, "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ=="], - "@svgr/babel-plugin-add-jsx-attribute": ["@svgr/babel-plugin-add-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.1", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA=="], - "@svgr/babel-plugin-remove-jsx-attribute": ["@svgr/babel-plugin-remove-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg=="], - "@svgr/babel-plugin-remove-jsx-empty-expression": ["@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA=="], - "@svgr/babel-plugin-replace-jsx-attribute-value": ["@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.1", "", { "dependencies": { "@tailwindcss/node": "4.3.1", "@tailwindcss/oxide": "4.3.1", "tailwindcss": "4.3.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ=="], - "@svgr/babel-plugin-svg-dynamic-title": ["@svgr/babel-plugin-svg-dynamic-title@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og=="], + "@tanstack/query-core": ["@tanstack/query-core@5.101.1", "", {}, "sha512-Y6Y92dkXtNqx67m2pMSxUsA3zOCwv862JexZRP8/EPwvKXMPu9m8rv43spiXWzOUIggQ3SQApttALStzhA8B4g=="], - "@svgr/babel-plugin-svg-em-dimensions": ["@svgr/babel-plugin-svg-em-dimensions@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g=="], - - "@svgr/babel-plugin-transform-react-native-svg": ["@svgr/babel-plugin-transform-react-native-svg@8.1.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q=="], - - "@svgr/babel-plugin-transform-svg-component": ["@svgr/babel-plugin-transform-svg-component@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw=="], - - "@svgr/babel-preset": ["@svgr/babel-preset@8.1.0", "", { "dependencies": { "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", "@svgr/babel-plugin-transform-svg-component": "8.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug=="], - - "@svgr/core": ["@svgr/core@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "camelcase": "^6.2.0", "cosmiconfig": "^8.1.3", "snake-case": "^3.0.4" } }, "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA=="], - - "@svgr/hast-util-to-babel-ast": ["@svgr/hast-util-to-babel-ast@8.0.0", "", { "dependencies": { "@babel/types": "^7.21.3", "entities": "^4.4.0" } }, "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q=="], - - "@svgr/plugin-jsx": ["@svgr/plugin-jsx@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "@svgr/hast-util-to-babel-ast": "8.0.0", "svg-parser": "^2.0.4" }, "peerDependencies": { "@svgr/core": "*" } }, "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA=="], - - "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="], - - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="], - - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="], - - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="], - - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="], - - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="], - - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="], - - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="], - - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="], - - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="], - - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="], - - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="], - - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="], - - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="], - - "@tailwindcss/vite": ["@tailwindcss/vite@4.2.4", "", { "dependencies": { "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw=="], - - "@tanstack/query-core": ["@tanstack/query-core@5.100.5", "", {}, "sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg=="], - - "@tanstack/react-query": ["@tanstack/react-query@5.100.5", "", { "dependencies": { "@tanstack/query-core": "5.100.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA=="], + "@tanstack/react-query": ["@tanstack/react-query@5.101.1", "", { "dependencies": { "@tanstack/query-core": "5.101.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-ZnONUuQKJe1bJMStXUL1s5uKN9FcfC28j5cK+iDZcdSHtUv1wtin1cGc/Oewhf2Oc4eKY7lggtpvT/AbMmhHew=="], "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/eslint-plugin-jsx-a11y": ["@types/eslint-plugin-jsx-a11y@6.10.1", "", { "dependencies": { "eslint": "^9" } }, "sha512-5RtuPVe0xz8BAhrkn2oww6Uw885atf962Q4fqZo48QdO3EQA7oCEDSXa6optgJ1ZMds3HD9ITK5bfm4AWuoXFQ=="], - "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -368,9 +253,9 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -378,44 +263,38 @@ "@types/seedrandom": ["@types/seedrandom@3.0.8", "", {}, "sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ=="], - "@types/set-cookie-parser": ["@types/set-cookie-parser@2.4.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw=="], - - "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/type-utils": "8.59.0", "@typescript-eslint/utils": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/type-utils": "8.62.0", "@typescript-eslint/utils": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.62.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.0", "@typescript-eslint/types": "^8.59.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.62.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.62.0", "@typescript-eslint/types": "^8.62.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0" } }, "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0" } }, "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.62.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/utils": "8.59.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/utils": "8.62.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.62.0", "", {}, "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.0", "@typescript-eslint/tsconfig-utils": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.62.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.62.0", "@typescript-eslint/tsconfig-utils": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.62.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ=="], - "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], + "@valibot/to-json-schema": ["@valibot/to-json-schema@1.7.1", "", { "peerDependencies": { "valibot": "^1.4.0" } }, "sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="], - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -438,19 +317,19 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "axe-core": ["axe-core@4.11.3", "", {}, "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg=="], + "axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.23", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.38", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw=="], "binary-searching": ["binary-searching@2.0.5", "", {}, "sha512-v4N2l3RxL+m4zDxyxz3Ne2aTmiPn8ZUpKFpdPtO+ItW1NcTCXA7JeHG5GMBSvoKSkQZ9ycS+EouDVxYB9ufKWA=="], - "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + "browserslist": ["browserslist@4.28.4", "", { "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", "electron-to-chromium": "^1.5.376", "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw=="], "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], @@ -460,33 +339,25 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "comment-parser": ["comment-parser@1.4.6", "", {}, "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg=="], + "comment-parser": ["comment-parser@1.4.7", "", {}, "sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - - "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -500,7 +371,7 @@ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], @@ -516,57 +387,51 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "electron-to-chromium": ["electron-to-chromium@1.5.344", "", {}, "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.378", "", {}, "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], - - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + "enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="], "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], + "es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], - "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - - "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "es-to-primitive": ["es-to-primitive@1.3.1", "", { "dependencies": { "es-abstract-get": "^1.0.0", "es-errors": "^1.3.0", "is-callable": "^1.2.7", "is-date-object": "^1.1.0", "is-symbol": "^1.1.1" } }, "sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + "eslint": ["eslint@10.5.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ=="], "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], - "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@4.4.1", "", { "dependencies": { "@eslint/css-tree": "^4.0.1", "@valibot/to-json-schema": "^1.6.0", "enhanced-resolve": "^5.20.1", "jiti": "^2.6.1", "synckit": "^0.11.12", "tailwind-csstree": "^0.3.0", "tsconfig-paths-webpack-plugin": "^4.2.0", "valibot": "^1.3.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "oxlint": "^1.35.0", "tailwindcss": "^3.3.0 || ^4.1.17" }, "optionalPeers": ["eslint", "oxlint"] }, "sha512-ueFciTgj2M+4YklYdtvpbMA3Nn22z60sQoSA4bnctOP4h0daUhJKAsDaGi888N00qWtIUqeK5Ikt6xnNnHPg2g=="], + "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@4.6.0", "", { "dependencies": { "@eslint/css-tree": "^4.0.1", "@valibot/to-json-schema": "^1.6.0", "enhanced-resolve": "^5.20.1", "jiti": "^2.6.1", "synckit": "^0.11.12", "tailwind-csstree": "^0.3.2", "tsconfig-paths-webpack-plugin": "^4.2.0", "valibot": "^1.3.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "oxlint": "^1.35.0", "tailwindcss": "^3.3.0 || ^4.1.17" }, "optionalPeers": ["eslint", "oxlint"] }, "sha512-Xeh/6KzLeays6jXSqTKx6iCdJL1qo5ant/mAabbz7dJqADYebKUUPR2zXp1xuiqqJ3HIm+MN+Ql2m4Jp0BxkMQ=="], "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], - "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.1", "synckit": "^0.11.12" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw=="], + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.6", "", { "dependencies": { "prettier-linter-helpers": "^1.0.1", "synckit": "^0.11.13" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], @@ -574,8 +439,6 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -586,12 +449,6 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], - - "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], - - "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], @@ -608,7 +465,7 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + "function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "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", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="], "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], @@ -616,8 +473,6 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -626,7 +481,7 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@17.5.0", "", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="], + "globals": ["globals@17.7.0", "", {}, "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], @@ -634,8 +489,6 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "graphql": ["graphql@16.13.2", "", {}, "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="], - "gsap": ["gsap@3.15.0", "", {}, "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -650,9 +503,7 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], - - "headers-polyfill": ["headers-polyfill@5.0.1", "", { "dependencies": { "@types/set-cookie-parser": "^2.4.10", "set-cookie-parser": "^3.0.1" } }, "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], @@ -660,8 +511,6 @@ "highlight-words-core": ["highlight-words-core@1.2.3", "", {}, "sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ=="], - "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -672,8 +521,6 @@ "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], @@ -686,12 +533,12 @@ "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + "is-document.all": ["is-document.all@1.0.0", "", { "dependencies": { "call-bound": "^1.0.4" } }, "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], @@ -700,8 +547,6 @@ "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], - "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], - "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], @@ -728,22 +573,20 @@ "javascript-time-ago": ["javascript-time-ago@2.6.4", "", { "dependencies": { "relative-time-format": "^1.1.12" } }, "sha512-7K/Z37LuwVaxxjutUDd1pXpznufPcox0b1UYu00ksAMMlV6IsxIvduwL3kgfPxuBVF8jVj7nhrKMPDslMq94aQ=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jotai": ["jotai@2.19.1", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-sqm9lVZiqBHZH8aSRk32DSiZDHY3yUIlulXYn9GQj7/LvoUdYXSMti7ZPJGo+6zjzKFt5a25k/I6iBCi43PJcw=="], + "jotai": ["jotai@2.20.1", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-dnuKfU/GLi8B28RRMjQ3AfoN7kfzP8o41+AX2FmITZqEMY8PHnjABq+VkEooomLwYaGjda+pgy0yFSjaHX/ZPg=="], - "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], + "js-cookie": ["js-cookie@3.0.8", "", {}, "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], @@ -784,8 +627,6 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], @@ -796,11 +637,9 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], + "lucide-react": ["lucide-react@1.21.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -810,7 +649,7 @@ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], - "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + "mdn-data": ["mdn-data@2.28.1", "", {}, "sha512-U9w+PzSZ00Z5m9rZ5ARVFL5xOfuCHdKYi/1RRwDCJsboFgJDNT3zT6PIPD7mZQYaQLhsZM3GfDRgSMRHhSmVng=="], "memoize-one": ["memoize-one@4.1.0", "", {}, "sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA=="], @@ -856,23 +695,17 @@ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msw": ["msw@2.13.6", "", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.7", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-GAJbQy8Ra/Ydjt0Hb2MGT2qhzd83J3+QZMHdH85uW7r/XkKc846+Ma2PLif5hGvTm5Yqa+wkcstpim0WeLZU9g=="], - - "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], - - "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + "node-releases": ["node-releases@2.0.48", "", {}, "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -888,8 +721,6 @@ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -898,43 +729,37 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], - - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.12", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA=="], + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + "prettier": ["prettier@3.8.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q=="], "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], - "prettier-plugin-jsdoc": ["prettier-plugin-jsdoc@1.8.0", "", { "dependencies": { "binary-searching": "^2.0.5", "comment-parser": "^1.4.0", "mdast-util-from-markdown": "^2.0.0" }, "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-byW8EBZ1DSA3CPdDGBXfcdqqhh2eq0+HlIOPTGZ6rf9O2p/AwBmtS0e49ot5ZeOdcszj81FyzbyHr/VS0eYpCg=="], + "prettier-plugin-jsdoc": ["prettier-plugin-jsdoc@1.8.1", "", { "dependencies": { "binary-searching": "^2.0.5", "comment-parser": "^1.4.0", "mdast-util-from-markdown": "^2.0.0" }, "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-XuMqBWTc3b/8eCOe+OlZlFy9Z413a7WOmF4i5hDGtjbtIFOdvRrVtGjXR2Feye3TrLWhkkkHheNXPTyYKxw3nA=="], - "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.4", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-UKii4RjY05SNt/WQi6/NcOn/LsT0/ILLXsxygjbRg5/YZelsSu5jTqorYHPDGq4nZy5q5hpCu+XdGZ1xaJEQgw=="], + "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.8.0", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-V8ITGH87yuBDF6JpEZTOVlUz/saAwqb8f3HRgUj8Lh+tGCcrmorhsLpYqzygwFwK0PE2Ib6Mv3M7T/uE2tZV1g=="], "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], "react-children-utilities": ["react-children-utilities@2.10.0", "", { "peerDependencies": { "react": ">=15" } }, "sha512-9naSkrOHACjoDsHDtAQR5mGMp3ffv1DkUSQPDa8iJFP0+lXloS4fw44hMHaWeNF3qL4B3GPiJ9032Oo309oa9g=="], - "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], "react-ga4": ["react-ga4@3.0.1", "", {}, "sha512-GyCc01bSheWXjzGDyHsXMOqk/SP5Cf/JrcJTg4hcpKx4eeSwaJKpJUc+ipF4ffLTZkmabmf3ZGBv4OKHTXNXyA=="], @@ -942,9 +767,7 @@ "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - - "react-router": ["react-router@7.14.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw=="], + "react-router": ["react-router@8.0.1", "", { "dependencies": { "cookie-es": "^3.1.1" }, "peerDependencies": { "react": ">=19.2.7", "react-dom": ">=19.2.7" }, "optionalPeers": ["react-dom"] }, "sha512-5EL/fANovVUhRK50NLS8RYfX0BxrimoKsHWUPPy8v5UEl8i6vzF7e4POo3u+AhPItDwccUAJjMfIOmydxBJmQw=="], "react-time-ago": ["react-time-ago@7.4.4", "", { "dependencies": { "javascript-time-ago": "^2.3.7", "memoize-one": "^6.0.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=0.16.8", "react-dom": ">=0.16.8" } }, "sha512-g1qqiEgIYgKvK6ySxyVq9nZ/s5mKbugY5fsgc0lB9suwVpbj58f47T578IqSkyzbe+kO6pk1ITmfTOTM8VizRw=="], @@ -954,15 +777,11 @@ "relative-time-format": ["relative-time-format@1.1.12", "", {}, "sha512-qaZBjmRIuXLfuLnzgqpFdBPa5W0euSX1tMnoMUHGPphLwJmrt8xbNiOIHrlvYOD6oNJ0M5owPCZyPibI8de5pQ=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "rettime": ["rettime@0.11.8", "", {}, "sha512-0fERGXktJTyJ+h8fBEiPxHPEFOu0h15JY7JtwrOVqR5K+vb99ho6IyOo7ekLS3h4sJCzIDy4VWKIbZUfe9njmg=="], - - "rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="], + "rolldown": ["rolldown@1.1.2", "", { "dependencies": { "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.2", "@rolldown/binding-darwin-arm64": "1.1.2", "@rolldown/binding-darwin-x64": "1.1.2", "@rolldown/binding-freebsd-x64": "1.1.2", "@rolldown/binding-linux-arm-gnueabihf": "1.1.2", "@rolldown/binding-linux-arm64-gnu": "1.1.2", "@rolldown/binding-linux-arm64-musl": "1.1.2", "@rolldown/binding-linux-ppc64-gnu": "1.1.2", "@rolldown/binding-linux-s390x-gnu": "1.1.2", "@rolldown/binding-linux-x64-gnu": "1.1.2", "@rolldown/binding-linux-x64-musl": "1.1.2", "@rolldown/binding-openharmony-arm64": "1.1.2", "@rolldown/binding-wasm32-wasi": "1.1.2", "@rolldown/binding-win32-arm64-msvc": "1.1.2", "@rolldown/binding-win32-x64-msvc": "1.1.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ=="], "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], @@ -976,9 +795,7 @@ "seedrandom": ["seedrandom@3.0.5", "", {}, "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -990,7 +807,7 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], @@ -998,55 +815,35 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], - "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "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" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + "string.prototype.trim": ["string.prototype.trim@1.2.11", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-object-atoms": "^1.1.2", "has-property-descriptors": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w=="], - "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + "string.prototype.trimend": ["string.prototype.trimend@1.0.10", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.2" } }, "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw=="], "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "svg-parser": ["svg-parser@2.0.4", "", {}, "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="], - - "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="], + "synckit": ["synckit@0.11.13", "", { "dependencies": { "@pkgr/core": "^0.3.6" } }, "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], - "tailwind-csstree": ["tailwind-csstree@0.3.1", "", { "peerDependencies": { "@eslint/css": ">=1.0.0" }, "optionalPeers": ["@eslint/css"] }, "sha512-v147gLOR+E+9H4dNaP9rBeS/S/CTQJMRItlX9jLOXjdBGfSRauLwiz7LBCViaQmn6URXIlOdN6iMzSzOaeoUUw=="], + "tailwind-csstree": ["tailwind-csstree@0.3.3", "", { "peerDependencies": { "@eslint/css": ">=1.0.0" }, "optionalPeers": ["@eslint/css"] }, "sha512-je9J5UYRsTJqAjYrIBMMlge8T/rreRd44pJxgG5Zx/zeo4kAC/liUKqzztRZrGlYRJLvIf2Cb1DVJMTXSzEShA=="], - "tailwindcss": ["tailwindcss@4.2.4", "", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="], + "tailwindcss": ["tailwindcss@4.3.1", "", {}, "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], - - "tldts": ["tldts@7.0.28", "", { "dependencies": { "tldts-core": "^7.0.28" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw=="], - - "tldts-core": ["tldts-core@7.0.28", "", {}, "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ=="], - - "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], @@ -1058,7 +855,7 @@ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "type-fest": ["type-fest@5.6.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="], + "type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -1066,31 +863,27 @@ "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - "typed-array-length": ["typed-array-length@1.0.7", "", { "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" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "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" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.59.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.0", "@typescript-eslint/parser": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/utils": "8.59.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw=="], + "typescript-eslint": ["typescript-eslint@8.62.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.62.0", "@typescript-eslint/parser": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/utils": "8.62.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], - "valibot": ["valibot@1.3.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg=="], - - "vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], + "valibot": ["valibot@1.4.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g=="], - "vite-plugin-svgr": ["vite-plugin-svgr@4.5.0", "", { "dependencies": { "@rollup/pluginutils": "^5.2.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, "peerDependencies": { "vite": ">=2.6.0" } }, "sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA=="], + "vite": ["vite@8.1.0", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "~1.1.2", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -1100,23 +893,15 @@ "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], @@ -1126,36 +911,64 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@eslint/eslintrc/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "@mswjs/interceptors/@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], + "@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@types/eslint-plugin-jsx-a11y/eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "eslint-plugin-jsx-a11y/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "react-time-ago/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="], - "headers-polyfill/set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - "react-time-ago/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="], + "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], + + "@types/eslint-plugin-jsx-a11y/eslint/@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + + "@types/eslint-plugin-jsx-a11y/eslint/@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@types/eslint-plugin-jsx-a11y/eslint/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@types/eslint-plugin-jsx-a11y/eslint/@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + + "@types/eslint-plugin-jsx-a11y/eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@types/eslint-plugin-jsx-a11y/eslint/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "@types/eslint-plugin-jsx-a11y/eslint/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "@types/eslint-plugin-jsx-a11y/eslint/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "@types/eslint-plugin-jsx-a11y/eslint/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "eslint-plugin-jsx-a11y/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], + + "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@types/eslint-plugin-jsx-a11y/eslint/@eslint/config-array/@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], - "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "@types/eslint-plugin-jsx-a11y/eslint/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "eslint-plugin-jsx-a11y/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "@types/eslint-plugin-jsx-a11y/eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index 4eabf1f..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,57 +0,0 @@ -import eslintJs from "@eslint/js"; -import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss"; -import eslintPluginJsxA11y from "eslint-plugin-jsx-a11y"; -import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; -import eslintPluginReactHooks from "eslint-plugin-react-hooks"; -import { defineConfig, globalIgnores } from "eslint/config"; -import globals from "globals"; -import typescriptEslint from "typescript-eslint"; - -export default defineConfig([ - globalIgnores(["dist", "public"]), - eslintJs.configs.recommended, - typescriptEslint.configs.recommended, - eslintPluginPrettierRecommended, - eslintPluginReactHooks.configs.flat.recommended, - eslintPluginJsxA11y.flatConfigs.recommended, - { - plugins: { - "better-tailwindcss": eslintPluginBetterTailwindcss, - }, - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - rules: { - /** GENERAL */ - "prefer-const": ["error", { destructuring: "all" }], - - /** TYPESCRIPT */ - "@typescript-eslint/no-unused-vars": ["warn", { caughtErrors: "none" }], - "@typescript-eslint/consistent-type-definitions": ["error", "type"], - "@typescript-eslint/consistent-type-imports": "error", - - /** ACCESSIBILITY */ - /** https://github.com/dequelabs/axe-core/issues/4566 */ - "jsx-a11y/no-noninteractive-tabindex": ["off"], - /** - * allow but still catch - * - */ - "jsx-a11y/label-has-associated-control": [ - "error", - { controlComponents: ["*"] }, - ], - - /** FORMATTING */ - "prettier/prettier": "warn", - ...eslintPluginBetterTailwindcss.configs["recommended-warn"].rules, - /** https://github.com/schoero/eslint-plugin-better-tailwindcss/issues/302 */ - "better-tailwindcss/enforce-consistent-line-wrapping": [ - "warn", - { strictness: "loose" }, - ], - }, - settings: { "better-tailwindcss": { entryPoint: "src/styles.css" } }, - }, -]); diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts new file mode 100644 index 0000000..6086dea --- /dev/null +++ b/frontend/eslint.config.ts @@ -0,0 +1,96 @@ +import js from "@eslint/js"; +import tailwind from "eslint-plugin-better-tailwindcss"; +import { getDefaultSelectors } from "eslint-plugin-better-tailwindcss/defaults"; +import { + MatcherType, + SelectorKind, +} from "eslint-plugin-better-tailwindcss/types"; +import prettier from "eslint-plugin-prettier/recommended"; +import reactHooks from "eslint-plugin-react-hooks"; +import { defineConfig, globalIgnores } from "eslint/config"; +import globals from "globals"; +import tslint from "typescript-eslint"; + +const tailwindSelectors = [ + ...getDefaultSelectors(), + { + kind: SelectorKind.Callee, + name: "^column$", + match: [{ type: MatcherType.ObjectValue, path: "^className$" }], + }, +]; + +export default defineConfig([ + globalIgnores(["dist", "public"]), + + { + name: "TypeScript", + extends: tslint.configs.recommended, + rules: { + "@typescript-eslint/no-unused-vars": ["warn", { caughtErrors: "none" }], + "@typescript-eslint/consistent-type-definitions": ["error", "type"], + "@typescript-eslint/consistent-type-imports": "error", + }, + }, + + { + name: "JavaScript", + files: ["**/*.{ts,tsx,js,jsx}"], + ...js.configs.recommended, + rules: { + "prefer-const": ["error", { destructuring: "all" }], + }, + }, + + { + name: "React Hooks", + extends: [reactHooks.configs.flat.recommended], + }, + + { + name: "Prettier", + extends: [prettier], + rules: { + "prettier/prettier": "warn", + }, + }, + + { + name: "Tailwind", + files: ["**/*.{ts,tsx,js,jsx}"], + extends: [tailwind.configs.recommended], + rules: { + "better-tailwindcss/enforce-consistent-class-order": [ + "warn", + { selectors: tailwindSelectors }, + ], + "better-tailwindcss/enforce-consistent-line-wrapping": [ + "warn", + { + preferSingleLine: true, + group: "never", + printWidth: 0, + selectors: tailwindSelectors, + }, + ], + "better-tailwindcss/no-unknown-classes": [ + "warn", + { ignore: ["^animate-"], selectors: tailwindSelectors }, + ], + "better-tailwindcss/no-unnecessary-whitespace": [ + "warn", + { selectors: tailwindSelectors }, + ], + }, + settings: { + "better-tailwindcss": { entryPoint: "./src/styles.css" }, + }, + }, + + { + languageOptions: { + globals: globals.browser, + ecmaVersion: 2020, + }, + }, +]); diff --git a/frontend/package.json b/frontend/package.json index db436e2..2bce909 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,64 +9,58 @@ "test:types": "tsc -b", "test:lint": "eslint .", "test:format": "prettier --check .", - "test": "bun run test:types && bun run test:lint && bun run test:format", + "test": "bun run test:types && bun run test:lint && bun run test:format && bun run build", "clean": "rm -rf node_modules dist bun.lock && bun pm cache rm" }, "dependencies": { - "@base-ui/react": "^1.1.0", + "@base-ui/react": "^1.6.0", "@fontsource-variable/outfit": "^5.2.8", "@fontsource-variable/sometype-mono": "^5.2.7", - "@reactuses/core": "^6.1.11", - "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-query": "^5.90.19", + "@reactuses/core": "^6.3.3", + "@tailwindcss/vite": "^4.3.1", + "@tanstack/react-query": "^5.101.1", "@tanstack/react-table": "^8.21.3", "clsx": "^2.1.1", - "gsap": "^3.14.2", - "highlight.js": "^11.11.1", - "javascript-time-ago": "^2.5.12", - "jotai": "^2.16.2", - "lodash": "^4.17.21", - "lucide-react": "^0.562.0", - "react": "^19.2.3", + "gsap": "^3.15.0", + "javascript-time-ago": "^2.6.4", + "jotai": "^2.20.1", + "lodash": "^4.18.1", + "lucide-react": "^1.21.0", + "react": "^19.2.7", "react-children-utilities": "^2.10.0", - "react-dom": "^19.2.3", + "react-dom": "^19.2.7", "react-ga4": "^3.0.1", "react-highlight-words": "^0.21.0", - "react-router": "^7.12.0", - "react-time-ago": "^7.3.5", + "react-router": "^8.0.1", + "react-time-ago": "^7.4.4", "seedrandom": "^3.0.5", - "tailwindcss": "^4.1.18", - "zod": "^4.3.6" + "tailwindcss": "^4.3.1", + "zod": "^4.4.3" }, "devDependencies": { - "@eslint/js": "^9.39.2", - "@ianvs/prettier-plugin-sort-imports": "^4.7.0", - "@types/lodash": "^4.17.23", - "@types/react": "^19.2.9", + "@eslint/js": "^10.0.1", + "@ianvs/prettier-plugin-sort-imports": "^4.7.1", + "@types/eslint-plugin-jsx-a11y": "^6.10.1", + "@types/lodash": "^4.17.24", + "@types/node": "^26.0.0", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/react-highlight-words": "^0.20.1", "@types/seedrandom": "^3.0.8", - "@vitejs/plugin-react": "^5.1.2", - "eslint": "^9.39.2", + "@vitejs/plugin-react": "^6.0.3", + "eslint": "^10.5.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-better-tailwindcss": "^4.0.1", + "eslint-plugin-better-tailwindcss": "^4.6.0", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-prettier": "^5.5.5", - "eslint-plugin-react-hooks": "^7.0.1", - "globals": "^17.0.0", - "msw": "^2.12.7", - "prettier": "^3.8.0", - "prettier-plugin-jsdoc": "^1.8.0", - "prettier-plugin-tailwindcss": "^0.7.2", - "type-fest": "^5.4.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.53.1", - "vite": "^7.3.1", - "vite-plugin-svgr": "^4.5.0" - }, - "msw": { - "workerDirectory": [ - "public" - ] + "eslint-plugin-prettier": "^5.5.6", + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.7.0", + "prettier": "^3.8.4", + "prettier-plugin-jsdoc": "^1.8.1", + "prettier-plugin-tailwindcss": "^0.8.0", + "type-fest": "^5.7.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.62.0", + "vite": "^8.1.0" } } diff --git a/frontend/public/404.html b/frontend/public/404.html index 0f58f06..2ac2f3e 100755 --- a/frontend/public/404.html +++ b/frontend/public/404.html @@ -3,7 +3,7 @@