diff --git a/.flake8 b/.flake8 index 5229a182..b297378e 100644 --- a/.flake8 +++ b/.flake8 @@ -11,4 +11,5 @@ exclude = env, base/, redis_data/, - .vagrant/ + .vagrant/, + frontend/node_modules/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0dcf695e..c8f76209 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-24.04 env: - PYTHONPATH: ${{ github.workspace }}:${{ github.workspace }}/web + PYTHONPATH: ${{ github.workspace }}:${{ github.workspace }}/backend steps: - name: Checkout code @@ -27,7 +27,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pip install -r tests/requirements.txt - pip install -r web/requirements.txt + pip install -r backend/requirements.txt - name: Run tests run: | diff --git a/.gitignore b/.gitignore index 6ae14d27..fe2d8e23 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,12 @@ dump.rdb # base directory base + +# TypeScript incremental build info +*.tsbuildinfo + +# Node / frontend build artifacts +node_modules/ + +# Generated from repo-root schemas/config by Vite +frontend/public/schemas/ diff --git a/README.md b/README.md index b4302adf..f4f3fb97 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,12 @@ To minimize setup overhead and enhance ease of use, running this application in sudo docker compose up -d ``` + This starts Redis, the backend API, the builder, and the frontend. Only the frontend is published on the host; nginx serves the UI and proxies `/api` to the backend. + **Note:** When starting the application for the first time, it takes some time to initialize the ArduPilot Git repositories at the backend. This process also involves populating the list of available versions and releases using the GitHub API, so please be patient. 5. **Access the Web Interface:** - The application binds to port 11080 on your host machine by default. Open your web browser and go to `http://localhost:11080` to interact with the web interface. To change the port, set the `WEB_PORT` environment variable in the .env file mentioned in the _Configure Environment Variables_ section. + The frontend binds to port 11080 on your host machine by default. Open your web browser and go to `http://localhost:11080` to interact with the web interface. To change the port, set the `WEB_PORT` environment variable in the `.env` file mentioned in the _Configure Environment Variables_ section. 6. **Stopping the Application:** To stop the application, you can use the following command: @@ -61,7 +63,7 @@ To minimize setup overhead and enhance ease of use, running this application in This will stop and remove the containers, but it will not delete any built images or volumes, preserving your data for future use. ## Running Locally Without Docker on Ubuntu -To run the ArduPilot Custom Firmware Builder locally without Docker, ensure you have an environment capable of building ArduPilot. Refer to the [ArduPilot Environment Setup Guide](https://ardupilot.org/dev/docs/building-setup-linux.html) if necessary. +This setup is intended for **local development** only, not production. Ensure you have an environment capable of building ArduPilot. Refer to the [ArduPilot Environment Setup Guide](https://ardupilot.org/dev/docs/building-setup-linux.html) if necessary. 1. **Clone the Custom-Build Repository:** ```bash @@ -86,7 +88,7 @@ To run the ArduPilot Custom Firmware Builder locally without Docker, ensure you 3. **Install Dependencies:** ```bash - pip install -r web/requirements.txt -r builder/requirements.txt + pip install -r backend/requirements.txt -r builder/requirements.txt ``` If pip is not installed, run: @@ -104,38 +106,37 @@ To run the ArduPilot Custom Firmware Builder locally without Docker, ensure you sudo systemctl status redis-server ``` -5. **Execute the Application:** - - For a development environment with auto-reload, run: - ```bash - python3 web/main.py - ``` - To change the port, use the `--port` argument: - ```bash - python3 web/main.py --port 9000 - ``` - - For a production environment, use: - ```bash - uvicorn web.main:app --host 0.0.0.0 --port 8080 - ``` +5. **Start the Backend and Frontend:** + In one terminal, start the API (listens on port 8080 by default): + ```bash + python3 backend/main.py + ``` + To use a different port, pass `--port` or set `BACKEND_PORT`. If you change it, update the Vite proxy target in `frontend/vite.config.ts` to match. - During the coding and testing phases, use the development environment to easily debug and make changes with auto-reload enabled. When deploying the app for end users, use the production environment to ensure better performance, scalability, and security. + In another terminal, start the UI: + ```bash + cd frontend + npm install + npm run dev + ``` - The application will automatically set up the required base directory at `./base` upon first execution. You may customize this path by setting the `CBS_BASEDIR` environment variable. + The application will automatically set up the required base directory at `./base` upon first execution. You may customize this path by setting the `CBS_BASEDIR` environment variable. 6. **Access the Web Interface:** - Once the application is running, you can access the interface in your web browser at http://localhost:8080. - - The default port is 8080, or the value of the `WEB_PORT` environment variable if set. You can override this by passing the `--port` argument when running the application directly (e.g., `python3 web/main.py --port 9000`) or when using uvicorn (e.g., `uvicorn web.main:app --port 5000`). Refer to the [uvicorn documentation](https://www.uvicorn.org/) for additional configuration options. + Open `http://localhost:5173` in your browser. Vite proxies `/api` requests to the backend, so you do not need nginx for local development. ## Directory Structure The default directory structure is established as follows: ``` /home/ └── CustomBuild + ├── schemas + │ └── config + │ └── 0.0.1.json (shared CustomBuild YAML schema) └── base - ├── ardupilot (used by the web component) - ├── artifacts + ├── ardupilot (used by the backend) + ├── artifacts (build bundles include custombuild.yaml) ├── configs | └── remotes.json (optional, see examples/remotes.json.sample) ├── secrets @@ -143,7 +144,7 @@ The default directory structure is established as follows: ├── tmp └── ardupilot (used by the builder component) ``` -The build artifacts are organized under the `base/artifacts` subdirectory. +The build artifacts are organized under the `base/artifacts` subdirectory. Each completed build archive (`.tar.gz`) includes firmware binaries, `build.log`, `extra_hwdef.dat`, and a Builder-generated `custombuild.yaml` for rebuilding. Config schemas live under `schemas/config/` at the repo root (consumed by Builder, backend, and frontend). ## Acknowledgements This project includes many valuable contributions made during the Google Summer of Code 2021. For more information, please see the [GSOC 2021 Blog Post](https://discuss.ardupilot.org/t/gsoc-2021-custom-firmware-builder/74946). diff --git a/web/Dockerfile b/backend/Dockerfile similarity index 96% rename from web/Dockerfile rename to backend/Dockerfile index fa3c68c7..4feb9619 100644 --- a/web/Dockerfile +++ b/backend/Dockerfile @@ -10,7 +10,7 @@ RUN groupadd -g 999 ardupilot && \ chown ardupilot:ardupilot /app COPY --chown=ardupilot:ardupilot . /app -WORKDIR /app/web +WORKDIR /app/backend RUN pip install --no-cache-dir -r requirements.txt ENV PYTHONPATH=/app diff --git a/web/__init__.py b/backend/__init__.py similarity index 100% rename from web/__init__.py rename to backend/__init__.py diff --git a/web/api/v1/__init__.py b/backend/api/v1/__init__.py similarity index 51% rename from web/api/v1/__init__.py rename to backend/api/v1/__init__.py index 81d5e30e..a818a3dd 100644 --- a/web/api/v1/__init__.py +++ b/backend/api/v1/__init__.py @@ -1,4 +1,4 @@ """API v1 module.""" -from web.api.v1.router import router +from backend.api.v1.router import router __all__ = ["router"] diff --git a/web/api/v1/admin.py b/backend/api/v1/admin.py similarity index 95% rename from web/api/v1/admin.py rename to backend/api/v1/admin.py index f1a63a0f..ab5e5e7d 100644 --- a/web/api/v1/admin.py +++ b/backend/api/v1/admin.py @@ -1,8 +1,8 @@ from fastapi import APIRouter, HTTPException, Depends, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from web.schemas import RefreshVersionsResponse -from web.services.admin import get_admin_service, AdminService +from backend.schemas import RefreshVersionsResponse +from backend.services.admin import get_admin_service, AdminService router = APIRouter(prefix="/admin", tags=["admin"]) diff --git a/web/api/v1/builds.py b/backend/api/v1/builds.py similarity index 78% rename from web/api/v1/builds.py rename to backend/api/v1/builds.py index 2c68c6ab..26b4211c 100644 --- a/web/api/v1/builds.py +++ b/backend/api/v1/builds.py @@ -1,3 +1,4 @@ +import os from typing import List, Optional from fastapi import ( APIRouter, @@ -8,15 +9,15 @@ Depends, Request ) -from fastapi.responses import FileResponse, PlainTextResponse +from fastapi.responses import FileResponse, PlainTextResponse, Response -from web.schemas import ( +from backend.schemas import ( BuildRequest, BuildSubmitResponse, BuildOut, ) -from web.services.builds import get_builds_service, BuildsService -from web.core.limiter import limiter +from backend.services.builds import get_builds_service, BuildsService +from backend.core.limiter import limiter router = APIRouter(prefix="/builds", tags=["builds"]) @@ -219,5 +220,53 @@ async def download_artifact( return FileResponse( path=artifact_path, media_type='application/gzip', - filename=f"{build_id}.tar.gz" + filename=os.path.basename(artifact_path) + ) + + +@router.get( + "/{build_id}/config", + responses={ + 404: { + "description": ( + "Build not found or config could not be generated" + ) + } + } +) +async def download_config( + build_id: str = Path(..., description="Unique build identifier"), + service: BuildsService = Depends(get_builds_service) +): + """ + Download the CustomBuild config YAML for a build. + + Generated from build metadata via the shared build_config module + (same shape as the YAML packed into the archive by Builder). + + Args: + build_id: The unique build identifier + + Returns: + YAML config file + + Raises: + 404: Build not found or config could not be generated + """ + result = service.get_build_config_yaml(build_id) + if not result: + raise HTTPException( + status_code=404, + detail=( + f"Config not available for build '{build_id}'. " + "Build may not exist or metadata is incomplete." + ) + ) + yaml_text, filename = result + return Response( + content=yaml_text, + media_type="application/yaml", + headers={ + "Content-Disposition": f'attachment; filename="{filename}"' + }, ) diff --git a/web/api/v1/router.py b/backend/api/v1/router.py similarity index 88% rename from web/api/v1/router.py rename to backend/api/v1/router.py index fa99a716..be1debc8 100644 --- a/web/api/v1/router.py +++ b/backend/api/v1/router.py @@ -6,7 +6,7 @@ """ from fastapi import APIRouter -from web.api.v1 import vehicles, builds, admin +from backend.api.v1 import vehicles, builds, admin # Create the main v1 router router = APIRouter(prefix="/v1") diff --git a/web/api/v1/vehicles.py b/backend/api/v1/vehicles.py similarity index 98% rename from web/api/v1/vehicles.py rename to backend/api/v1/vehicles.py index 01380bf7..47f7ff10 100644 --- a/web/api/v1/vehicles.py +++ b/backend/api/v1/vehicles.py @@ -1,14 +1,14 @@ from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, Path -from web.schemas import ( +from backend.schemas import ( VehicleBase, VersionOut, BoardOut, StandardArtifactOut, FeatureOut, ) -from web.services.vehicles import get_vehicles_service, VehiclesService +from backend.services.vehicles import get_vehicles_service, VehiclesService router = APIRouter(prefix="/vehicles", tags=["vehicles"]) diff --git a/web/core/__init__.py b/backend/core/__init__.py similarity index 50% rename from web/core/__init__.py rename to backend/core/__init__.py index 91590a50..6b70b63b 100644 --- a/web/core/__init__.py +++ b/backend/core/__init__.py @@ -1,8 +1,8 @@ """ Core application components. """ -from web.core.config import get_settings -from web.core.startup import initialize_application +from backend.core.config import get_settings +from backend.core.startup import initialize_application __all__ = [ "get_settings", diff --git a/web/core/config.py b/backend/core/config.py similarity index 100% rename from web/core/config.py rename to backend/core/config.py diff --git a/web/core/limiter.py b/backend/core/limiter.py similarity index 94% rename from web/core/limiter.py rename to backend/core/limiter.py index 831dedfb..0d551885 100644 --- a/web/core/limiter.py +++ b/backend/core/limiter.py @@ -4,7 +4,7 @@ from slowapi.errors import RateLimitExceeded from slowapi import Limiter from slowapi.util import get_remote_address -from web.core.config import get_settings +from backend.core.config import get_settings logger = logging.getLogger(__name__) diff --git a/web/core/logging_config.py b/backend/core/logging_config.py similarity index 100% rename from web/core/logging_config.py rename to backend/core/logging_config.py diff --git a/web/core/startup.py b/backend/core/startup.py similarity index 100% rename from web/core/startup.py rename to backend/core/startup.py diff --git a/web/docker-entrypoint.sh b/backend/docker-entrypoint.sh similarity index 100% rename from web/docker-entrypoint.sh rename to backend/docker-entrypoint.sh diff --git a/web/main.py b/backend/main.py similarity index 89% rename from web/main.py rename to backend/main.py index 827a1d7b..f9dc6547 100755 --- a/web/main.py +++ b/backend/main.py @@ -14,13 +14,13 @@ from slowapi.errors import RateLimitExceeded from slowapi.middleware import SlowAPIMiddleware -from web.api.v1 import router as v1_router -from web.ui import router as ui_router +from backend.api.v1 import router as v1_router +from backend.ui import router as ui_router -from web.core.config import get_settings -from web.core.startup import initialize_application -from web.core.logging_config import setup_logging -from web.core.limiter import limiter, rate_limit_exceeded_handler +from backend.core.config import get_settings +from backend.core.startup import initialize_application +from backend.core.logging_config import setup_logging +from backend.core.limiter import limiter, rate_limit_exceeded_handler import ap_git import build_manager @@ -134,6 +134,7 @@ async def lifespan(app: FastAPI): title="CustomBuild API", description="API for ArduPilot Custom Firmware Builder", version="1.0.0", + openapi_url="/api/openapi.json", docs_url="/api/docs", redoc_url="/api/redoc", lifespan=lifespan, @@ -169,8 +170,8 @@ async def health_check(): parser.add_argument( "--port", type=int, - default=int(os.getenv("WEB_PORT", 8080)), - help="Port to run the server on (default: 8080 or WEB_PORT env var)" + default=int(os.getenv("BACKEND_PORT", 8080)), + help="Port to run the server on (default: 8080 or BACKEND_PORT env var)" ) args = parser.parse_args() diff --git a/web/requirements.txt b/backend/requirements.txt similarity index 92% rename from web/requirements.txt rename to backend/requirements.txt index 1862bbda..3f28e210 100644 --- a/web/requirements.txt +++ b/backend/requirements.txt @@ -9,3 +9,4 @@ packaging==25.0 jinja2==3.1.2 python-multipart==0.0.6 slowapi==0.1.9 +PyYAML==6.0.2 diff --git a/web/schemas/__init__.py b/backend/schemas/__init__.py similarity index 89% rename from web/schemas/__init__.py rename to backend/schemas/__init__.py index 7d31acf2..eca63a65 100644 --- a/web/schemas/__init__.py +++ b/backend/schemas/__init__.py @@ -6,12 +6,12 @@ """ # Admin schemas -from web.schemas.admin import ( +from backend.schemas.admin import ( RefreshVersionsResponse, ) # Build schemas -from web.schemas.builds import ( +from backend.schemas.builds import ( BuildVersionInfo, RemoteInfo, BuildProgress, @@ -21,7 +21,7 @@ ) # Vehicle schemas -from web.schemas.vehicles import ( +from backend.schemas.vehicles import ( VehicleBase, VersionBase, VersionOut, diff --git a/web/schemas/admin.py b/backend/schemas/admin.py similarity index 100% rename from web/schemas/admin.py rename to backend/schemas/admin.py diff --git a/web/schemas/builds.py b/backend/schemas/builds.py similarity index 88% rename from web/schemas/builds.py rename to backend/schemas/builds.py index fc7e8a38..54a3a2c3 100644 --- a/web/schemas/builds.py +++ b/backend/schemas/builds.py @@ -1,7 +1,7 @@ -from typing import List, Literal +from typing import List, Literal, Optional from pydantic import BaseModel, Field -from web.schemas.vehicles import VehicleBase, BoardBase, RemoteInfo +from backend.schemas.vehicles import VehicleBase, BoardBase, RemoteInfo # --- Build Progress --- @@ -47,6 +47,10 @@ class BuildSubmitResponse(BaseModel): class BuildVersionInfo(BaseModel): """Version information for a build.""" id: str = Field(..., description="Version ID used for this build") + name: Optional[str] = Field(None, description="Version display name") + type: Optional[Literal["beta", "stable", "latest", "tag"]] = Field( + None, description="Version type classification" + ) remote_info: RemoteInfo = Field( ..., description="Source repository information" ) diff --git a/web/schemas/vehicles.py b/backend/schemas/vehicles.py similarity index 100% rename from web/schemas/vehicles.py rename to backend/schemas/vehicles.py diff --git a/backend/services/__init__.py b/backend/services/__init__.py new file mode 100644 index 00000000..fab43c72 --- /dev/null +++ b/backend/services/__init__.py @@ -0,0 +1,15 @@ +""" +Business logic services for the application. +""" +from backend.services.vehicles import get_vehicles_service, VehiclesService +from backend.services.builds import get_builds_service, BuildsService +from backend.services.admin import get_admin_service, AdminService + +__all__ = [ + "get_vehicles_service", + "VehiclesService", + "get_builds_service", + "BuildsService", + "get_admin_service", + "AdminService", +] diff --git a/web/services/admin.py b/backend/services/admin.py similarity index 97% rename from web/services/admin.py rename to backend/services/admin.py index e8d1745c..3b677a88 100644 --- a/web/services/admin.py +++ b/backend/services/admin.py @@ -5,7 +5,7 @@ from typing import List from fastapi import Depends, Request -from web.core.config import get_settings, Settings +from backend.core.config import get_settings, Settings logger = logging.getLogger(__name__) diff --git a/web/services/builds.py b/backend/services/builds.py similarity index 72% rename from web/services/builds.py rename to backend/services/builds.py index 9cc6b671..28f81dcd 100644 --- a/web/services/builds.py +++ b/backend/services/builds.py @@ -6,7 +6,7 @@ from fastapi import Request from typing import List, Optional -from web.schemas import ( +from backend.schemas import ( BuildRequest, BuildSubmitResponse, BuildOut, @@ -14,7 +14,7 @@ RemoteInfo, BuildVersionInfo, ) -from web.schemas.vehicles import VehicleBase, BoardBase +from backend.schemas.vehicles import VehicleBase, BoardBase # Import external modules # pylint: disable=wrong-import-position @@ -65,6 +65,10 @@ def create_build( if not vehicle_id: raise ValueError("vehicle_id is required") + vehicle = self.vehicles_manager.get_vehicle_by_id(vehicle_id) + if vehicle is None: + raise ValueError("Invalid vehicle_id") + # Get version info using version_id version_info = self.versions_manager.get_version_info( vehicle_id=vehicle_id, @@ -103,37 +107,10 @@ def create_build( commit_ref=commit_ref ) - # Map feature labels (IDs from API) to defines - # (required by build manager) - selected_feature_defines = set() - if build_request.selected_features: - # Get build options to map labels to defines - with self.repo.get_checkout_lock(): - options = ( - self.ap_src_metadata_fetcher - .get_build_options_at_commit( - remote=remote_name, - commit_ref=commit_ref - ) - ) - - # Create label to define mapping - label_to_define = { - option.label: option.define for option in options - } - - # Map each selected feature label to its define - for feature_label in build_request.selected_features: - if feature_label in label_to_define: - selected_feature_defines.add( - label_to_define[feature_label] - ) - else: - logger.warning( - f"Feature label '{feature_label}' not found in " - f"build options for {vehicle_id} {remote_name} " - f"{commit_ref}" - ) + if version_info.release_type == "latest": + version_display_name = "master" + else: + version_display_name = version_info.version_number # Create build info build_info = build_manager.BuildInfo( @@ -142,7 +119,11 @@ def create_build( remote_info=remote_info, git_hash=git_hash, board=board_name, - selected_features=selected_feature_defines + selected_features=set(build_request.selected_features), + vehicle_name=vehicle.name, + board_name=board_name, + version_name=version_display_name, + version_type=version_info.release_type, ) # Submit build @@ -282,12 +263,47 @@ def get_artifact_path(self, build_id: str) -> Optional[str]: ]: return None - artifact_path = self.manager.get_build_archive_path(build_id) + artifact_path = self.manager.get_build_archive_path( + build_id, build_info.vehicle_id, build_info.board + ) if os.path.exists(artifact_path): return artifact_path return None + def get_build_config_yaml(self, build_id: str) -> Optional[tuple]: + """ + Generate custombuild.yaml from BuildInfo. + + Args: + build_id: The unique build identifier + + Returns: + (yaml_text, download_filename) or None if unavailable + """ + from build_config import config_dict_from_build_info, dump_config_yaml + + if not self.manager.build_exists(build_id): + return None + + build_info = self.manager.get_build_info(build_id) + if build_info is None: + return None + + try: + yaml_text = dump_config_yaml(config_dict_from_build_info(build_info)) + except Exception as e: + logger.error( + f"Error generating config YAML for build {build_id}: {e}" + ) + return None + + filename = ( + f"custombuild-{build_info.vehicle_id}-" + f"{build_info.board}-{build_id}.yaml" + ) + return yaml_text, filename + def _build_info_to_output( self, build_id: str, @@ -315,69 +331,46 @@ def _build_info_to_output( url=build_info.remote_info.url ) - # Map feature defines back to labels for API response - selected_feature_labels = [] - if build_info.selected_features: - try: - # Get build options to map defines back to labels - with self.repo.get_checkout_lock(): - options = ( - self.ap_src_metadata_fetcher - .get_build_options_at_commit( - remote=build_info.remote_info.name, - commit_ref=build_info.git_hash - ) - ) - - # Create define to label mapping - define_to_label = { - option.define: option.label for option in options - } - - # Map each selected feature define to its label - for feature_define in build_info.selected_features: - if feature_define in define_to_label: - selected_feature_labels.append( - define_to_label[feature_define] - ) - else: - # Fallback: use define if label not found - logger.warning( - f"Feature define '{feature_define}' not " - f"found in build options for build " - f"{build_id}" - ) - selected_feature_labels.append(feature_define) - except Exception as e: - logger.error( - f"Error mapping feature defines to labels for " - f"build {build_id}: {e}" - ) - # Fallback: use defines as-is - selected_feature_labels = list( - build_info.selected_features - ) - vehicle = self.vehicles_manager.get_vehicle_by_id( build_info.vehicle_id ) + if vehicle is not None: + vehicle_name = vehicle.name + else: + vehicle_name = build_info.vehicle_name or "" + + v_info = self.versions_manager.get_version_info( + vehicle_id=build_info.vehicle_id, + version_id=build_info.version_id + ) + if v_info is not None: + version_type = v_info.release_type + if v_info.release_type == "latest": + version_name = "master" + else: + version_name = v_info.version_number + else: + version_name = build_info.version_name + version_type = build_info.version_type return BuildOut( build_id=build_id, vehicle=VehicleBase( id=build_info.vehicle_id, - name=vehicle.name + name=vehicle_name ), board=BoardBase( id=build_info.board, - name=build_info.board # Board name is same as board ID for now + name=build_info.board_name ), version=BuildVersionInfo( id=build_info.version_id, + name=version_name, + type=version_type, remote_info=remote_info, git_hash=build_info.git_hash ), - selected_features=selected_feature_labels, + selected_features=list(build_info.selected_features), progress=progress, time_created=build_info.time_created, ) diff --git a/web/services/vehicles.py b/backend/services/vehicles.py similarity index 97% rename from web/services/vehicles.py rename to backend/services/vehicles.py index ebde4853..74adb299 100644 --- a/web/services/vehicles.py +++ b/backend/services/vehicles.py @@ -7,7 +7,7 @@ from metadata_manager.firmware_server.index import latest_features_txt_url from metadata_manager.versions_manager.providers import OFFICIAL_REMOTE_NAME -from web.schemas import ( +from backend.schemas import ( VehicleBase, RemoteInfo, VersionOut, @@ -73,12 +73,9 @@ def get_versions( continue if version_info.release_type == "latest": - title = f"Latest ({version_info.remote_info.name})" + title = "master" else: - rel_type = version_info.release_type - ver_num = version_info.version_number - remote = version_info.remote_info.name - title = f"{rel_type} {ver_num} ({remote})" + title = version_info.version_number versions.append(VersionOut( id=version_info.version_id, diff --git a/web/static/images/ardupilot_logo.png b/backend/static/images/ardupilot_logo.png similarity index 100% rename from web/static/images/ardupilot_logo.png rename to backend/static/images/ardupilot_logo.png diff --git a/web/static/images/bg.png b/backend/static/images/bg.png similarity index 100% rename from web/static/images/bg.png rename to backend/static/images/bg.png diff --git a/web/static/images/button-closed.png b/backend/static/images/button-closed.png similarity index 100% rename from web/static/images/button-closed.png rename to backend/static/images/button-closed.png diff --git a/web/static/images/button-open.png b/backend/static/images/button-open.png similarity index 100% rename from web/static/images/button-open.png rename to backend/static/images/button-open.png diff --git a/web/static/images/button.png b/backend/static/images/button.png similarity index 100% rename from web/static/images/button.png rename to backend/static/images/button.png diff --git a/web/static/images/logo.png b/backend/static/images/logo.png similarity index 100% rename from web/static/images/logo.png rename to backend/static/images/logo.png diff --git a/web/static/js/add_build.js b/backend/static/js/add_build.js similarity index 100% rename from web/static/js/add_build.js rename to backend/static/js/add_build.js diff --git a/web/static/js/index.js b/backend/static/js/index.js similarity index 100% rename from web/static/js/index.js rename to backend/static/js/index.js diff --git a/web/static/styles/main.css b/backend/static/styles/main.css similarity index 100% rename from web/static/styles/main.css rename to backend/static/styles/main.css diff --git a/web/templates/add_build.html b/backend/templates/add_build.html similarity index 100% rename from web/templates/add_build.html rename to backend/templates/add_build.html diff --git a/web/templates/index.html b/backend/templates/index.html similarity index 100% rename from web/templates/index.html rename to backend/templates/index.html diff --git a/web/ui/__init__.py b/backend/ui/__init__.py similarity index 64% rename from web/ui/__init__.py rename to backend/ui/__init__.py index 62fa8d7d..c2574d30 100644 --- a/web/ui/__init__.py +++ b/backend/ui/__init__.py @@ -1,6 +1,6 @@ """ UI module for web interface routes. """ -from web.ui.router import router +from backend.ui.router import router __all__ = ["router"] diff --git a/web/ui/router.py b/backend/ui/router.py similarity index 100% rename from web/ui/router.py rename to backend/ui/router.py diff --git a/build_config/__init__.py b/build_config/__init__.py new file mode 100644 index 00000000..de96a99a --- /dev/null +++ b/build_config/__init__.py @@ -0,0 +1,23 @@ +"""Shared CustomBuild YAML config generation and validation.""" + +from build_config.config import ( + CONFIG_VERSION, + CONFIG_FILENAME, + build_config_dict, + config_dict_from_build_info, + dump_config_yaml, + schema_path_for_version, + validate_config_dict, + write_config_yaml, +) + +__all__ = [ + "CONFIG_VERSION", + "CONFIG_FILENAME", + "build_config_dict", + "config_dict_from_build_info", + "dump_config_yaml", + "schema_path_for_version", + "validate_config_dict", + "write_config_yaml", +] diff --git a/build_config/config.py b/build_config/config.py new file mode 100644 index 00000000..672d3371 --- /dev/null +++ b/build_config/config.py @@ -0,0 +1,88 @@ +"""Serialize and validate CustomBuild config YAML (schemas/config).""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Mapping, Sequence + +import yaml +from jsonschema import Draft202012Validator + +CONFIG_VERSION = "0.0.1" +CONFIG_FILENAME = "custombuild.yaml" + +_SCHEMAS_DIR = Path(__file__).resolve().parent.parent / "schemas" / "config" + + +def schema_path_for_version(version: str = CONFIG_VERSION) -> Path: + path = _SCHEMAS_DIR / f"{version}.json" + if not path.is_file(): + raise FileNotFoundError(f"No config schema for version '{version}' at {path}") + return path + + +def validate_config_dict(config: Mapping[str, Any]) -> None: + version = config.get("config_version") + if not isinstance(version, str): + raise ValueError("Invalid or missing config_version") + schema = json.loads(schema_path_for_version(version).read_text(encoding="utf-8")) + Draft202012Validator(schema).validate(dict(config)) + + +def build_config_dict( + *, + vehicle_id: str, + vehicle_name: str, + version_id: str, + version_name: str, + version_type: str, + remote_name: str, + board_id: str, + board_name: str, + selected_features: Sequence[str], + config_version: str = CONFIG_VERSION, +) -> dict[str, Any]: + """Build a schema-compliant config dict (selected_features are API labels).""" + return { + "config_version": config_version, + "vehicle": {"id": vehicle_id, "name": vehicle_name}, + "version": { + "id": version_id, + "name": version_name, + "type": version_type, + "remote_name": remote_name, + }, + "board": {"id": board_id, "name": board_name}, + "selected_features": list(selected_features), + } + + +def dump_config_yaml(config: Mapping[str, Any]) -> str: + validate_config_dict(config) + return yaml.safe_dump( + dict(config), + default_flow_style=False, + sort_keys=False, + allow_unicode=True, + ) + + +def write_config_yaml(path: Path | str, config: Mapping[str, Any]) -> None: + path = Path(path) + path.write_text(dump_config_yaml(config), encoding="utf-8") + + +def config_dict_from_build_info(build_info: Any) -> dict[str, Any]: + """Build config from BuildInfo fields set at submit time.""" + return build_config_dict( + vehicle_id=build_info.vehicle_id, + vehicle_name=build_info.vehicle_name, + version_id=build_info.version_id, + version_name=build_info.version_name, + version_type=build_info.version_type, + remote_name=build_info.remote_info.name, + board_id=build_info.board, + board_name=build_info.board_name, + selected_features=list(build_info.selected_features), + ) diff --git a/build_manager/manager.py b/build_manager/manager.py index 60bbc2ee..50af92de 100644 --- a/build_manager/manager.py +++ b/build_manager/manager.py @@ -48,7 +48,11 @@ def __init__(self, remote_info: RemoteInfo, git_hash: str, board: str, - selected_features: set) -> None: + selected_features: set, + vehicle_name: str, + board_name: str, + version_name: str, + version_type: str) -> None: """ Initialize build information object including vehicle, remote, git hash, selected features, and progress of the build. @@ -61,7 +65,11 @@ def __init__(self, source commit to build on. git_hash (str): The git commit hash to build on. board (str): Board to build for. - selected_features (set): Set of features selected for the build. + selected_features (set): Set of feature API labels/IDs for the build. + vehicle_name (str): Display name for rebuild config YAML. + board_name (str): Display name for rebuild config YAML. + version_name (str): Display name for rebuild config YAML. + version_type (str): Release type for rebuild config YAML. """ self.vehicle_id = vehicle_id self.version_id = version_id @@ -69,6 +77,10 @@ def __init__(self, self.git_hash = git_hash self.board = board self.selected_features = selected_features + self.vehicle_name = vehicle_name + self.board_name = board_name + self.version_name = version_name + self.version_type = version_type self.progress = BuildProgress( state=BuildState.PENDING, percent=0 @@ -84,9 +96,13 @@ def to_dict(self) -> dict: 'git_hash': self.git_hash, 'board': self.board, 'selected_features': list(self.selected_features), + 'vehicle_name': self.vehicle_name, + 'board_name': self.board_name, + 'version_name': self.version_name, + 'version_type': self.version_type, 'progress': self.progress.to_dict(), 'time_created': self.time_created, - 'time_started': getattr(self, 'time_started', None), + 'time_started': self.time_started, } @@ -200,13 +216,12 @@ def __generate_build_id(self, build_info: BuildInfo) -> str: build_info (BuildInfo): The build information object. Returns: - str: The generated build ID (64 characters). + str: The generated build ID (8 characters). """ h = hashlib.md5( f"{build_info}-{time.time_ns()}".encode() - ).hexdigest() - bid = f"{build_info.vehicle_id}-{build_info.board}-{h}" - return bid + ).hexdigest()[:8] + return h def submit_build(self, build_info: BuildInfo) -> str: @@ -460,19 +475,23 @@ def get_build_log_path(self, build_id: str) -> str: 'build.log' ) - def get_build_archive_path(self, build_id: str) -> str: + def get_build_archive_path( + self, build_id: str, vehicle_id: str, board: str + ) -> str: """ Return the path to the build archive. Parameters: build_id (str): The ID of the build. + vehicle_id (str): The vehicle identifier. + board (str): The board identifier. Returns: str: The path to the build archive. """ return os.path.join( self.get_build_artifacts_dir_path(build_id), - f"{build_id}.tar.gz" + f"{vehicle_id}-{board}-{build_id}.tar.gz" ) @staticmethod diff --git a/build_manager/progress_updater.py b/build_manager/progress_updater.py index ee155048..efda989c 100644 --- a/build_manager/progress_updater.py +++ b/build_manager/progress_updater.py @@ -186,7 +186,9 @@ def __refresh_running_build_state(self, build_id: str) -> BuildState: # Builder ships the archive post completion # This is irrespective of SUCCESS or FAILURE if not os.path.exists( - bm.get_singleton().get_build_archive_path(build_id) + bm.get_singleton().get_build_archive_path( + build_id, build_info.vehicle_id, build_info.board + ) ): return BuildState.RUNNING diff --git a/builder/builder.py b/builder/builder.py index 7a16cb16..7ac2780e 100644 --- a/builder/builder.py +++ b/builder/builder.py @@ -13,10 +13,36 @@ VehiclesManager as vehm ) from pathlib import Path +from build_config import ( + CONFIG_FILENAME, + config_dict_from_build_info, + write_config_yaml, +) CBS_BUILD_TIMEOUT_SEC = int(os.getenv('CBS_BUILD_TIMEOUT_SEC', 900)) +def resolve_feature_defines(selected_labels, all_features): + """ + Map API feature labels to preprocessor defines for extra_hwdef. + + Returns: + tuple: (enabled_defines, disabled_defines, all_defines, unknown_labels) + """ + label_to_define = { + feature.label: feature.define for feature in all_features + } + all_defines = set(label_to_define.values()) + selected = set(selected_labels) + known = set(label_to_define) + unknown_labels = selected.difference(known) + enabled_defines = { + label_to_define[label] for label in known.intersection(selected) + } + disabled_defines = all_defines.difference(enabled_defines) + return enabled_defines, disabled_defines, all_defines, unknown_labels + + class Builder: """ Processes build requests, perform builds and ship build artifacts @@ -102,22 +128,24 @@ def __generate_extrahwdef(self, build_id: str) -> None: ) build_info = bm.get_singleton().get_build_info(build_id) - selected_features = build_info.selected_features + selected_labels = build_info.selected_features self.logger.debug( - f"Selected features for {build_id}: {selected_features}" + f"Selected feature labels for {build_id}: {selected_labels}" ) all_features = apfetch.get_singleton().get_build_options_at_commit( remote=build_info.remote_info.name, commit_ref=build_info.git_hash, ) - all_defines = { - feature.define - for feature in all_features - } - enabled_defines = selected_features.intersection(all_defines) - disabled_defines = all_defines.difference(enabled_defines) + enabled_defines, disabled_defines, all_defines, unknown_labels = ( + resolve_feature_defines(selected_labels, all_features) + ) + if unknown_labels: + self.logger.warning( + f"Unknown feature labels not found in build options; " + f"skipping: {sorted(unknown_labels)}" + ) self.logger.info(f"Enabled defines for {build_id}: {enabled_defines}") - self.logger.info(f"Disabled defines for {build_id}: {enabled_defines}") + self.logger.info(f"Disabled defines for {build_id}: {disabled_defines}") with open(self.__get_path_to_extra_hwdef(build_id), "w") as f: # Undefine all defines at the beginning @@ -226,7 +254,9 @@ def __generate_archive(self, build_id: str) -> None: build_id (str): Unique identifier for the build. """ build_info = bm.get_singleton().get_build_info(build_id) - archive_path = bm.get_singleton().get_build_archive_path(build_id) + archive_path = bm.get_singleton().get_build_archive_path( + build_id, build_info.vehicle_id, build_info.board + ) files_to_include = [] @@ -261,10 +291,18 @@ def __generate_archive(self, build_id: str) -> None: ) files_to_include.append(extra_hwdef_path_abs) - # create archive + # include rebuild config YAML (Builder is sole canonical author) + config_path = Path( + self.__get_path_to_build_dir(build_id) + ) / CONFIG_FILENAME + write_config_yaml(config_path, config_dict_from_build_info(build_info)) + files_to_include.append(str(config_path.resolve())) + + # create archive (inner folder matches download basename) + folder_name = Path(archive_path).name.removesuffix(".tar.gz") with tarfile.open(archive_path, "w:gz") as tar: for file in files_to_include: - arcname = f"{build_id}/{os.path.basename(file)}" + arcname = f"{folder_name}/{os.path.basename(file)}" self.logger.debug(f"Added {file} as {arcname}") tar.add(file, arcname=arcname) self.logger.info(f"Generated {archive_path}.") diff --git a/builder/requirements.txt b/builder/requirements.txt index 3c8b6d37..d238f48c 100644 --- a/builder/requirements.txt +++ b/builder/requirements.txt @@ -3,3 +3,4 @@ redis==5.2.1 dill==0.3.8 requests==2.31.0 packaging==25.0 +PyYAML==6.0.2 diff --git a/docker-compose.yml b/docker-compose.yml index 4ae3fe1a..84404bce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,10 +8,10 @@ services: - ./redis_data:/data:rw command: redis-server - app: + backend: build: context: . - dockerfile: ./web/Dockerfile + dockerfile: ./backend/Dockerfile restart: always environment: CBS_REDIS_HOST: redis @@ -28,8 +28,6 @@ services: - ./base:/base:rw depends_on: - redis - ports: - - "127.0.0.1:${WEB_PORT:-8080}:8080" builder: build: @@ -48,3 +46,13 @@ services: - ./base:/base:rw depends_on: - redis + + frontend: + build: + context: . + dockerfile: ./frontend/Dockerfile + restart: always + depends_on: + - backend + ports: + - "${WEB_PORT:-11080}:80" diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..5b08070c --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env +*.local diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 00000000..1c3b8eb9 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,19 @@ +module.exports = { + root: true, + env: { browser: true, es2022: true }, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'react-hooks'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + rules: { + '@typescript-eslint/no-unused-vars': ['warn', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }], + '@typescript-eslint/no-explicit-any': 'off', + }, + ignorePatterns: ['dist/', 'node_modules/'], +}; diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..8a84a6ee --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,25 @@ +# Stage 1: Build the React app +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY frontend/package.json frontend/package-lock.json* ./ +RUN npm ci + +COPY frontend/ ./ +# Shared config schemas (single source of truth at repo root) +COPY schemas/ /schemas/ + +RUN npm run build + +# Stage 2: Serve with nginx +FROM nginx:1.27-alpine + +COPY --from=builder /app/dist /usr/share/nginx/html + +# nginx config that handles SPA routing (serves index.html for all routes) +COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..956d50d3 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,17 @@ + + + + + + + + ArduPilot CustomBuild + + + + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 00000000..f78284e4 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://backend:8080/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..26b843fe --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4523 @@ +{ + "name": "custombuild-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "custombuild-frontend", + "version": "0.1.0", + "dependencies": { + "@xyflow/react": "^12.11.2", + "ajv": "^8.20.0", + "clsx": "^2.1.1", + "js-yaml": "^4.1.1", + "lucide-react": "^0.441.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.19.43", + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.18.0", + "@typescript-eslint/parser": "^8.18.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.1", + "eslint-plugin-react-hooks": "^5.1.0", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.11", + "typescript": "^5.5.3", + "vite": "^5.4.8" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", + "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/type-utils": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.64.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", + "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", + "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.64.0", + "@typescript-eslint/types": "^8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", + "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", + "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", + "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", + "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", + "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.64.0", + "@typescript-eslint/tsconfig-utils": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", + "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", + "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@xyflow/react": { + "version": "12.11.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.2.tgz", + "integrity": "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.79", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.79", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.79.tgz", + "integrity": "sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.441.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.441.0.tgz", + "integrity": "sha512-0vfExYtvSDhkC2lqg0zYVW1Uu9GsI4knuV9GP9by5z0Xhc4Zi5RejTxfz9LsjRmCyWVzHCJvxGKZWcRyvQCWVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..3928cfe2 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,37 @@ +{ + "name": "custombuild-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "lint": "eslint src --ext .ts,.tsx" + }, + "dependencies": { + "@xyflow/react": "^12.11.2", + "ajv": "^8.20.0", + "clsx": "^2.1.1", + "js-yaml": "^4.1.1", + "lucide-react": "^0.441.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.19.43", + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.18.0", + "@typescript-eslint/parser": "^8.18.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.1", + "eslint-plugin-react-hooks": "^5.1.0", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.11", + "typescript": "^5.5.3", + "vite": "^5.4.8" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 00000000..d41ad635 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/ardupilot_logo.png b/frontend/public/ardupilot_logo.png new file mode 100644 index 00000000..ed67b531 Binary files /dev/null and b/frontend/public/ardupilot_logo.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 00000000..84730df8 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 00000000..f49ab464 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,4 @@ + + + AP + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 00000000..2f8b34f0 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,219 @@ +import { useState, useRef, useEffect } from 'react'; +import { Menu, X } from 'lucide-react'; +import { BuildForm } from './components/BuildForm'; +import { BuildsTable } from './components/BuildsTable'; +import { HeroPanel } from './components/HeroPanel'; +import { ThemeToggle } from './components/ThemeToggle'; +import { type BuildConfig, configFromQueryParams } from './buildConfig'; + +const THEME_KEY = 'custombuild-theme'; + +const NAV_LINKS = [ + { href: 'https://ardupilot.org', label: 'ardupilot.org', external: true }, + { href: 'https://github.com/ArduPilot/CustomBuild', label: 'GitHub', external: true }, + { href: 'https://ardupilot.org/copter/docs/common-custom-firmware.html', label: 'Help', external: true }, + { href: '/api/docs', label: 'API Docs', external: false }, +] as const; + +function readInitialDark(): boolean { + try { + const stored = localStorage.getItem(THEME_KEY); + if (stored === 'dark') return true; + if (stored === 'light') return false; + } catch { /* ignore */ } + return window.matchMedia('(prefers-color-scheme: dark)').matches; +} + +function prefersReducedMotion(): boolean { + return window.matchMedia('(prefers-reduced-motion: reduce)').matches; +} + +export default function App() { + const [pendingConfig, setPendingConfig] = useState(null); + const [isDark, setIsDark] = useState(readInitialDark); + const [themeUserSet, setThemeUserSet] = useState(() => { + try { return localStorage.getItem(THEME_KEY) !== null; } + catch { return false; } + }); + const [menuOpen, setMenuOpen] = useState(false); + const formRef = useRef(null); + + useEffect(() => { + const root = document.documentElement; + if (isDark) { + root.classList.remove('light'); + } else { + root.classList.add('light'); + } + }, [isDark]); + + useEffect(() => { + if (themeUserSet) return; + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + const onChange = (e: MediaQueryListEvent) => setIsDark(e.matches); + mq.addEventListener('change', onChange); + return () => mq.removeEventListener('change', onChange); + }, [themeUserSet]); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const config = configFromQueryParams(params); + if (config) { + setPendingConfig(config); + window.history.replaceState({}, '', window.location.pathname); + } + }, []); + + function toggleTheme() { + setIsDark(d => { + const next = !d; + try { localStorage.setItem(THEME_KEY, next ? 'dark' : 'light'); } + catch { /* ignore */ } + return next; + }); + setThemeUserSet(true); + } + + function handleRebuild(config: BuildConfig) { + setPendingConfig(config); + formRef.current?.scrollIntoView({ + behavior: prefersReducedMotion() ? 'auto' : 'smooth', + block: 'center', + }); + } + + const linkClass = 'text-sm text-gray-500 hover:text-gray-300 transition-colors'; + + return ( +
+
+ +
+
+
+ ArduPilot +
+
+ + + +
+
+ {menuOpen && ( + + )} +
+ +
+
+
+ +
+ +
+

+ ArduPilot +

+

+ CustomBuild +

+

+ Build exactly the firmware you need. Choose your vehicle, board, and feature set, + and we'll compile it for you. +

+

+ Versatile · Trusted · Open +

+
+ +
+ setPendingConfig(null)} + /> +
+
+ +
+
+ All Builds +
+
+
+ +
+ +
+ + +
+ ); +} diff --git a/frontend/src/api.ts b/frontend/src/api.ts new file mode 100644 index 00000000..90b3ec93 --- /dev/null +++ b/frontend/src/api.ts @@ -0,0 +1,91 @@ +import type { + Vehicle, Version, Board, Feature, Build, + BuildRequest, BuildSubmitResponse, StandardArtifact, +} from './types'; +import { parseConfigYaml, type BuildConfig } from './buildConfig'; + +const API_BASE = '/api/v1'; + +function apiUrl(...segments: string[]): string { + const path = segments.map(encodeURIComponent).join('/'); + return `${API_BASE}/${path}`; +} + +export function buildArtifactUrl(buildId: string): string { + return apiUrl('builds', buildId, 'artifact'); +} + +function buildConfigUrl(buildId: string): string { + return apiUrl('builds', buildId, 'config'); +} + +export async function fetchBuildConfig(buildId: string): Promise { + const res = await fetch(buildConfigUrl(buildId)); + if (!res.ok) throw new Error(`Config unavailable (${res.status})`); + return parseConfigYaml(await res.text()); +} + +export function commitUrl(remoteUrl: string, gitHash: string): string { + return `${remoteUrl.replace(/\.git$/, '')}/commit/${gitHash}`; +} + +async function apiFetch(url: string): Promise { + const res = await fetch(url); + if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`); + return res.json() as Promise; +} + +export async function fetchVehicles(): Promise { + return apiFetch(apiUrl('vehicles')); +} + +export async function fetchVersions(vehicleId: string): Promise { + return apiFetch(apiUrl('vehicles', vehicleId, 'versions')); +} + +export async function fetchBoards(vehicleId: string, versionId: string): Promise { + return apiFetch(apiUrl('vehicles', vehicleId, 'versions', versionId, 'boards')); +} + +export async function fetchFeatures(vehicleId: string, versionId: string, boardId: string): Promise { + return apiFetch( + apiUrl('vehicles', vehicleId, 'versions', versionId, 'boards', boardId, 'features'), + ); +} + +export async function fetchBuilds(limit = 10, offset = 0): Promise { + return apiFetch(`${apiUrl('builds')}?limit=${limit}&offset=${offset}`); +} + +export async function fetchBuild(buildId: string): Promise { + return apiFetch(apiUrl('builds', buildId)); +} + +export async function submitBuild(req: BuildRequest): Promise { + const res = await fetch(apiUrl('builds'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(req), + }); + if (!res.ok) throw new Error(`Submit error ${res.status}: ${await res.text()}`); + return res.json() as Promise; +} + +export async function fetchBuildLogs(buildId: string): Promise { + const res = await fetch(apiUrl('builds', buildId, 'logs')); + if (!res.ok) throw new Error('Logs unavailable'); + return res.text(); +} + +export async function fetchStandardArtifacts( + vehicleId: string, + versionId: string, + boardId: string, +): Promise { + const res = await fetch( + apiUrl('vehicles', vehicleId, 'versions', versionId, 'boards', boardId, 'standard_artifacts'), + ); + if (res.status === 404) return null; + if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`); + return res.json() as Promise; +} diff --git a/frontend/src/buildConfig.ts b/frontend/src/buildConfig.ts new file mode 100644 index 00000000..130ee7bd --- /dev/null +++ b/frontend/src/buildConfig.ts @@ -0,0 +1,109 @@ +import { load as yamlLoad } from 'js-yaml'; +import Ajv, { type ValidateFunction } from 'ajv/dist/2020'; + +const CONFIG_VERSION = '0.0.1'; + +interface BuildConfigVehicle { + id: string; + name: string; +} + +interface BuildConfigFirmwareVersion { + id: string; + name: string; + type: string; + remote_name: string; +} + +interface BuildConfigBoard { + id: string; + name: string; +} + +export interface BuildConfig { + config_version: string; + vehicle: BuildConfigVehicle; + version?: BuildConfigFirmwareVersion; + board?: BuildConfigBoard; + selected_features: string[]; + /** When true the config loader applies board defaults instead of selected_features */ + use_default_features?: boolean; +} + +/** Build a partial config from URL query parameters. Absent/empty params leave fields undefined. */ +export function configFromQueryParams(params: URLSearchParams): BuildConfig | null { + const vehicleId = params.get('vehicle_id'); + if (!vehicleId) return null; + + const versionId = params.get('version_id'); + const boardId = params.get('board_id'); + + return { + config_version: CONFIG_VERSION, + vehicle: { id: vehicleId, name: vehicleId }, + version: versionId + ? { id: versionId, name: versionId, type: 'tag', remote_name: '' } + : undefined, + board: boardId + ? { id: boardId, name: boardId } + : undefined, + selected_features: [], + use_default_features: true, + }; +} + +const schemaCache = new Map>(); +const validatorCache = new Map(); +const ajv = new Ajv({ allErrors: true }); + +async function fetchSchema(version: string): Promise> { + const cached = schemaCache.get(version); + if (cached) return cached; + const res = await fetch(`/schemas/config/${version}.json`); + if (!res.ok) + throw new Error(`No schema found for config version "${version}". The config file may be too new or unsupported.`); + const schema = await res.json() as Record; + schemaCache.set(version, schema); + return schema; +} + +async function getValidator(version: string): Promise { + const cached = validatorCache.get(version); + if (cached) return cached; + const schema = await fetchSchema(version); + const validate = ajv.compile(schema); + validatorCache.set(version, validate); + return validate; +} + +async function validateConfig(raw: unknown): Promise { + if (typeof raw !== 'object' || raw === null) + throw new Error('Config must be a YAML object'); + + const obj = raw as Record; + if (typeof obj.config_version !== 'string' || !obj.config_version.match(/^\d+\.\d+\.\d+$/)) + throw new Error('Invalid or missing "config_version" field (expected semver, e.g. "0.0.1")'); + + const validate = await getValidator(obj.config_version); + + if (!validate(raw)) { + const messages = (validate.errors ?? []) + .map(e => ` - ${e.instancePath || '(root)'} ${e.message}`) + .join('\n'); + throw new Error(`Config validation failed:\n${messages}`); + } + + return raw as BuildConfig; +} + +export async function parseConfigYaml(yamlText: string): Promise { + let raw: unknown; + try { + raw = yamlLoad(yamlText); + } catch (e: unknown) { + throw new Error( + `YAML parse error: ${e instanceof Error ? e.message : String(e)}` + ); + } + return validateConfig(raw); +} diff --git a/frontend/src/components/BuildForm.tsx b/frontend/src/components/BuildForm.tsx new file mode 100644 index 00000000..c42ec92d --- /dev/null +++ b/frontend/src/components/BuildForm.tsx @@ -0,0 +1,728 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { + Sliders, ChevronRight, Terminal, + ArrowRight, RefreshCw, PackageOpen, +} from 'lucide-react'; +import clsx from 'clsx'; +import type { + Vehicle, Version, Board, Feature, FormStep, StandardArtifact, BuildState, +} from '../types'; +import { + fetchVehicles, submitBuild, fetchStandardArtifacts, +} from '../api'; +import { + StepHeader, SearchableDropdown, VersionSelector, FormSection, ChosenPill, LoadingFiller, VehicleSelector, +} from './StepComponents'; +import { FeaturesModal } from './FeaturesModal'; +import { BuildInfoModal } from './BuildInfoModal'; +import { ConfigDropZone } from './ConfigDropZone'; +import { ConfigConflictModal } from './ConfigConflictModal'; +import { CollapsibleBanner } from './CollapsibleBanner'; +import { ErrorBanner } from './ErrorBanner'; +import { StandardArtifactsGrid } from './StandardArtifactsGrid'; +import { DoneStep } from './DoneStep'; +import { parseConfigYaml, type BuildConfig } from '../buildConfig'; +import { useBuildPolling } from '../hooks/useBuildPolling'; +import { useConfigLoad, type ConfigPhase } from '../hooks/useConfigLoad'; + +const STEPS = [ + { id: 'vehicle', label: 'Vehicle' }, + { id: 'version', label: 'Version' }, + { id: 'board', label: 'Board' }, +]; + +function headerStep(step: FormStep): string { + if (['choice', 'standard-files', 'features', 'building', 'done'].includes(step)) return '__done__'; + return step; +} + +function versionSupportsStandardArtifacts(version: Version): boolean { + return version.remote.name === 'ardupilot'; +} + +const TYPE_ORDER: Record = { stable: 0, beta: 1, latest: 2, tag: 3 }; + +function sortVersions(vs: Version[]): Version[] { + return [...vs].sort((a, b) => { + const typeDiff = TYPE_ORDER[a.type] - TYPE_ORDER[b.type]; + if (typeDiff !== 0) return typeDiff; + if (a.type === 'stable' || a.type === 'beta') { + const aParts = a.name.split('.').map(Number); + const bParts = b.name.split('.').map(Number); + for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { + const diff = (bParts[i] ?? 0) - (aParts[i] ?? 0); + if (diff !== 0) return diff; + } + } + return 0; + }); +} + +function versionBadge(t: Version['type']) { + return ({ + stable: 'badge-green', beta: 'badge-yellow', latest: 'badge-blue', tag: 'badge-gray', + } as const)[t]; +} + +interface BuildFormProps { + initialConfig?: BuildConfig | null; + onConsumeInitialConfig?: () => void; +} + +export function BuildForm({ initialConfig, onConsumeInitialConfig }: BuildFormProps) { + const [step, setStep] = useState('vehicle'); + const [vehicles, setVehicles] = useState([]); + const [versions, setVersions] = useState([]); + const [boards, setBoards] = useState([]); + const [features, setFeatures] = useState([]); + + const [vehicle, setVehicle] = useState(null); + const [version, setVersion] = useState(null); + const [board, setBoard] = useState(null); + const [selected, setSelected] = useState>(new Set()); + const [showFeatsModal, setShowFeatsModal] = useState(false); + const [showBuildModal, setShowBuildModal] = useState(false); + + const [buildId, setBuildId] = useState(null); + const [buildProgress, setBuildProgress] = useState(0); + const [buildState, setBuildState] = useState('PENDING'); + + const [standardArtifacts, setStandardArtifacts] = useState(null); + const [standardFilesLoading, setStandardFilesLoading] = useState(false); + + const [vehiclesLoading, setVehiclesLoading] = useState(false); + const [versionsLoading, setVersionsLoading] = useState(false); + const [boardsLoading, setBoardsLoading] = useState(false); + const [featuresLoading, setFeaturesLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const [error, setError] = useState(null); + + const [unavailableFeatures, setUnavailableFeatures] = useState([]); + const [dismissedUnavailable, setDismissedUnavailable] = useState(false); + const [autoAddedFeatures, setAutoAddedFeatures] = useState([]); + const [dismissedAutoAdded, setDismissedAutoAdded] = useState(false); + + const [featureConfigParseError, setFeatureConfigParseError] = useState(null); + + const [configPhase, setConfigPhase] = useState({ phase: 'idle' }); + const [configParseError, setConfigParseError] = useState(null); + + const loadGenRef = useRef(0); + const vehiclesRef = useRef([]); + const versionsRef = useRef([]); + const boardsRef = useRef([]); + const featuresRef = useRef([]); + const resetRef = useRef<() => void>(() => {}); + + useEffect(() => { vehiclesRef.current = vehicles; }, [vehicles]); + useEffect(() => { versionsRef.current = versions; }, [versions]); + useEffect(() => { boardsRef.current = boards; }, [boards]); + useEffect(() => { featuresRef.current = features; }, [features]); + + function bumpLoadGen(): number { + return ++loadGenRef.current; + } + + const { startPolling, stopBuildPolling } = useBuildPolling({ + setBuildProgress, + setBuildState, + setStep, + }); + + const reset = useCallback(() => { + bumpLoadGen(); + stopBuildPolling(); + setStep('vehicle'); + setVehicle(null); setVersion(null); setBoard(null); + setSelected(new Set()); setBuildId(null); + setBuildProgress(0); setBuildState('PENDING'); + setVersionsLoading(false); setBoardsLoading(false); + setFeaturesLoading(false); setSubmitting(false); + setError(null); + setUnavailableFeatures([]); setDismissedUnavailable(false); + setAutoAddedFeatures([]); setDismissedAutoAdded(false); + setFeatureConfigParseError(null); + setConfigPhase({ phase: 'idle' }); setConfigParseError(null); + setStandardArtifacts(null); setStandardFilesLoading(false); + }, [stopBuildPolling]); + + resetRef.current = reset; + + const { + startConfigLoad, + applyConfigFeatures, + handleConflictSelect, + handleConflictCancel, + loadVersions, + loadBoards, + loadDefaultFeatures, + } = useConfigLoad({ + loadGenRef, + bumpLoadGen, + reset: () => resetRef.current(), + vehiclesRef, + versionsRef, + boardsRef, + vehicle, + version, + setConfigPhase, + setError, + setVehicles, + setVersions, + setBoards, + setFeatures, + setVehicle, + setVersion, + setBoard, + setSelected, + setStep, + setVersionsLoading, + setBoardsLoading, + setFeaturesLoading, + setUnavailableFeatures, + setDismissedUnavailable, + setAutoAddedFeatures, + setDismissedAutoAdded, + }); + + function goToStep(next: FormStep) { + bumpLoadGen(); + setVersionsLoading(false); + setBoardsLoading(false); + setFeaturesLoading(false); + setStandardFilesLoading(false); + setError(null); + setFeatureConfigParseError(null); + setShowFeatsModal(false); + + if (next === 'vehicle') { + setVersion(null); + setBoard(null); + setVersions([]); + setBoards([]); + setFeatures([]); + setSelected(new Set()); + setStandardArtifacts(null); + setUnavailableFeatures([]); + setDismissedUnavailable(false); + setAutoAddedFeatures([]); + setDismissedAutoAdded(false); + } else if (next === 'version') { + setBoard(null); + setBoards([]); + setFeatures([]); + setSelected(new Set()); + setStandardArtifacts(null); + setUnavailableFeatures([]); + setDismissedUnavailable(false); + setAutoAddedFeatures([]); + setDismissedAutoAdded(false); + } else if (next === 'board') { + setFeatures([]); + setSelected(new Set()); + setStandardArtifacts(null); + setUnavailableFeatures([]); + setDismissedUnavailable(false); + setAutoAddedFeatures([]); + setDismissedAutoAdded(false); + } + + setStep(next); + } + + useEffect(() => { + const gen = bumpLoadGen(); + setVehiclesLoading(true); + fetchVehicles() + .then(data => { + if (gen !== loadGenRef.current) return; + setVehicles(data); + }) + .catch(() => { + if (gen !== loadGenRef.current) return; + setError('Failed to fetch vehicles from server'); + }) + .finally(() => { + if (gen !== loadGenRef.current) return; + setVehiclesLoading(false); + }); + }, []); + + const startConfigLoadRef = useRef(startConfigLoad); + startConfigLoadRef.current = startConfigLoad; + + useEffect(() => { + if (!initialConfig) return; + onConsumeInitialConfig?.(); + startConfigLoadRef.current(initialConfig); + }, [initialConfig, onConsumeInitialConfig]); + + const handleFileDrop = useCallback((yamlText: string) => { + setConfigParseError(null); + parseConfigYaml(yamlText) + .then(config => startConfigLoadRef.current(config)) + .catch((e: unknown) => setConfigParseError(e instanceof Error ? e.message : 'Invalid config file')); + }, []); + + const applyConfigFeaturesRef = useRef(applyConfigFeatures); + applyConfigFeaturesRef.current = applyConfigFeatures; + + const handleFeatureConfigDrop = useCallback((yamlText: string) => { + setFeatureConfigParseError(null); + parseConfigYaml(yamlText) + .then(config => { + setUnavailableFeatures([]); setDismissedUnavailable(false); + setAutoAddedFeatures([]); setDismissedAutoAdded(false); + applyConfigFeaturesRef.current(config, featuresRef.current); + }) + .catch((e: unknown) => + setFeatureConfigParseError(e instanceof Error ? e.message : 'Invalid config file'), + ); + }, []); + + function selectVehicle(id: string) { + const v = vehicles.find(x => x.id === id)!; + setStep('version'); + void loadVersions(v); + } + + function selectVersion(id: string) { + const v = versions.find(x => x.id === id)!; + setStandardArtifacts(null); + setStep('board'); + void loadBoards(vehicle!, v); + } + + function selectBoard(id: string) { + const b = boards.find(x => x.id === id)!; + setBoard(b); + setError(null); + setStep('choice'); + } + + function chooseCustom() { + void loadDefaultFeatures(vehicle!, version!, board!); + } + + function chooseStandard() { + const gen = bumpLoadGen(); + setStandardArtifacts(null); + setStandardFilesLoading(true); + setError(null); + setStep('standard-files'); + fetchStandardArtifacts(vehicle!.id, version!.id, board!.id) + .then(result => { + if (gen !== loadGenRef.current) return; + setStandardArtifacts(result); + }) + .catch(() => { + if (gen !== loadGenRef.current) return; + setStandardArtifacts(null); + setError('Failed to fetch standard build artifacts'); + }) + .finally(() => { + if (gen !== loadGenRef.current) return; + setStandardFilesLoading(false); + }); + } + + async function startBuild() { + try { + setSubmitting(true); + setError(null); + const res = await submitBuild({ + vehicle_id: vehicle!.id, + board_id: board!.id, + version_id: version!.id, + selected_features: Array.from(selected), + }); + setBuildId(res.build_id); + setBuildProgress(0); + setBuildState('PENDING'); + setStep('building'); + startPolling(res.build_id); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Build submit failed'); + } finally { + setSubmitting(false); + } + } + + const isConfigLoading = configPhase.phase === 'loading'; + const failedBuild = buildState === 'FAILURE' || buildState === 'ERROR' || buildState === 'TIMED_OUT'; + + return ( + <> +
+
+
+ goToStep(id as FormStep)) : undefined} + /> +
+ {step !== 'vehicle' && ( + + )} +
+ + {isConfigLoading && ( +
+ + Loading config… +
+ )} + + {step === 'vehicle' && ( +
+ + ({ id: v.id, name: v.name }))} + selected={vehicle?.id} + onSelect={selectVehicle} + /> + + +
+
+ or +
+
+ + + + +
+ )} + + {step === 'version' && ( +
+ goToStep('vehicle')} /> + + ({ + id: v.id, name: v.name, + type: v.type, badge: v.type, + badgeColor: versionBadge(v.type), + remote: v.remote.name !== 'ardupilot' ? v.remote.name : undefined, + }))} + selected={version?.id} + onSelect={selectVersion} + /> + + +
+ )} + + {step === 'board' && ( +
+ goToStep('vehicle')} /> + goToStep('version')} /> + + ({ id: b.id, name: b.name }))} + selected={board?.id} + onSelect={selectBoard} + placeholder="Choose a board…" + /> + + + +
+ )} + + {step === 'choice' && ( +
+ goToStep('vehicle')} /> + goToStep('version')} /> + goToStep('board')} /> + +
+ {version && versionSupportsStandardArtifacts(version) && ( + + )} + +
+
+ +
+ )} + + {step === 'standard-files' && ( +
+ goToStep('vehicle')} /> + goToStep('version')} /> + goToStep('board')} /> + {standardFilesLoading ? ( + +
+ {[1, 2, 3, 4, 5, 6, 7, 8].map(i => ( +
+ ))} +
+ + ) : standardArtifacts === null || standardArtifacts.length === 0 ? ( + +
+ + +
+
+ ) : ( + + )} +
+ )} + + {step === 'features' && ( +
+ goToStep('vehicle')} /> + goToStep('version')} /> + goToStep('board')} /> + + {autoAddedFeatures.length > 0 && !dismissedAutoAdded && ( + 1 ? 's were' : ' was'} auto-selected to satisfy dependencies not listed in your config:`} + items={autoAddedFeatures} + onDismiss={() => setDismissedAutoAdded(true)} + /> + )} + + {unavailableFeatures.length > 0 && !dismissedUnavailable && ( + 1 ? 's' : ''} from your config ${unavailableFeatures.length > 1 ? 'are' : 'is'} unavailable for this board/version combination and ${unavailableFeatures.length > 1 ? 'have' : 'has'} been skipped:`} + items={unavailableFeatures} + onDismiss={() => setDismissedUnavailable(true)} + /> + )} + + + {featuresLoading ? ( +
+
+
+
+
+
+
+ ) : ( +
+
+
+ {selected.size} / {features.length} features selected +
+

+ {unavailableFeatures.length > 0 + ? 'Config applied with adjustments — review below' + : 'Defaults pre-applied based on your board'} +

+
+ +
+ )} + + +
+
+ + or load feature selection from config + +
+
+ + {featuresLoading ? ( +
+
+
+
+
+ ) : ( + + Applies feature list only · vehicle/version/board in the file are ignored ·{' '} + .yaml /{' '} + .yml + + } + /> + )} + + + + + {featuresLoading ? ( + <> +
+ + + ) : ( + + )} +
+ )} + + {step === 'building' && ( +
+
+ + + + +
+ + +
+
+ {buildState} + + {buildProgress}% + +
+
+
+
+ {buildProgress === 0 ? ( +

+ + {buildState === 'PENDING' + ? 'Your build is in the queue and will start soon…' + : 'Setting up the build environment…'} +

+ ) : ( +

+ Compiling {vehicle?.name} with {selected.size} feature overrides for {board?.name}… +

+ )} +
+ + + +
+ )} + + {step === 'done' && ( +
+ setShowBuildModal(true)} + onRebuild={startConfigLoad} + /> +
+ )} +
+ + {configPhase.phase === 'conflict' && ( + handleConflictSelect(id, configPhase)} + onCancel={handleConflictCancel} + /> + )} + + {showFeatsModal && ( + { setSelected(sel); setShowFeatsModal(false); }} + onClose={() => setShowFeatsModal(false)} + /> + )} + + {showBuildModal && buildId && ( + setShowBuildModal(false)} + /> + )} + + ); +} diff --git a/frontend/src/components/BuildInfoModal.tsx b/frontend/src/components/BuildInfoModal.tsx new file mode 100644 index 00000000..193d6d4c --- /dev/null +++ b/frontend/src/components/BuildInfoModal.tsx @@ -0,0 +1,410 @@ +import { useEffect, useRef, useState } from 'react'; +import { + X, Download, Terminal, Info, Package, Loader2, Clock, +} from 'lucide-react'; +import clsx from 'clsx'; +import type { Build, BuildState } from '../types'; +import { fetchBuild, fetchBuildLogs, buildArtifactUrl, commitUrl } from '../api'; +import { ModalShell } from './ModalShell'; +import { StateBadge, VersionTypeBadge, formatAge, isTerminal } from './buildStatus'; + +function colorize(line: string) { + if (line.startsWith('[SUCCESS]')) return 'text-emerald-400'; + if (line.startsWith('[ERROR]') || line.startsWith('[FAIL]')) return 'text-red-400'; + if (line.startsWith('[WARN]')) return 'text-orange-400'; + if (line.startsWith('[INFO]')) return 'text-blue-400'; + return 'text-gray-400'; +} + +type Tab = 'details' | 'logs'; + +interface BuildInfoModalProps { + buildId: string; + initialTab?: Tab; + initialFeaturesExpanded?: boolean; + onClose: () => void; +} + +export function BuildInfoModal({ buildId, initialTab = 'details', initialFeaturesExpanded = false, onClose }: BuildInfoModalProps) { + const [tab, setTab] = useState(initialTab); + const [build, setBuild] = useState(null); + const [loadingBuild, setLoadingBuild] = useState(true); + const [logs, setLogs] = useState(''); + const [logsLoading, setLogsLoading] = useState(false); + const [logsError, setLogsError] = useState(null); + + const pollRef = useRef>(); + const logPollRef = useRef>(); + const buildStateRef = useRef(); + + useEffect(() => { + buildStateRef.current = build?.progress.state; + }, [build?.progress.state]); + + useEffect(() => { + let alive = true; + setLoadingBuild(true); + setBuild(null); + + const clearPoll = () => { + clearTimeout(pollRef.current); + pollRef.current = undefined; + }; + + const schedulePoll = () => { + clearPoll(); + pollRef.current = setTimeout(async () => { + if (!alive) return; + try { + const updated = await fetchBuild(buildId); + if (!alive) return; + setBuild(updated); + if (isTerminal(updated.progress.state)) { + clearPoll(); + return; + } + } catch { /* ignore */ } + if (alive) schedulePoll(); + }, 2000); + }; + + async function load() { + try { + const b = await fetchBuild(buildId); + if (!alive) return; + setBuild(b); + setLoadingBuild(false); + if (!isTerminal(b.progress.state)) schedulePoll(); + } catch { + if (alive) setLoadingBuild(false); + } + } + + load(); + return () => { + alive = false; + clearPoll(); + }; + }, [buildId]); + + // Logs fetching / polling — serialized self-scheduling + useEffect(() => { + if (tab !== 'logs') { + clearTimeout(logPollRef.current); + logPollRef.current = undefined; + return; + } + + let alive = true; + setLogsError(null); + + const clearLogPoll = () => { + clearTimeout(logPollRef.current); + logPollRef.current = undefined; + }; + + const scheduleLogPoll = () => { + clearLogPoll(); + const state = buildStateRef.current; + if (state && isTerminal(state)) return; + logPollRef.current = setTimeout(runLogFetch, 3000); + }; + + async function runLogFetch() { + if (!alive) return; + setLogsLoading(true); + try { + const l = await fetchBuildLogs(buildId); + if (!alive) return; + setLogs(l); + setLogsError(null); + } catch { + if (!alive) return; + setLogsError('Failed to refresh logs'); + } finally { + if (alive) setLogsLoading(false); + } + if (alive) scheduleLogPoll(); + } + + runLogFetch(); + return () => { + alive = false; + clearLogPoll(); + }; + }, [tab, buildId]); + + useEffect(() => { + const state = build?.progress.state; + if (state && isTerminal(state)) { + clearTimeout(logPollRef.current); + logPollRef.current = undefined; + } + }, [build?.progress.state]); + + const state = build?.progress.state ?? 'PENDING'; + const terminal = isTerminal(state); + + return ( + +
+
+ +

Build Info

+ {buildId} + {build && } +
+ +
+ +
+ setTab('details')}> + + Details + + setTab('logs')}> + + Logs + {!terminal && build && ( + + )} + +
+ +
+ {loadingBuild ? ( +
+ + Loading… +
+ ) : tab === 'details' ? ( + + ) : ( + + )} +
+ +
+ {!terminal && build ? ( +
+
+
+
+ + {build.progress.percent}% + +
+ ) : ( +
+ )} + + +
+ + ); +} + +function TabButton({ active, onClick, children }: { + active: boolean; + onClick: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +function FeaturesList({ features, initialExpanded = false }: { features: string[]; initialExpanded?: boolean }) { + const [showAll, setShowAll] = useState(initialExpanded); + const CAP = 20; + const visible = showAll ? features : features.slice(0, CAP); + const overflow = features.length - CAP; + return ( +
+

Selected features

+
+ {visible.map(f => ( + + {f} + + ))} + {!showAll && overflow > 0 && ( + + )} + {showAll && overflow > 0 && ( + + )} +
+
+ ); +} + +function DetailsPane({ build, initialFeaturesExpanded }: { build: Build | null; initialFeaturesExpanded?: boolean }) { + if (!build) return

No data

; + + const rows: [string, React.ReactNode][] = [ + ['Vehicle', build.vehicle.name], + ['Board', build.board.name], + ['Version', ( + + {build.version.name ?? build.version.id} + {build.version.type && } + + )], + ['Git hash', ( + + {build.version.git_hash.substring(0, 8)} + + )], + ['Created', {formatAge(build.time_created)}], + ['Progress', `${build.progress.percent}%`], + ['Features', build.selected_features.length === 0 + ? none + : {build.selected_features.length} selected + ], + ]; + + return ( +
+ {rows.map(([label, value]) => ( +
+ {label} + {value} +
+ ))} + + {build.selected_features.length > 0 && ( + + )} +
+ ); +} + +function LogsPane({ logs, loading, error }: { + logs: string; + loading: boolean; + error: string | null; +}) { + const containerRef = useRef(null); + const [autoScroll, setAutoScroll] = useState(true); + const autoScrollRef = useRef(true); + + const NEAR_BOTTOM = 120; + const MAX_LINES = 2000; + + const allLines = logs ? logs.split('\n') : []; + const omitted = Math.max(0, allLines.length - MAX_LINES); + const lines = omitted > 0 ? allLines.slice(-MAX_LINES) : allLines; + + const scrollToBottom = () => { + const el = containerRef.current; + if (!el) return; + el.scrollTop = el.scrollHeight; + autoScrollRef.current = true; + setAutoScroll(true); + }; + + const handleScroll = () => { + const el = containerRef.current; + if (!el) return; + const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM; + if (autoScrollRef.current !== atBottom) { + autoScrollRef.current = atBottom; + setAutoScroll(atBottom); + } + }; + + useEffect(() => { + const el = containerRef.current; + if (!el || !autoScrollRef.current) return; + el.scrollTop = el.scrollHeight; + }, [logs]); + + return ( +
+ {error && ( +
+ {error} +
+ )} +
+ {loading && !logs ? ( +
+ + Fetching logs… +
+ ) : ( + <> + {omitted > 0 && ( +
+ … {omitted.toLocaleString()} earlier lines omitted +
+ )} + {lines.map((line, i) => ( +
{line ||
}
+ ))} + + )} +
+ {!autoScroll && ( + + )} +
+ ); +} diff --git a/frontend/src/components/BuildsTable.tsx b/frontend/src/components/BuildsTable.tsx new file mode 100644 index 00000000..cd123fbf --- /dev/null +++ b/frontend/src/components/BuildsTable.tsx @@ -0,0 +1,342 @@ +import { useState, useEffect, useRef } from 'react'; +import { + ChevronLeft, ChevronRight, + Clock, RotateCcw, Info, Download, Loader2, +} from 'lucide-react'; +import clsx from 'clsx'; +import type { Build } from '../types'; +import { fetchBuilds, commitUrl, buildArtifactUrl, fetchBuildConfig } from '../api'; +import { BuildInfoModal } from './BuildInfoModal'; +import type { BuildConfig } from '../buildConfig'; +import { Tooltip } from './Tooltip'; +import { + NON_TERMINAL_BUILD_STATES, + isTerminal, + StateBadge, + VersionTypeBadge, + formatAge, +} from './buildStatus'; + +const PAGE_SIZE = 5; +const POLL_INTERVAL_MS = 4000; + +// Fixed tracks so header/rows share identical widths. Actions must fit 3 icon buttons +// (~26px each + gaps); too-narrow tracks overflow and look like header/row drift. +const COLS = 'grid-cols-[140px_130px_140px_minmax(220px,1fr)_110px_96px_128px]'; +const TABLE_MIN = 'min-w-[1100px]'; + + +function VersionCell({ version }: { version: Build['version'] }) { + const label = version.name ?? version.id; + return ( +
+
+ {label} + {version.type && } +
+ + {version.git_hash.slice(0, 10)} + +
+ ); +} + +interface BuildsTableProps { + onRebuild?: (config: BuildConfig) => void; +} + +export function BuildsTable({ onRebuild }: BuildsTableProps = {}) { + const [builds, setBuilds] = useState([]); + const [page, setPage] = useState(0); + const [hasMore, setHasMore] = useState(false); + const [loading, setLoading] = useState(true); + const [selectedBuildId, setSelectedBuildId] = useState(null); + const [modalInitialTab, setModalInitialTab] = useState<'details' | 'logs'>('details'); + const [modalFeaturesExpanded, setModalFeaturesExpanded] = useState(false); + const [rebuildLoadingId, setRebuildLoadingId] = useState(null); + const [rebuildError, setRebuildError] = useState(null); + const sectionRef = useRef(null); + const pollTimerRef = useRef | null>(null); + const pageRef = useRef(page); + const fetchGenRef = useRef(0); + const inFlightRef = useRef(false); + pageRef.current = page; + + function openBuild(b: Build) { + const tab = NON_TERMINAL_BUILD_STATES.includes(b.progress.state) ? 'logs' : 'details'; + setModalInitialTab(tab); + setModalFeaturesExpanded(false); + setSelectedBuildId(b.build_id); + } + + function openBuildFeatures(b: Build) { + setModalInitialTab('details'); + setModalFeaturesExpanded(true); + setSelectedBuildId(b.build_id); + } + + async function handleRebuildClick(buildId: string) { + if (!onRebuild || rebuildLoadingId) return; + setRebuildError(null); + setRebuildLoadingId(buildId); + try { + const config = await fetchBuildConfig(buildId); + onRebuild(config); + } catch (e: unknown) { + setRebuildError( + e instanceof Error ? e.message : `Failed to load config for ${buildId}`, + ); + } finally { + setRebuildLoadingId(null); + } + } + + function doFetch(p: number, isInitial = false) { + if (!isInitial && inFlightRef.current) return; + + const gen = ++fetchGenRef.current; + inFlightRef.current = true; + if (isInitial) setLoading(true); + + fetchBuilds(PAGE_SIZE + 1, p * PAGE_SIZE) + .then(data => { + if (gen !== fetchGenRef.current) return; + setHasMore(data.length > PAGE_SIZE); + setBuilds(data.slice(0, PAGE_SIZE)); + }) + .finally(() => { + if (gen === fetchGenRef.current) { + inFlightRef.current = false; + if (isInitial) setLoading(false); + } + }); + } + + const doFetchRef = useRef(doFetch); + doFetchRef.current = doFetch; + + useEffect(() => { + inFlightRef.current = false; + doFetchRef.current(pageRef.current, true); + }, [page]); + + useEffect(() => { + const el = sectionRef.current; + if (!el) return; + + function startPolling() { + if (pollTimerRef.current) return; + pollTimerRef.current = setInterval(() => { + doFetchRef.current(pageRef.current); + }, POLL_INTERVAL_MS); + } + + function stopPolling() { + if (pollTimerRef.current) { + clearInterval(pollTimerRef.current); + pollTimerRef.current = null; + } + } + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) startPolling(); + else stopPolling(); + }, + { threshold: 0.1 } + ); + observer.observe(el); + + return () => { observer.disconnect(); stopPolling(); }; + }, []); + + return ( + <> +
+
+

Recent Builds

+

All builds across the server

+
+ +
+ {rebuildError && ( +
+ {rebuildError} +
+ )} +
+
+
+ {([ + { top: 'Build ID' }, + { top: 'Vehicle', btm: 'Board' }, + { top: 'Version', btm: 'Git SHA' }, + { top: 'Features' }, + { top: 'Status' }, + { top: 'Age' }, + { top: 'Actions' }, + ] as { top: string; btm?: string }[]).map(({ top, btm }) => ( +
+ + {top}{btm && /} + + {btm && {btm}} +
+ ))} +
+ + {loading ? ( +
+ {Array.from({ length: PAGE_SIZE }).map((_, i) => ( +
+ {Array.from({ length: 7 }).map((_, j) => ( +
+ ))} +
+ ))} +
+ ) : builds.length === 0 ? ( +
No builds found
+ ) : ( +
+ {builds.map(b => { + const terminal = isTerminal(b.progress.state); + return ( +
+
+ +
+ +
+
{b.vehicle.name}
+
{b.board.name}
+
+ + + +
+ {b.selected_features.length === 0 ? ( + none + ) : ( +
+ {b.selected_features.slice(0, 5).map(f => ( + + {f} + + ))} + {b.selected_features.length > 5 && ( + + )} +
+ )} +
+ +
+ +
+ +
+ + {formatAge(b.time_created)} +
+ +
+ + + + + + + + + +
+
+ ); + })} +
+ )} +
+
+ + {(page > 0 || hasMore) && ( +
+ + Page {page + 1} + +
+ + +
+
+ )} +
+
+ + {selectedBuildId && ( + setSelectedBuildId(null)} + /> + )} + + ); +} diff --git a/frontend/src/components/CollapsibleBanner.tsx b/frontend/src/components/CollapsibleBanner.tsx new file mode 100644 index 00000000..439bdb59 --- /dev/null +++ b/frontend/src/components/CollapsibleBanner.tsx @@ -0,0 +1,82 @@ +import { useState } from 'react'; +import { AlertTriangle, XCircle, ChevronDown, ChevronUp } from 'lucide-react'; +import clsx from 'clsx'; + +interface CollapsibleBannerProps { + items: string[]; + message: string; + color: 'yellow' | 'blue'; + onDismiss: () => void; + collapseAfter?: number; +} + +const COLORS = { + yellow: { + border: 'border-yellow-400/30', + bg: 'bg-yellow-400/5', + icon: 'text-yellow-400', + heading: 'text-yellow-300', + expand: 'text-yellow-400 hover:text-yellow-300', + }, + blue: { + border: 'border-blue-400/30', + bg: 'bg-blue-400/5', + icon: 'text-blue-400', + heading: 'text-blue-300', + expand: 'text-blue-400 hover:text-blue-300', + }, +}; + +export function CollapsibleBanner({ + items, + message, + color, + onDismiss, + collapseAfter = 4, +}: CollapsibleBannerProps) { + const [expanded, setExpanded] = useState(false); + const c = COLORS[color]; + const collapsible = items.length > collapseAfter; + const visible = expanded || !collapsible ? items : items.slice(0, collapseAfter); + + return ( +
+
+
+ +
+

{message}

+
    + {visible.map(f => ( +
  • • {f}
  • + ))} +
+ {collapsible && ( + + )} +
+
+ +
+
+ ); +} diff --git a/frontend/src/components/ConfigConflictModal.tsx b/frontend/src/components/ConfigConflictModal.tsx new file mode 100644 index 00000000..2d0fa5f4 --- /dev/null +++ b/frontend/src/components/ConfigConflictModal.tsx @@ -0,0 +1,134 @@ +import { useState } from 'react'; +import { AlertTriangle, X, ChevronRight } from 'lucide-react'; +import clsx from 'clsx'; +import { ModalShell } from './ModalShell'; + +export type ConflictKind = 'vehicle' | 'version' | 'board'; + +interface Option { + id: string; + name: string; + badge?: string; +} + +interface ConfigConflictModalProps { + kind: ConflictKind; + requested: { id: string; name: string }; + available: Option[]; + onSelect: (id: string) => void; + onCancel: () => void; +} + +const LABELS: Record = { + vehicle: 'Vehicle', + version: 'Firmware version', + board: 'Board', +}; + +export function ConfigConflictModal({ + kind, + requested, + available, + onSelect, + onCancel, +}: ConfigConflictModalProps) { + const [selected, setSelected] = useState(null); + const [query, setQuery] = useState(''); + + const q = query.toLowerCase(); + const filtered = available.filter(o => + o.name.toLowerCase().includes(q) || o.id.toLowerCase().includes(q), + ); + + const label = LABELS[kind]; + + return ( + +
+
+ +
+

+ {label} not available +

+

+ The config references{' '} + + {requested.name} + {' '} + ({requested.id}), which is not currently listed on the + server. Select an alternative to continue. +

+
+
+ +
+ +
+ setQuery(e.target.value)} + placeholder={`Search ${label.toLowerCase()}…`} + className="w-full bg-surface-3 border border-surface-4 rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 outline-none focus:ring-1 focus:ring-yellow-400/50 shrink-0" + /> + +
+ {filtered.length === 0 ? ( +

+ No results +

+ ) : ( + filtered.map(opt => ( + + )) + )} +
+
+ +
+ + +
+
+ ); +} diff --git a/frontend/src/components/ConfigDropZone.tsx b/frontend/src/components/ConfigDropZone.tsx new file mode 100644 index 00000000..38d5a609 --- /dev/null +++ b/frontend/src/components/ConfigDropZone.tsx @@ -0,0 +1,101 @@ +import { useState, useRef, useCallback, useId, type ReactNode } from 'react'; +import { UploadCloud, FileText, AlertTriangle } from 'lucide-react'; +import clsx from 'clsx'; + +interface ConfigDropZoneProps { + onLoad: (yamlText: string, fileName: string) => void; + title?: string; + hint?: ReactNode; +} + +export function ConfigDropZone({ + onLoad, + title = 'Load config file', + hint, +}: ConfigDropZoneProps) { + const secondaryHint = hint ?? ( + <> + Drag & drop or click · .yaml /{' '} + .yml + + ); + const [dragging, setDragging] = useState(false); + const [error, setError] = useState(null); + const onLoadRef = useRef(onLoad); + onLoadRef.current = onLoad; + const inputId = useId(); + + const readFile = useCallback((file: File) => { + if (!file.name.match(/\.(yaml|yml)$/i)) { + setError('Only .yaml or .yml files are supported'); + return; + } + setError(null); + const reader = new FileReader(); + reader.onload = e => onLoadRef.current((e.target?.result as string) ?? '', file.name); + reader.onerror = () => setError('Failed to read file'); + reader.readAsText(file); + }, []); + + const onDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setDragging(false); + const file = e.dataTransfer.files[0]; + if (file) readFile(file); + }, [readFile]); + + const onDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setDragging(true); + }; + const onDragLeave = () => setDragging(false); + + const onInputChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) readFile(file); + e.target.value = ''; + }; + + return ( +
+ + {error && ( +

+ + {error} +

+ )} +
+ ); +} diff --git a/frontend/src/components/DoneStep.tsx b/frontend/src/components/DoneStep.tsx new file mode 100644 index 00000000..7d745440 --- /dev/null +++ b/frontend/src/components/DoneStep.tsx @@ -0,0 +1,128 @@ +import { + CheckCircle2, XCircle, AlertTriangle, + Download, Copy, Terminal, +} from 'lucide-react'; +import clsx from 'clsx'; +import type { Vehicle, Version, Board, BuildState } from '../types'; +import { buildArtifactUrl } from '../api'; +import type { BuildConfig } from '../buildConfig'; + +interface DoneStepProps { + buildState: BuildState; + buildId: string | null; + vehicle: Vehicle | null; + version: Version | null; + board: Board | null; + selected: Set; + onViewLogs: () => void; + onRebuild: (config: BuildConfig) => void; +} + +export function DoneStep({ + buildState, buildId, vehicle, version, board, selected, + onViewLogs, onRebuild, +}: DoneStepProps) { + const isSuccess = buildState === 'SUCCESS'; + const isTimeout = buildState === 'TIMED_OUT'; + const icon = isSuccess + ? + : isTimeout + ? + : ; + const message = isSuccess ? 'Build complete!' : isTimeout ? 'Build timed out' : 'Build failed'; + + function makeConfig(): BuildConfig | null { + if (!vehicle || !version || !board) return null; + return { + config_version: '0.0.1', + vehicle: { id: vehicle.id, name: vehicle.name }, + version: { + id: version.id, + name: version.name, + type: version.type, + remote_name: version.remote.name, + }, + board: { id: board.id, name: board.name }, + selected_features: Array.from(selected), + }; + } + + return ( +
+
+ {icon} +
+
{message}
+
+ {buildId && ( + Build ID: {buildId} + )} + {vehicle && ( + Vehicle: {vehicle.name} + )} + {vehicle && board && ·} + {board && ( + Board: {board.name} + )} + {board && buildId && ·} +
+
+
+ +
+ + + {isSuccess && ( + + )} + + {isSuccess ? ( + + + Download Bundle + + ) : ( + + )} +
+
+ ); +} diff --git a/frontend/src/components/ErrorBanner.tsx b/frontend/src/components/ErrorBanner.tsx new file mode 100644 index 00000000..0726c3aa --- /dev/null +++ b/frontend/src/components/ErrorBanner.tsx @@ -0,0 +1,11 @@ +import { AlertTriangle } from 'lucide-react'; + +export function ErrorBanner({ message }: { message: string | null }) { + if (!message) return null; + return ( +

+ + {message} +

+ ); +} diff --git a/frontend/src/components/FeaturesGraphView.tsx b/frontend/src/components/FeaturesGraphView.tsx new file mode 100644 index 00000000..a0a06aa3 --- /dev/null +++ b/frontend/src/components/FeaturesGraphView.tsx @@ -0,0 +1,1003 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type MouseEvent as ReactMouseEvent, +} from 'react'; +import { createPortal } from 'react-dom'; +import { + ReactFlow, + Background, + BaseEdge, + MarkerType, + Position, + Handle, + getBezierPath, + useReactFlow, + ReactFlowProvider, + useNodesState, + useEdgesState, + useStore, + type Node, + type Edge, + type EdgeProps, + type NodeProps, + BackgroundVariant, +} from '@xyflow/react'; +import { + Search, X, Check, EyeOff, Eye, ZoomIn, ZoomOut, LayoutGrid, Scan, +} from 'lucide-react'; +import clsx from 'clsx'; +import type { Feature } from '../types'; +import { + collectTransitiveDeps, + collectTransitiveDependents, + reducedDependencyEdges, +} from '../featureDeps'; +import { searchFeatures } from '../featureSearch'; +import '@xyflow/react/dist/style.css'; + +const NODE_WIDTH = 220; +const NODE_HEIGHT = 64; +const MIN_ZOOM = 0.02; +const MAX_ZOOM = 2; +const RANK_SEP = 180; +const NODE_GAP = 24; +const ROOT_GAP = 40; +const STANDALONE_GAP_Y = 48; +const FIT_PADDING = 0.12; +const INFO_CARD_DELAY_MS = 500; +const INFO_CARD_WIDTH = 280; +const INFO_CARD_GAP = 8; +const INFO_CARD_EST_H = 148; + +interface FeatureNodeData extends Record { + feature: Feature; + searchHighlight: boolean; + focused: boolean; // Subtree-view root + dimmed: boolean; + treeActive: boolean; + onFocus: (id: string) => void; + onShowAll: () => void; +} + +interface FeatureEdgeData extends Record { + treeActive: boolean; + dimmed: boolean; +} + +const HANDLE_CLASS = '!w-2 !h-2 !bg-gray-500 !border-0 !pointer-events-none'; + +interface SelectionCtx { + selected: Set; + onToggle: (id: string) => void; +} + +const FeatureGraphSelectionContext = createContext(null); + +function useSelection() { + const ctx = useContext(FeatureGraphSelectionContext); + if (!ctx) throw new Error('FeatureGraphSelectionContext missing'); + return ctx; +} + +function FeatureEdge({ + id, + sourceX, + sourceY, + targetX, + targetY, + data, + markerEnd, + style, +}: EdgeProps>) { + const treeActive = data?.treeActive ?? false; + const dimmed = data?.dimmed ?? false; + + const [path] = getBezierPath({ + sourceX, + sourceY, + sourcePosition: Position.Left, + targetX, + targetY, + targetPosition: Position.Right, + }); + const stroke = treeActive + ? 'rgb(250, 204, 21)' + : 'rgb(107, 114, 128)'; + + return ( + + ); +} + +function FeatureGraphNode({ data }: NodeProps>) { + const { + feature, searchHighlight, focused, dimmed, treeActive, onFocus, onShowAll, + } = data; + const { selected, onToggle } = useSelection(); + const checked = selected.has(feature.id); + + function handleToggle(e: ReactMouseEvent) { + e.preventDefault(); + e.stopPropagation(); + onToggle(feature.id); + } + + function handleCardClick(e: ReactMouseEvent) { + e.preventDefault(); + e.stopPropagation(); + if (focused) onShowAll(); + else onFocus(feature.id); + } + + return ( +
e.stopPropagation()} + onPointerDown={(e: ReactMouseEvent) => e.stopPropagation()} + > + +
+ +
+
+ {feature.name} +
+
+ {feature.category.name} +
+
+
+ +
+ ); +} + +const nodeTypes = { feature: FeatureGraphNode }; +const edgeTypes = { feature: FeatureEdge }; + +function isStandalone( + id: string, + forwardDeps: Map>, + reverseDeps: Map>, +): boolean { + const outs = forwardDeps.get(id); + const ins = reverseDeps.get(id); + return (!outs || outs.size === 0) && (!ins || ins.size === 0); +} + +/** Ancestors + self + descendants for hover/focus trees. */ +function relatedTreeIds( + id: string, + forwardDeps: Map>, + reverseDeps: Map>, + featureMap: Map, +): Set { + const deps = collectTransitiveDeps(id, forwardDeps, featureMap); + const dependents = collectTransitiveDependents(id, reverseDeps); + const set = new Set([id]); + deps.forEach(d => set.add(d)); + dependents.forEach(d => set.add(d)); + return set; +} + +/** Longest-path column rank: roots = 0; else 1 + max(dep ranks). */ +function computeRanks( + connectedIds: Set, + forwardDeps: Map>, +): Map { + const ranks = new Map(); + const visiting = new Set(); + + function rankOf(id: string): number { + const cached = ranks.get(id); + if (cached !== undefined) return cached; + if (visiting.has(id)) return 0; + visiting.add(id); + + let maxDep = -1; + for (const depId of forwardDeps.get(id) ?? []) { + if (!connectedIds.has(depId)) continue; + maxDep = Math.max(maxDep, rankOf(depId)); + } + visiting.delete(id); + + const r = maxDep < 0 ? 0 : maxDep + 1; + ranks.set(id, r); + return r; + } + + for (const id of connectedIds) rankOf(id); + return ranks; +} + +/** + * Primary layout parent = dependency with maximum rank (deepest / rightmost). + * Matches longest-path columns so packing stays under the deepest dep. + */ +function buildLayoutChildren( + connectedIds: Set, + forwardDeps: Map>, + ranks: Map, +): { children: Map; roots: string[] } { + const children = new Map(); + const roots: string[] = []; + + const sortedIds = [...connectedIds].sort((a, b) => a.localeCompare(b)); + + for (const id of sortedIds) { + const inSetDeps = [...(forwardDeps.get(id) ?? [])].filter(d => connectedIds.has(d)); + if (inSetDeps.length === 0) { + roots.push(id); + continue; + } + + // Prefer deepest (highest rank) dependency as layout parent + inSetDeps.sort((a, b) => { + const rd = (ranks.get(b) ?? 0) - (ranks.get(a) ?? 0); + return rd !== 0 ? rd : a.localeCompare(b); + }); + const parent = inSetDeps[0]; + if (!children.has(parent)) children.set(parent, []); + children.get(parent)!.push(id); + } + + for (const [, kids] of children) { + kids.sort((a, b) => a.localeCompare(b)); + } + roots.sort((a, b) => a.localeCompare(b)); + + return { children, roots }; +} + +function computeSubtreeHeights( + roots: string[], + children: Map, +): Map { + const heights = new Map(); + + function heightOf(id: string): number { + const cached = heights.get(id); + if (cached !== undefined) return cached; + const kids = children.get(id) ?? []; + let h: number; + if (kids.length === 0) { + h = NODE_HEIGHT; + } else { + const sum = kids.reduce((acc, k) => acc + heightOf(k), 0); + h = Math.max(sum + NODE_GAP * (kids.length - 1), NODE_HEIGHT); + } + heights.set(id, h); + return h; + } + + for (const r of roots) heightOf(r); + return heights; +} + +function placeTree( + id: string, + top: number, + ranks: Map, + children: Map, + heights: Map, + positions: Map, +): void { + const kids = children.get(id) ?? []; + const blockH = heights.get(id) ?? NODE_HEIGHT; + const rank = ranks.get(id) ?? 0; + const x = rank * (NODE_WIDTH + RANK_SEP); + + if (kids.length === 0) { + positions.set(id, { x, y: top }); + return; + } + + let cursor = top; + for (const kid of kids) { + placeTree(kid, cursor, ranks, children, heights, positions); + cursor += (heights.get(kid) ?? NODE_HEIGHT) + NODE_GAP; + } + + const firstKid = positions.get(kids[0])!; + const lastKid = positions.get(kids[kids.length - 1])!; + const y = (firstKid.y + lastKid.y + NODE_HEIGHT) / 2 - NODE_HEIGHT / 2; + const minY = top; + const maxY = top + blockH - NODE_HEIGHT; + positions.set(id, { x, y: Math.min(maxY, Math.max(minY, y)) }); +} + +function buildLayout( + features: Feature[], + forwardDeps: Map>, + reverseDeps: Map>, + hideStandalone: boolean, +): { + positions: Map; + visible: Feature[]; + edgePairs: Array<{ source: string; target: string }>; +} { + const connected = features.filter(f => !isStandalone(f.id, forwardDeps, reverseDeps)); + const standalones = hideStandalone + ? [] + : features.filter(f => isStandalone(f.id, forwardDeps, reverseDeps)); + + const connectedIds = new Set(connected.map(f => f.id)); + const positions = new Map(); + // Graph edges: covering only (selection still uses full forwardDeps) + const edgePairs = reducedDependencyEdges(connectedIds, forwardDeps); + + if (connected.length > 0) { + const ranks = computeRanks(connectedIds, forwardDeps); + const { children, roots } = buildLayoutChildren(connectedIds, forwardDeps, ranks); + const heights = computeSubtreeHeights(roots, children); + + let cursor = 0; + for (const root of roots) { + placeTree(root, cursor, ranks, children, heights, positions); + cursor += (heights.get(root) ?? NODE_HEIGHT) + ROOT_GAP; + } + + for (const id of connectedIds) { + if (!positions.has(id)) { + positions.set(id, { x: 0, y: cursor }); + cursor += NODE_HEIGHT + NODE_GAP; + } + } + } + + if (standalones.length > 0) { + let maxY = 0; + if (positions.size > 0) { + maxY = Math.max(...[...positions.values()].map(p => p.y + NODE_HEIGHT)); + } + const startY = positions.size > 0 ? maxY + STANDALONE_GAP_Y : 0; + standalones + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .forEach((f, i) => { + positions.set(f.id, { + x: 0, + y: startY + i * (NODE_HEIGHT + NODE_GAP), + }); + }); + } + + return { positions, visible: [...connected, ...standalones], edgePairs }; +} + +function buildFlowElements( + visible: Feature[], + positions: Map, + edgePairs: Array<{ source: string; target: string }>, + searchHighlightId: string | null, + focusedId: string | null, + hoverTree: Set | null, + onFocus: (id: string) => void, + onShowAll: () => void, +): { nodes: Node[]; edges: Edge[] } { + const hovering = hoverTree !== null && hoverTree.size > 0; + + const nodes: Node[] = visible.map(f => { + const pos = positions.get(f.id)!; + const inTree = hovering && hoverTree!.has(f.id); + return { + id: f.id, + type: 'feature', + position: pos, + className: 'nopan nodrag', + data: { + feature: f, + searchHighlight: searchHighlightId === f.id, + focused: focusedId === f.id, + dimmed: hovering && !inTree, + treeActive: !!inTree, + onFocus, + onShowAll, + }, + }; + }); + + const edges: Edge[] = edgePairs.map(({ source, target }) => { + const inTree = hovering && hoverTree!.has(source) && hoverTree!.has(target); + const dimmed = hovering && !inTree; + const stroke = inTree ? 'rgb(250, 204, 21)' : 'rgb(107, 114, 128)'; + return { + id: `${source}->${target}`, + source, + target, + type: 'feature', + data: { + treeActive: !!inTree, + dimmed, + }, + markerEnd: { + type: MarkerType.ArrowClosed, + width: 16, + height: 16, + color: stroke, + }, + }; + }); + + return { nodes, edges }; +} + +function ZoomSlider() { + const { zoomTo, zoomIn, zoomOut, fitView } = useReactFlow(); + const zoom = useStore(s => s.transform[2]); + + return ( +
+ + zoomTo(Number(e.target.value), { duration: 0 })} + className="features-zoom-slider" + aria-label="Zoom" + style={{ writingMode: 'vertical-lr', direction: 'rtl', height: 100, width: 14 }} + /> + + + {Math.round(zoom * 100)}% + + +
+ ); +} + +interface FeatureInfoCardState { + feature: Feature; + top: number; + left: number; +} + +function placeInfoTooltip( + nodeRect: DOMRect, + cardW: number, + cardH: number, +): { top: number; left: number } { + return { + top: nodeRect.top - INFO_CARD_GAP - cardH, + left: nodeRect.left + nodeRect.width / 2 - cardW / 2, + }; +} + +function FeatureInfoCard({ feature, top, left }: FeatureInfoCardState) { + const showName = feature.name !== feature.id; + const caretBorder = 'rgb(var(--s4))'; + + return createPortal( +
+
+
+ {feature.id} +
+ {showName && ( +
{feature.name}
+ )} +
+ + {feature.category.name} + + + default {feature.default.enabled ? 'on' : 'off'} + +
+

+ {feature.description?.trim() || 'No description'} +

+
+
+
, + document.body, + ); +} + +interface FeaturesGraphViewProps { + features: Feature[]; + selected: Set; + forwardDeps: Map>; + reverseDeps: Map>; + featureMap: Map; + onToggle: (id: string) => void; +} + +function FeaturesGraphCanvas({ + features, + selected, + forwardDeps, + reverseDeps, + featureMap, + onToggle, +}: FeaturesGraphViewProps) { + const { fitView } = useReactFlow(); + const [hideStandalone, setHideStandalone] = useState(true); + const [search, setSearch] = useState(''); + const [searchHighlightId, setSearchHighlightId] = useState(null); + const [hoveredId, setHoveredId] = useState(null); + const [focusedId, setFocusedId] = useState(null); + const [dropdownOpen, setDropdownOpen] = useState(false); + const [infoCard, setInfoCard] = useState(null); + const highlightTimeoutRef = useRef | null>(null); + const searchWrapRef = useRef(null); + const hoverLeaveTimer = useRef | null>(null); + const infoCardTimer = useRef | null>(null); + + const clearInfoCard = useCallback(() => { + if (infoCardTimer.current) { + clearTimeout(infoCardTimer.current); + infoCardTimer.current = null; + } + setInfoCard(null); + }, []); + + const onToggleRef = useRef(onToggle); + onToggleRef.current = onToggle; + const stableToggle = useCallback((id: string) => onToggleRef.current(id), []); + + const onFocusRef = useRef((id: string) => setFocusedId(id)); + onFocusRef.current = (id: string) => { + setFocusedId(id); + setHoveredId(null); + clearInfoCard(); + }; + const stableFocus = useCallback((id: string) => onFocusRef.current(id), []); + + const showAll = useCallback(() => { + setFocusedId(null); + setHoveredId(null); + clearInfoCard(); + }, [clearInfoCard]); + + const selectionValue = useMemo( + () => ({ selected, onToggle: stableToggle }), + [selected, stableToggle], + ); + + const layoutFeatures = useMemo(() => { + if (!focusedId) return features; + const tree = relatedTreeIds(focusedId, forwardDeps, reverseDeps, featureMap); + return features.filter(f => tree.has(f.id)); + }, [features, focusedId, forwardDeps, reverseDeps, featureMap]); + + const layout = useMemo( + () => buildLayout( + layoutFeatures, + forwardDeps, + reverseDeps, + // Focus already scopes the set; keep standalones so a lone focused + // feature still appears. + focusedId ? false : hideStandalone, + ), + [layoutFeatures, forwardDeps, reverseDeps, hideStandalone, focusedId], + ); + + const hoverTree = useMemo(() => { + if (!hoveredId) return null; + return relatedTreeIds(hoveredId, forwardDeps, reverseDeps, featureMap); + }, [hoveredId, forwardDeps, reverseDeps, featureMap]); + + const flowElements = useMemo( + () => + buildFlowElements( + layout.visible, + layout.positions, + layout.edgePairs, + searchHighlightId, + focusedId, + hoverTree, + stableFocus, + showAll, + ), + [ + layout, + searchHighlightId, + focusedId, + hoverTree, + stableFocus, + showAll, + ], + ); + + const [nodes, setNodes, onNodesChange] = useNodesState>( + flowElements.nodes, + ); + const [edges, setEdges, onEdgesChange] = useEdgesState>( + flowElements.edges, + ); + const [graphReady, setGraphReady] = useState(false); + const initialFitDone = useRef(false); + + useEffect(() => { + setNodes(flowElements.nodes); + setEdges(flowElements.edges); + }, [flowElements, setNodes, setEdges]); + + useEffect(() => { + if (layout.visible.length === 0) { + setGraphReady(true); + return; + } + + let cancelled = false; + // Wait until RF has committed node measurements, then fit without animating + // the first paint (avoids zoom-1 → fitView flicker). + const id = requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (cancelled) return; + const animate = initialFitDone.current; + fitView({ + padding: FIT_PADDING, + duration: animate ? 300 : 0, + minZoom: MIN_ZOOM, + maxZoom: MAX_ZOOM, + }); + initialFitDone.current = true; + setGraphReady(true); + }); + }); + return () => { + cancelled = true; + cancelAnimationFrame(id); + }; + }, [layout, focusedId, fitView]); + + useEffect(() => { + return () => { + if (infoCardTimer.current) clearTimeout(infoCardTimer.current); + if (hoverLeaveTimer.current) clearTimeout(hoverLeaveTimer.current); + if (highlightTimeoutRef.current) clearTimeout(highlightTimeoutRef.current); + }; + }, []); + + useEffect(() => { + function onDocClick(e: MouseEvent) { + if (!searchWrapRef.current?.contains(e.target as HTMLElement)) { + setDropdownOpen(false); + } + } + document.addEventListener('mousedown', onDocClick); + return () => document.removeEventListener('mousedown', onDocClick); + }, []); + + useEffect(() => { + function onKeyDown(e: KeyboardEvent) { + if (e.key !== 'Escape') return; + if (search || dropdownOpen) return; + if (focusedId) { + e.stopPropagation(); + setFocusedId(null); + } + } + document.addEventListener('keydown', onKeyDown); + return () => document.removeEventListener('keydown', onKeyDown); + }, [focusedId, search, dropdownOpen]); + + const searchPool = layout.visible; + + const matches = useMemo( + () => (search.trim() ? searchFeatures(searchPool, search, 8) : []), + [search, searchPool], + ); + + const zoomToFeature = useCallback( + (id: string) => { + if (highlightTimeoutRef.current) clearTimeout(highlightTimeoutRef.current); + setSearchHighlightId(id); + setDropdownOpen(false); + + requestAnimationFrame(() => { + fitView({ + nodes: [{ id }], + duration: 450, + maxZoom: 1.4, + padding: 0.55, + }); + }); + + highlightTimeoutRef.current = setTimeout(() => setSearchHighlightId(null), 2500); + }, + [fitView], + ); + + function clearSearch() { + setSearch(''); + setSearchHighlightId(null); + setDropdownOpen(false); + if (highlightTimeoutRef.current) clearTimeout(highlightTimeoutRef.current); + fitView({ + padding: FIT_PADDING, + duration: 400, + minZoom: MIN_ZOOM, + maxZoom: MAX_ZOOM, + }); + } + + function handleSearchKeyDown(e: React.KeyboardEvent) { + if (e.key === 'Enter' && matches.length > 0) { + e.preventDefault(); + const first = matches[0]; + setSearch(first.name); + zoomToFeature(first.id); + } else if (e.key === 'Escape' && (search || dropdownOpen)) { + e.stopPropagation(); + if (search) clearSearch(); + else setDropdownOpen(false); + } + } + + function handleNodeMouseEnter(e: React.MouseEvent, node: Node) { + if (hoverLeaveTimer.current) clearTimeout(hoverLeaveTimer.current); + setHoveredId(node.id); + + clearInfoCard(); + const feature = featureMap.get(node.id); + if (!feature) return; + + const el = (e.target as HTMLElement).closest('.react-flow__node'); + const nodeRect = el?.getBoundingClientRect(); + infoCardTimer.current = setTimeout(() => { + const nr = nodeRect ?? new DOMRect(e.clientX, e.clientY, 0, 0); + const placed = placeInfoTooltip(nr, INFO_CARD_WIDTH, INFO_CARD_EST_H); + setInfoCard({ feature, ...placed }); + }, INFO_CARD_DELAY_MS); + } + + function handleNodeMouseLeave() { + if (hoverLeaveTimer.current) clearTimeout(hoverLeaveTimer.current); + hoverLeaveTimer.current = setTimeout(() => setHoveredId(null), 80); + clearInfoCard(); + } + + return ( + +
+ + + + +
+ {focusedId && ( + + )} + {!focusedId && ( + + )} + +
+ +
+
+ + { + setSearch(e.target.value); + setDropdownOpen(true); + }} + onFocus={() => setDropdownOpen(true)} + onKeyDown={handleSearchKeyDown} + /> + {search && ( + + )} +
+ {dropdownOpen && matches.length > 0 && ( +
    + {matches.map(f => ( +
  • + +
  • + ))} +
+ )} + {dropdownOpen && search.trim() && matches.length === 0 && ( +
+ No features match +
+ )} +
+ + {nodes.length === 0 && ( +
+

+ {hideStandalone && !focusedId + ? 'No connected features — turn on “Show standalone features” to see all.' + : 'No features to display'} +

+
+ )} + + {infoCard && } +
+
+ ); +} + +export function FeaturesGraphView(props: FeaturesGraphViewProps) { + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/FeaturesModal.tsx b/frontend/src/components/FeaturesModal.tsx new file mode 100644 index 00000000..999f6982 --- /dev/null +++ b/frontend/src/components/FeaturesModal.tsx @@ -0,0 +1,576 @@ +import { useEffect, useRef, useMemo, useState, useCallback } from 'react'; +import { + X, Search, Check, Minus, SlidersHorizontal, AlertTriangle, ChevronDown, ChevronUp, List, Network, +} from 'lucide-react'; +import clsx from 'clsx'; +import type { Feature } from '../types'; +import { + buildFeatureMaps, + collectTransitiveDeps, + collectTransitiveDependents, +} from '../featureDeps'; +import { searchFeatures } from '../featureSearch'; +import { ModalShell } from './ModalShell'; +import { FeaturesGraphView } from './FeaturesGraphView'; + +type ViewMode = 'list' | 'graph'; + +interface FeaturesModalProps { + features: Feature[]; + selected: Set; + onDone: (selected: Set) => void; + onClose: () => void; +} + +export function FeaturesModal({ features, selected, onDone, onClose }: FeaturesModalProps) { + const [localSelected, setLocalSelected] = useState>(new Set(selected)); + const [viewMode, setViewMode] = useState('list'); + const [search, setSearch] = useState(''); + const [highlightedCat, setHighlightedCat] = useState(null); + const [pendingUncheck, setPendingUncheck] = useState(null); + const catFirstRefsMap = useRef>(new Map()); + const featureListRef = useRef(null); + const highlightTimeoutRef = useRef | null>(null); + + const { forwardDeps, reverseDeps, featureMap } = useMemo( + () => buildFeatureMaps(features), + [features], + ); + + const collectAllDeps = useCallback( + (id: string): Set => collectTransitiveDeps(id, forwardDeps, featureMap), + [forwardDeps, featureMap], + ); + + const collectAllDependents = useCallback( + (id: string): Set => collectTransitiveDependents(id, reverseDeps), + [reverseDeps], + ); + + const grouped = features.reduce>((acc, f) => { + if (!acc[f.category.id]) acc[f.category.id] = { catName: f.category.name, features: [] }; + acc[f.category.id].features.push(f); + return acc; + }, {}); + + const firstFeaturePerCat = useMemo(() => { + const map = new Map(); + features.forEach(f => { if (!map.has(f.category.id)) map.set(f.category.id, f.id); }); + return map; + }, [features]); + + const visibleFeatures = useMemo( + () => (search.trim() ? searchFeatures(features, search) : features), + [features, search], + ); + + function toggle(id: string) { + if (localSelected.has(id)) { + if (pendingUncheck === id) { + const dependents = collectAllDependents(id); + setLocalSelected(prev => { + const next = new Set(prev); + next.delete(id); + dependents.forEach(d => next.delete(d)); + return next; + }); + setPendingUncheck(null); + } else { + const dependents = collectAllDependents(id); + const activeDependents = [...dependents].filter(d => localSelected.has(d)); + if (activeDependents.length > 0) { + setPendingUncheck(id); + } else { + setLocalSelected(prev => { const next = new Set(prev); next.delete(id); return next; }); + setPendingUncheck(null); + } + } + } else { + const deps = collectAllDeps(id); + setLocalSelected(prev => { + const next = new Set(prev); + next.add(id); + deps.forEach(d => next.add(d)); + return next; + }); + if (pendingUncheck === id) setPendingUncheck(null); + } + } + + /** Graph view: deps are visible, so uncheck immediately removes dependents too. */ + function toggleGraph(id: string) { + if (localSelected.has(id)) { + const dependents = collectAllDependents(id); + setLocalSelected(prev => { + const next = new Set(prev); + next.delete(id); + dependents.forEach(d => next.delete(d)); + return next; + }); + if (pendingUncheck === id) setPendingUncheck(null); + } else { + const deps = collectAllDeps(id); + setLocalSelected(prev => { + const next = new Set(prev); + next.add(id); + deps.forEach(d => next.add(d)); + return next; + }); + if (pendingUncheck === id) setPendingUncheck(null); + } + } + + function cancelPendingUncheck() { + setPendingUncheck(null); + } + + function scrollToCategory(catId: string) { + const el = catFirstRefsMap.current.get(catId); + const container = featureListRef.current; + if (el && container) { + const smooth = !window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const top = el.getBoundingClientRect().top + - container.getBoundingClientRect().top + + container.scrollTop; + container.scrollTo({ top, behavior: smooth ? 'smooth' : 'auto' }); + } + if (highlightTimeoutRef.current) clearTimeout(highlightTimeoutRef.current); + setHighlightedCat(catId); + highlightTimeoutRef.current = setTimeout(() => setHighlightedCat(null), 1800); + } + + function toggleAllInCat(catId: string, catFeatures: Feature[]) { + const ids = catFeatures.map(f => f.id); + const allSelected = ids.every(id => localSelected.has(id)); + + if (allSelected) { + const toRemove = new Set(ids); + ids.forEach(id => collectAllDependents(id).forEach(d => toRemove.add(d))); + setLocalSelected(prev => { + const next = new Set(prev); + toRemove.forEach(id => next.delete(id)); + return next; + }); + if (pendingUncheck && toRemove.has(pendingUncheck)) setPendingUncheck(null); + } else { + const toAdd = new Set(ids); + ids.forEach(id => collectAllDeps(id).forEach(d => toAdd.add(d))); + setLocalSelected(prev => { + const next = new Set(prev); + toAdd.forEach(id => next.add(id)); + return next; + }); + } + } + + function toggleAll() { + const allSelected = features.every(f => localSelected.has(f.id)); + setLocalSelected(allSelected ? new Set() : new Set(features.map(f => f.id))); + setPendingUncheck(null); + } + + const totalCount = features.length; + const selectedCount = localSelected.size; + + return ( + +
+
+ +

Select Features

+ {selectedCount} / {totalCount} enabled +
+ + +
+
+ +
+ + {viewMode === 'list' ? ( +
+
+
+
+ 0 && selectedCount < totalCount} + onToggle={toggleAll} + label="Toggle all features" + /> + All Features +
+
+
+
+ {Object.entries(grouped).map(([catId, { catName, features: catFeatures }]) => { + const selCount = catFeatures.filter(f => localSelected.has(f.id)).length; + return ( + toggleAllInCat(catId, catFeatures)} + /> + ); + })} +
+
+
+ +
+
+
+ + setSearch(e.target.value)} + /> + {search && ( + + )} +
+
+ +
+ {visibleFeatures.length === 0 ? ( +

No features match

+ ) : ( +
+ {visibleFeatures.map(f => { + const activeDependents = pendingUncheck === f.id + ? [...collectAllDependents(f.id)].filter(d => localSelected.has(d)).map(d => featureMap.get(d)!).filter(Boolean) + : []; + return ( + { + if (el) catFirstRefsMap.current.set(f.category.id, el); + else catFirstRefsMap.current.delete(f.category.id); + } + : undefined + } + /> + ); + })} +
+ )} +
+
+
+ ) : ( + + )} + +
+ + +
+
+ ); +} + +function CategoryCard({ + catId, catName, total, selectedCount, isHighlighted, onScrollTo, onToggleAll, +}: { + catId: string; + catName: string; + total: number; + selectedCount: number; + isHighlighted: boolean; + onScrollTo: (id: string) => void; + onToggleAll: () => void; +}) { + const hasSelection = selectedCount > 0; + const allSelected = selectedCount === total; + const indeterminate = hasSelection && !allSelected; + return ( +
onScrollTo(catId)} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onScrollTo(catId); + } + }} + className={clsx( + 'shrink-0 md:w-full rounded-xl px-3.5 py-3 border transition-all duration-150 bg-surface-2 cursor-pointer', + isHighlighted + ? 'border-yellow-400 shadow-[0_0_8px_rgba(250,204,21,0.35)]' + : 'border-surface-4 hover:border-yellow-400/40 hover:bg-hover', + )} + > +
+ + + {catName} + + + {selectedCount}/{total} + +
+
+ ); +} + +function TriStateCheckbox({ checked, indeterminate, onToggle, label }: { + checked: boolean; + indeterminate: boolean; + onToggle: () => void; + label: string; +}) { + const ariaChecked: boolean | 'mixed' = checked ? true : indeterminate ? 'mixed' : false; + return ( + + ); +} + +function FeatureRow({ feature, checked, onToggle, catHighlighted, innerRef, pendingUncheck, activeDependents, onCancelPendingUncheck }: { + feature: Feature; + checked: boolean; + onToggle: (id: string) => void; + catHighlighted?: boolean; + innerRef?: (el: HTMLElement | null) => void; + pendingUncheck?: boolean; + activeDependents?: Feature[]; + onCancelPendingUncheck?: () => void; +}) { + const [expandedDeps, setExpandedDeps] = useState(false); + const rowRef = useRef(null); + + // Collapse when row scrolls out of view + useEffect(() => { + if (!pendingUncheck) return; + const el = rowRef.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { if (!entry.isIntersecting) onCancelPendingUncheck?.(); }, + { threshold: 0 } + ); + observer.observe(el); + return () => observer.disconnect(); + }, [pendingUncheck, onCancelPendingUncheck]); + + useEffect(() => { + if (!pendingUncheck) setExpandedDeps(false); + }, [pendingUncheck]); + + const TREE_LIMIT = 4; + const shown = activeDependents?.slice(0, expandedDeps ? undefined : TREE_LIMIT) ?? []; + const hiddenCount = (activeDependents?.length ?? 0) - TREE_LIMIT; + + function setRef(el: HTMLDivElement | null) { + rowRef.current = el; + innerRef?.(el); + } + + return ( +
+ + + {pendingUncheck && activeDependents && activeDependents.length > 0 && ( +
+
+ + + Disabling this will also disable {activeDependents.length} dependent{activeDependents.length !== 1 ? 's' : ''}: + +
+
+ {shown.map((dep, i) => { + const isLast = i === shown.length - 1 && (expandedDeps || hiddenCount <= 0); + return ( +
+ {isLast ? '└──' : '├──'} + {dep.name} + {dep.category.name} +
+ ); + })} + {!expandedDeps && hiddenCount > 0 && ( +
+ └── + +
+ )} + {expandedDeps && hiddenCount > 0 && ( + + )} +
+
+ + +
+
+ )} +
+ ); +} diff --git a/frontend/src/components/HeroPanel.tsx b/frontend/src/components/HeroPanel.tsx new file mode 100644 index 00000000..8561a636 --- /dev/null +++ b/frontend/src/components/HeroPanel.tsx @@ -0,0 +1,23 @@ +export function HeroPanel() { + return ( +
+

+ ArduPilot +

+
+ + CustomBuild + +
+ +

+ Build exactly the firmware you need. Choose your vehicle, board, and feature set, + and we'll compile it for you. +

+ +

+ Versatile · Trusted · Open +

+
+ ); +} diff --git a/frontend/src/components/ModalShell.tsx b/frontend/src/components/ModalShell.tsx new file mode 100644 index 00000000..11efa083 --- /dev/null +++ b/frontend/src/components/ModalShell.tsx @@ -0,0 +1,115 @@ +import { useEffect, useRef, type CSSProperties, type ReactNode } from 'react'; +import { createPortal } from 'react-dom'; +import clsx from 'clsx'; + +const FOCUSABLE = + 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'; + +interface ModalShellProps { + children: ReactNode; + panelClassName?: string; + backdropStyle?: CSSProperties; + onClose?: () => void; + /** Separate absolute backdrop layer (conflict modal layout). */ + separateBackdrop?: boolean; + ariaLabelledBy?: string; +} + +export function ModalShell({ + children, + panelClassName, + backdropStyle = { background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(4px)' }, + onClose, + separateBackdrop = false, + ariaLabelledBy, +}: ModalShellProps) { + const closeRef = useRef(onClose); + closeRef.current = onClose; + const panelRef = useRef(null); + const previousFocusRef = useRef(null); + + useEffect(() => { + const prev = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { document.body.style.overflow = prev; }; + }, []); + + useEffect(() => { + previousFocusRef.current = document.activeElement as HTMLElement | null; + const panel = panelRef.current; + if (!panel) return; + + const focusables = () => + Array.from(panel.querySelectorAll(FOCUSABLE)).filter( + el => !el.hasAttribute('disabled') && el.offsetParent !== null, + ); + + const initial = focusables(); + (initial[0] ?? panel).focus(); + + function onKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') { + e.stopPropagation(); + closeRef.current?.(); + return; + } + if (e.key !== 'Tab' || !panelRef.current) return; + + const nodes = focusables(); + if (nodes.length === 0) { + e.preventDefault(); + panelRef.current.focus(); + return; + } + const first = nodes[0]; + const last = nodes[nodes.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('keydown', onKeyDown); + previousFocusRef.current?.focus?.(); + }; + }, []); + + function handleBackdropClick() { + closeRef.current?.(); + } + + return createPortal( +
+ {separateBackdrop && ( +
+ )} +
e.stopPropagation()} + > + {children} +
+
, + document.body, + ); +} diff --git a/frontend/src/components/StandardArtifactsGrid.tsx b/frontend/src/components/StandardArtifactsGrid.tsx new file mode 100644 index 00000000..257caf2a --- /dev/null +++ b/frontend/src/components/StandardArtifactsGrid.tsx @@ -0,0 +1,69 @@ +import { ArrowDownToLine, Cpu, FileText } from 'lucide-react'; +import clsx from 'clsx'; +import type { StandardArtifact } from '../types'; +import { FormSection } from './StepComponents'; + +const FIRMWARE_FORMATS = new Set(['bin', 'apj', 'elf', 'hex', '']); + +const FORMAT_BADGE: Record = { + bin: 'bg-orange-500/20 text-orange-300 border border-orange-500/30', + apj: 'bg-sky-500/20 text-sky-300 border border-sky-500/30', + elf: 'bg-violet-500/20 text-violet-300 border border-violet-500/30', + hex: 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30', + '': 'bg-orange-500/20 text-orange-300 border border-orange-500/30', +}; +const DEFAULT_BADGE = 'bg-gray-500/20 text-gray-400 border border-gray-500/30'; + +function getBaseName(name: string) { + const dot = name.lastIndexOf('.'); + return dot === -1 ? name : name.slice(0, dot); +} + +function ArtifactCard({ artifact }: { artifact: StandardArtifact }) { + const format = artifact.format.toLowerCase(); + const isFirmware = FIRMWARE_FORMATS.has(format); + const formatLabel = format.toUpperCase() || 'BIN'; + const badgeCls = FORMAT_BADGE[format] ?? DEFAULT_BADGE; + return ( + + + {formatLabel} + +
+ {isFirmware + ? + : } +
+ + {getBaseName(artifact.name)} + + +
+ ); +} + +export function StandardArtifactsGrid({ artifacts }: { artifacts: StandardArtifact[] }) { + const firmwareArtifacts = artifacts.filter(a => + FIRMWARE_FORMATS.has(a.format.toLowerCase()), + ); + const otherArtifacts = artifacts.filter(a => + !FIRMWARE_FORMATS.has(a.format.toLowerCase()), + ); + + return ( + +
+ {firmwareArtifacts.map(a => )} + {otherArtifacts.map(a => )} +
+
+ ); +} diff --git a/frontend/src/components/StepComponents.tsx b/frontend/src/components/StepComponents.tsx new file mode 100644 index 00000000..8715283d --- /dev/null +++ b/frontend/src/components/StepComponents.tsx @@ -0,0 +1,419 @@ +import { type ReactNode, useState, useRef, useEffect } from 'react'; +import { Check, ChevronRight, ChevronDown, ChevronUp } from 'lucide-react'; +import clsx from 'clsx'; + +interface Step { + id: string; + label: string; +} + +interface StepHeaderProps { + steps: Step[]; + currentStep: string; + onStepClick?: (id: string) => void; +} + +export function StepHeader({ steps, currentStep, onStepClick }: StepHeaderProps) { + const currentIdx = steps.findIndex(s => s.id === currentStep); + // If currentStep is not in the list (post-flow), treat all as done + const effectiveIdx = currentIdx === -1 ? steps.length : currentIdx; + + return ( +
+ {steps.map((step, idx) => { + const done = idx < effectiveIdx; + const active = idx === effectiveIdx; + const clickable = done && !!onStepClick; + const className = clsx( + 'flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-mono font-medium transition-all duration-300 border', + done && 'bg-yellow-400 border-yellow-400 text-black', + active && 'bg-transparent border-yellow-400 text-yellow-400', + !done && !active && 'bg-transparent border-surface-4 text-gray-500', + clickable && 'cursor-pointer hover:bg-yellow-300 hover:border-yellow-300', + ); + const content = ( + <> + {done ? ( + + ) : ( + {idx + 1} + )} + {step.label} + + ); + return ( +
+ {clickable ? ( + + ) : ( +
+ {content} +
+ )} + {idx < steps.length - 1 && ( + + )} +
+ ); + })} +
+ ); +} + +interface Option { + id: string; + name: string; +} + +interface SearchableDropdownProps { + options: Option[]; + selected?: string; + onSelect: (id: string) => void; + placeholder?: string; + loading?: boolean; +} + +export function SearchableDropdown({ options, selected, onSelect, placeholder = 'Select…', loading }: SearchableDropdownProps) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + const ref = useRef(null); + + const selectedOpt = options.find(o => o.id === selected); + const filtered = options.filter(o => o.name.toLowerCase().includes(query.toLowerCase())); + + useEffect(() => { + function handleClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + setQuery(''); + } + } + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, []); + + if (loading) return
; + + return ( +
+ + + {open && ( +
+
+ setQuery(e.target.value)} + placeholder="Search…" + className="w-full bg-surface-3 border border-surface-4 rounded px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 outline-none focus:ring-1 focus:ring-yellow-400/50" + /> +
+
+ {filtered.length === 0 ? ( +

No results

+ ) : filtered.map(opt => ( + + ))} +
+
+ )} +
+ ); +} + +const FILLER_MESSAGES: Record<'boards' | 'features', string> = { + boards: 'Loading available boards…', + features: 'Loading features…', +}; + +export function LoadingFiller({ kind, loading }: { kind: 'boards' | 'features'; loading: boolean }) { + if (!loading) return null; + + return ( +

+ {FILLER_MESSAGES[kind]} +

+ ); +} + +interface VersionSelectorOption { + id: string; + name: string; + type: 'stable' | 'beta' | 'latest' | 'tag'; + badge?: string; + badgeColor?: string; + remote?: string; +} + +interface VersionSelectorProps { + options: VersionSelectorOption[]; + selected?: string; + onSelect: (id: string) => void; + loading?: boolean; +} + +const QUICK_TYPES = [ + { type: 'stable' as const, label: 'Stable', color: 'badge-green' }, + { type: 'beta' as const, label: 'Beta', color: 'badge-yellow' }, + { type: 'latest' as const, label: 'Latest', color: 'badge-blue' }, +]; + +export function VersionSelector({ options, selected, onSelect, loading }: VersionSelectorProps) { + const selectedOpt = options.find(o => o.id === selected); + + // One representative per quick type (first match) + const quickOptions = QUICK_TYPES.map(qt => ({ + ...qt, + version: options.find(o => o.type === qt.type), + })).filter(qt => qt.version); + + // Non-quick = selected but not the exact pill representative for its type + const quickIds = new Set(quickOptions.map(qt => qt.version!.id)); + const isNonQuick = !!selectedOpt && !quickIds.has(selectedOpt.id); + + const [showMore, setShowMore] = useState(isNonQuick); + const [query, setQuery] = useState(''); + const selectedRowRef = useRef(null); + + // Scroll the pre-selected item into view when the dropdown opens on return + useEffect(() => { + if (showMore && isNonQuick && selectedRowRef.current) { + selectedRowRef.current.scrollIntoView({ block: 'nearest' }); + } + }, [showMore, isNonQuick]); + + const filtered = options.filter(o => o.name.toLowerCase().includes(query.toLowerCase())); + + function toggleMore() { + setShowMore(v => !v); + if (showMore) setQuery(''); + } + + function handleSelect(id: string) { + onSelect(id); + if (quickIds.has(id)) { + setShowMore(false); + setQuery(''); + } + } + + if (loading) return
; + + return ( +
+
+ {quickOptions.map(({ type, label, color, version }) => ( + + ))} + + +
+ + {showMore && ( +
+
+ setQuery(e.target.value)} + placeholder="Search versions…" + className="w-full bg-surface-3 border border-surface-4 rounded px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 outline-none focus:ring-1 focus:ring-yellow-400/50" + /> +
+
+ {filtered.length === 0 ? ( +

No results

+ ) : filtered.map(opt => ( + + ))} +
+
+ )} +
+ ); +} + +const PROMOTED_VEHICLE_IDS = ['copter', 'plane', 'rover'] as const; + +const VEHICLE_ICONS: Record = { + copter: , + plane: , + rover: , + sub: , + tracker: , + heli: , + blimp: , + 'ap-periph': , +}; + +const DEFAULT_VEHICLE_ICON = ; + +interface VehicleSelectorProps { + options: { id: string; name: string }[]; + selected?: string; + onSelect: (id: string) => void; + loading?: boolean; +} + +export function VehicleSelector({ options, selected, onSelect, loading }: VehicleSelectorProps) { + if (loading) { + return ( +
+ {[...Array(8)].map((_, i) => ( +
+ ))} +
+ ); + } + + const promotedIds = new Set(PROMOTED_VEHICLE_IDS as readonly string[]); + const promoted = (PROMOTED_VEHICLE_IDS as readonly string[]) + .map(id => options.find(o => o.id === id)) + .filter(Boolean) as { id: string; name: string }[]; + const rest = options.filter(o => !promotedIds.has(o.id)); + const sorted = [...promoted, ...rest]; + + return ( +
+ {sorted.map(v => { + const isSelected = selected === v.id; + return ( + + ); + })} +
+ ); +} + +export function FormSection({ label, children }: { label: string; children: ReactNode }) { + return ( +
+

{label}

+ {children} +
+ ); +} + +export function ChosenPill({ label, value, onEdit }: { label: string; value: string; onEdit?: () => void }) { + return ( +
+ {label}: + {value} + {onEdit && ( + + )} +
+ ); +} diff --git a/frontend/src/components/ThemeToggle.tsx b/frontend/src/components/ThemeToggle.tsx new file mode 100644 index 00000000..bdf8251a --- /dev/null +++ b/frontend/src/components/ThemeToggle.tsx @@ -0,0 +1,21 @@ +import { Sun, Moon } from 'lucide-react'; + +interface ThemeToggleProps { + isDark: boolean; + onToggle: () => void; +} + +export function ThemeToggle({ isDark, onToggle }: ThemeToggleProps) { + return ( + + ); +} diff --git a/frontend/src/components/Tooltip.tsx b/frontend/src/components/Tooltip.tsx new file mode 100644 index 00000000..ef4a76eb --- /dev/null +++ b/frontend/src/components/Tooltip.tsx @@ -0,0 +1,74 @@ +import { useState, useRef, useEffect, useId, cloneElement, type ReactElement } from 'react'; +import { createPortal } from 'react-dom'; + +interface TooltipProps { + text: string; + children: ReactElement; +} + +export function Tooltip({ text, children }: TooltipProps) { + const [pos, setPos] = useState<{ top: number; left: number } | null>(null); + const wrapperRef = useRef(null); + const tooltipId = useId(); + + function updatePos() { + if (!wrapperRef.current) return; + const r = wrapperRef.current.getBoundingClientRect(); + const left = Math.min( + Math.max(r.left + r.width / 2, 48), + window.innerWidth - 48, + ); + setPos({ top: r.top - 8, left }); + } + + useEffect(() => { + if (!pos) return; + const onReposition = () => updatePos(); + window.addEventListener('scroll', onReposition, true); + window.addEventListener('resize', onReposition); + return () => { + window.removeEventListener('scroll', onReposition, true); + window.removeEventListener('resize', onReposition); + }; + }, [pos]); + + const child = cloneElement(children, { + 'aria-describedby': pos ? tooltipId : undefined, + } as Record); + + return ( + <> + setPos(null)} + onFocus={e => { + if ((e.target as HTMLElement).matches(':focus-visible')) updatePos(); + }} + onBlur={() => setPos(null)} + onPointerDown={() => setPos(null)} + onClick={() => setPos(null)} + > + {child} + + {pos && createPortal( +