From b2d322af8a4294d47401ec13f46fc0d3f2fc0a97 Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Sat, 25 Jul 2026 13:52:18 +0530 Subject: [PATCH 01/11] backend: rename web directory to backend Co-authored-by: Cursor --- .github/workflows/tests.yml | 4 ++-- {web => backend}/Dockerfile | 2 +- {web => backend}/__init__.py | 0 {web => backend}/api/v1/__init__.py | 2 +- {web => backend}/api/v1/admin.py | 4 ++-- {web => backend}/api/v1/builds.py | 6 +++--- {web => backend}/api/v1/router.py | 2 +- {web => backend}/api/v1/vehicles.py | 4 ++-- {web => backend}/core/__init__.py | 4 ++-- {web => backend}/core/config.py | 0 {web => backend}/core/limiter.py | 2 +- {web => backend}/core/logging_config.py | 0 {web => backend}/core/startup.py | 0 {web => backend}/docker-entrypoint.sh | 0 {web => backend}/main.py | 17 +++++++++-------- {web => backend}/requirements.txt | 0 {web => backend}/schemas/__init__.py | 6 +++--- {web => backend}/schemas/admin.py | 0 {web => backend}/schemas/builds.py | 2 +- {web => backend}/schemas/vehicles.py | 0 backend/services/__init__.py | 15 +++++++++++++++ {web => backend}/services/admin.py | 2 +- {web => backend}/services/builds.py | 4 ++-- {web => backend}/services/vehicles.py | 9 +++------ .../static/images/ardupilot_logo.png | Bin {web => backend}/static/images/bg.png | Bin .../static/images/button-closed.png | Bin {web => backend}/static/images/button-open.png | Bin {web => backend}/static/images/button.png | Bin {web => backend}/static/images/logo.png | Bin {web => backend}/static/js/add_build.js | 0 {web => backend}/static/js/index.js | 0 {web => backend}/static/styles/main.css | 0 {web => backend}/templates/add_build.html | 0 {web => backend}/templates/index.html | 0 {web => backend}/ui/__init__.py | 2 +- {web => backend}/ui/router.py | 0 tests/{web => backend}/conftest.py | 4 ++-- tests/{web => backend}/test_admin_api.py | 2 +- tests/{web => backend}/test_admin_service.py | 2 +- tests/{web => backend}/test_builds_api.py | 6 +++--- tests/{web => backend}/test_builds_service.py | 4 ++-- tests/{web => backend}/test_config.py | 2 +- tests/{web => backend}/test_vehicles_api.py | 6 +++--- .../{web => backend}/test_vehicles_service.py | 6 +++--- web/services/__init__.py | 15 --------------- 46 files changed, 66 insertions(+), 68 deletions(-) rename {web => backend}/Dockerfile (96%) rename {web => backend}/__init__.py (100%) rename {web => backend}/api/v1/__init__.py (51%) rename {web => backend}/api/v1/admin.py (95%) rename {web => backend}/api/v1/builds.py (97%) rename {web => backend}/api/v1/router.py (88%) rename {web => backend}/api/v1/vehicles.py (98%) rename {web => backend}/core/__init__.py (50%) rename {web => backend}/core/config.py (100%) rename {web => backend}/core/limiter.py (94%) rename {web => backend}/core/logging_config.py (100%) rename {web => backend}/core/startup.py (100%) rename {web => backend}/docker-entrypoint.sh (100%) rename {web => backend}/main.py (89%) rename {web => backend}/requirements.txt (100%) rename {web => backend}/schemas/__init__.py (89%) rename {web => backend}/schemas/admin.py (100%) rename {web => backend}/schemas/builds.py (96%) rename {web => backend}/schemas/vehicles.py (100%) create mode 100644 backend/services/__init__.py rename {web => backend}/services/admin.py (97%) rename {web => backend}/services/builds.py (99%) rename {web => backend}/services/vehicles.py (97%) rename {web => backend}/static/images/ardupilot_logo.png (100%) rename {web => backend}/static/images/bg.png (100%) rename {web => backend}/static/images/button-closed.png (100%) rename {web => backend}/static/images/button-open.png (100%) rename {web => backend}/static/images/button.png (100%) rename {web => backend}/static/images/logo.png (100%) rename {web => backend}/static/js/add_build.js (100%) rename {web => backend}/static/js/index.js (100%) rename {web => backend}/static/styles/main.css (100%) rename {web => backend}/templates/add_build.html (100%) rename {web => backend}/templates/index.html (100%) rename {web => backend}/ui/__init__.py (64%) rename {web => backend}/ui/router.py (100%) rename tests/{web => backend}/conftest.py (98%) rename tests/{web => backend}/test_admin_api.py (99%) rename tests/{web => backend}/test_admin_service.py (97%) rename tests/{web => backend}/test_builds_api.py (99%) rename tests/{web => backend}/test_builds_service.py (99%) rename tests/{web => backend}/test_config.py (99%) rename tests/{web => backend}/test_vehicles_api.py (99%) rename tests/{web => backend}/test_vehicles_service.py (99%) delete mode 100644 web/services/__init__.py 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/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 97% rename from web/api/v1/builds.py rename to backend/api/v1/builds.py index cd8e651c..26b4211c 100644 --- a/web/api/v1/builds.py +++ b/backend/api/v1/builds.py @@ -11,13 +11,13 @@ ) 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"]) 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 100% rename from web/requirements.txt rename to backend/requirements.txt 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 96% rename from web/schemas/builds.py rename to backend/schemas/builds.py index 85984d31..6a733c8a 100644 --- a/web/schemas/builds.py +++ b/backend/schemas/builds.py @@ -1,7 +1,7 @@ from typing import List, Literal, Optional from pydantic import BaseModel, Field -from web.schemas.vehicles import VehicleBase, BoardBase, RemoteInfo, VersionType +from backend.schemas.vehicles import VehicleBase, BoardBase, RemoteInfo, VersionType # --- Build Progress --- 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 99% rename from web/services/builds.py rename to backend/services/builds.py index 3efdb9fc..ccc9baa7 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 from build_config import config_dict_from_build_info, dump_config_yaml # Import external modules 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 8196f5d3..c331c822 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 _KNOWN_TYPES = {"beta", "stable", "latest", "tag"} release_type = ( 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/tests/web/conftest.py b/tests/backend/conftest.py similarity index 98% rename from tests/web/conftest.py rename to tests/backend/conftest.py index 302bdc8b..c0c1fbe5 100644 --- a/tests/web/conftest.py +++ b/tests/backend/conftest.py @@ -225,8 +225,8 @@ def app_with_mocked_dependencies( from fastapi import FastAPI from slowapi.middleware import SlowAPIMiddleware from slowapi.errors import RateLimitExceeded - from web.api.v1 import router as v1_router - from web.core.limiter import limiter, rate_limit_exceeded_handler + from backend.api.v1 import router as v1_router + from backend.core.limiter import limiter, rate_limit_exceeded_handler # Set environment variables for test configuration os.environ["CBS_BASEDIR"] = test_base_dir diff --git a/tests/web/test_admin_api.py b/tests/backend/test_admin_api.py similarity index 99% rename from tests/web/test_admin_api.py rename to tests/backend/test_admin_api.py index 9f24a8a9..fdda0586 100644 --- a/tests/web/test_admin_api.py +++ b/tests/backend/test_admin_api.py @@ -6,7 +6,7 @@ from fastapi import status -from web.core.config import get_settings +from backend.core.config import get_settings class TestAdminRefreshVersionsEndpoint: diff --git a/tests/web/test_admin_service.py b/tests/backend/test_admin_service.py similarity index 97% rename from tests/web/test_admin_service.py rename to tests/backend/test_admin_service.py index a54e864e..13541b28 100644 --- a/tests/web/test_admin_service.py +++ b/tests/backend/test_admin_service.py @@ -4,7 +4,7 @@ import pytest from unittest.mock import Mock -from web.services.admin import AdminService +from backend.services.admin import AdminService class TestAdminService: diff --git a/tests/web/test_builds_api.py b/tests/backend/test_builds_api.py similarity index 99% rename from tests/web/test_builds_api.py rename to tests/backend/test_builds_api.py index 6cf4769f..84fdd95d 100644 --- a/tests/web/test_builds_api.py +++ b/tests/backend/test_builds_api.py @@ -5,7 +5,7 @@ from unittest.mock import Mock, patch, PropertyMock, MagicMock from fastapi import status -from web.schemas import ( +from backend.schemas import ( BuildRequest, BuildSubmitResponse, BuildOut, @@ -13,7 +13,7 @@ BuildVersionInfo, RemoteInfo, ) -from web.schemas.vehicles import VehicleBase, BoardBase +from backend.schemas.vehicles import VehicleBase, BoardBase class TestBuildsAPI: @@ -25,7 +25,7 @@ class TestBuildsAPI: @contextmanager def override_builds_service(client, mock_service): """Temporarily override the get_builds_service dependency.""" - from web.services.builds import get_builds_service + from backend.services.builds import get_builds_service client.app.dependency_overrides[get_builds_service] = lambda: mock_service try: yield diff --git a/tests/web/test_builds_service.py b/tests/backend/test_builds_service.py similarity index 99% rename from tests/web/test_builds_service.py rename to tests/backend/test_builds_service.py index 3361b9a6..5f50427a 100644 --- a/tests/web/test_builds_service.py +++ b/tests/backend/test_builds_service.py @@ -8,8 +8,8 @@ import build_manager as bm from metadata_manager import RemoteInfo as ManagerRemoteInfo from metadata_manager import RemoteInfo, VersionInfo -from web.schemas import BuildRequest -from web.services.builds import BuildsService +from backend.schemas import BuildRequest +from backend.services.builds import BuildsService @pytest.fixture diff --git a/tests/web/test_config.py b/tests/backend/test_config.py similarity index 99% rename from tests/web/test_config.py rename to tests/backend/test_config.py index 123a3f3a..15e0a659 100644 --- a/tests/web/test_config.py +++ b/tests/backend/test_config.py @@ -4,7 +4,7 @@ import os from unittest.mock import patch -from web.core.config import Settings +from backend.core.config import Settings class TestSettings: diff --git a/tests/web/test_vehicles_api.py b/tests/backend/test_vehicles_api.py similarity index 99% rename from tests/web/test_vehicles_api.py rename to tests/backend/test_vehicles_api.py index 74b866b3..2cd8e3de 100644 --- a/tests/web/test_vehicles_api.py +++ b/tests/backend/test_vehicles_api.py @@ -5,7 +5,7 @@ from unittest.mock import Mock from fastapi import status -from web.schemas import ( +from backend.schemas import ( VehicleBase, VersionOut, BoardOut, @@ -26,7 +26,7 @@ class TestVehiclesAPI: @contextmanager def override_vehicles_service(client, mock_service): """Temporarily override the get_vehicles_service dependency.""" - from web.services.vehicles import get_vehicles_service + from backend.services.vehicles import get_vehicles_service client.app.dependency_overrides[get_vehicles_service] = lambda: mock_service try: yield @@ -37,7 +37,7 @@ def override_vehicles_service(client, mock_service): def dummy_version(): return VersionOut( id="copter-4.5.0-stable", - name="stable 4.5.0 (ardupilot)", + name="4.5.0", type="stable", remote=RemoteInfo( name="ardupilot", diff --git a/tests/web/test_vehicles_service.py b/tests/backend/test_vehicles_service.py similarity index 99% rename from tests/web/test_vehicles_service.py rename to tests/backend/test_vehicles_service.py index cb42d6bc..608c568f 100644 --- a/tests/web/test_vehicles_service.py +++ b/tests/backend/test_vehicles_service.py @@ -10,7 +10,7 @@ Vehicle, VersionInfo, ) -from web.services.vehicles import VehiclesService +from backend.services.vehicles import VehiclesService @pytest.fixture @@ -370,7 +370,7 @@ def test_get_versions_latest_name_format( ] versions = service.get_versions("copter") - assert versions[0].name == "Latest (ardupilot)" + assert versions[0].name == "master" def test_get_versions_non_latest_name_format( self, service, mock_versions_manager @@ -386,7 +386,7 @@ def test_get_versions_non_latest_name_format( ] versions = service.get_versions("copter") - assert versions[0].name == "stable 4.5.0 (ardupilot)" + assert versions[0].name == "4.5.0" # Tests for get_version diff --git a/web/services/__init__.py b/web/services/__init__.py deleted file mode 100644 index dea8646b..00000000 --- a/web/services/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Business logic services for the application. -""" -from web.services.vehicles import get_vehicles_service, VehiclesService -from web.services.builds import get_builds_service, BuildsService -from web.services.admin import get_admin_service, AdminService - -__all__ = [ - "get_vehicles_service", - "VehiclesService", - "get_builds_service", - "BuildsService", - "get_admin_service", - "AdminService", -] From b5aeda225fea8f65337f7eb804b3b246486b5472 Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Sat, 25 Jul 2026 13:52:18 +0530 Subject: [PATCH 02/11] frontend: add React UI Co-authored-by: Cursor --- frontend/.eslintrc.cjs | 19 + frontend/Dockerfile | 25 + frontend/index.html | 17 + frontend/nginx.conf | 20 + frontend/package-lock.json | 4523 +++++++++++++++++ frontend/package.json | 37 + frontend/postcss.config.js | 6 + frontend/public/ardupilot_logo.png | Bin 0 -> 67402 bytes frontend/public/favicon.ico | Bin 0 -> 1150 bytes frontend/public/favicon.svg | 4 + frontend/src/App.tsx | 219 + frontend/src/api.ts | 91 + frontend/src/buildConfig.ts | 115 + frontend/src/components/BuildForm.tsx | 728 +++ frontend/src/components/BuildInfoModal.tsx | 411 ++ frontend/src/components/BuildsTable.tsx | 351 ++ frontend/src/components/CollapsibleBanner.tsx | 82 + .../src/components/ConfigConflictModal.tsx | 134 + frontend/src/components/ConfigDropZone.tsx | 101 + frontend/src/components/DoneStep.tsx | 128 + frontend/src/components/ErrorBanner.tsx | 11 + frontend/src/components/FeaturesGraphView.tsx | 1010 ++++ frontend/src/components/FeaturesModal.tsx | 576 +++ frontend/src/components/HeroPanel.tsx | 23 + frontend/src/components/ModalShell.tsx | 115 + .../src/components/StandardArtifactsGrid.tsx | 69 + frontend/src/components/StepComponents.tsx | 419 ++ frontend/src/components/ThemeToggle.tsx | 21 + frontend/src/components/Tooltip.tsx | 74 + frontend/src/components/buildStatus.tsx | 55 + frontend/src/featureDeps.ts | 156 + frontend/src/featureSearch.ts | 44 + frontend/src/hooks/useBuildPolling.ts | 71 + frontend/src/hooks/useConfigLoad.ts | 365 ++ frontend/src/index.css | 560 ++ frontend/src/main.tsx | 10 + frontend/src/types.ts | 106 + frontend/src/vite-env.d.ts | 1 + frontend/tailwind.config.js | 42 + frontend/tsconfig.json | 15 + frontend/vite.config.ts | 38 + 41 files changed, 10792 insertions(+) create mode 100644 frontend/.eslintrc.cjs create mode 100644 frontend/Dockerfile create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/ardupilot_logo.png create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api.ts create mode 100644 frontend/src/buildConfig.ts create mode 100644 frontend/src/components/BuildForm.tsx create mode 100644 frontend/src/components/BuildInfoModal.tsx create mode 100644 frontend/src/components/BuildsTable.tsx create mode 100644 frontend/src/components/CollapsibleBanner.tsx create mode 100644 frontend/src/components/ConfigConflictModal.tsx create mode 100644 frontend/src/components/ConfigDropZone.tsx create mode 100644 frontend/src/components/DoneStep.tsx create mode 100644 frontend/src/components/ErrorBanner.tsx create mode 100644 frontend/src/components/FeaturesGraphView.tsx create mode 100644 frontend/src/components/FeaturesModal.tsx create mode 100644 frontend/src/components/HeroPanel.tsx create mode 100644 frontend/src/components/ModalShell.tsx create mode 100644 frontend/src/components/StandardArtifactsGrid.tsx create mode 100644 frontend/src/components/StepComponents.tsx create mode 100644 frontend/src/components/ThemeToggle.tsx create mode 100644 frontend/src/components/Tooltip.tsx create mode 100644 frontend/src/components/buildStatus.tsx create mode 100644 frontend/src/featureDeps.ts create mode 100644 frontend/src/featureSearch.ts create mode 100644 frontend/src/hooks/useBuildPolling.ts create mode 100644 frontend/src/hooks/useConfigLoad.ts create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/types.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts 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..4dbf30ae --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,20 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://backend:8080/api/; + proxy_read_timeout 120s; + proxy_send_timeout 120s; + 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 0000000000000000000000000000000000000000..ed67b531b915a8a53018ee1574f8a7af878001ac GIT binary patch literal 67402 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 uKOyW-wf(VtYu{Z!KPsaQrqP9 z`IL+ab$Yu}mQpG!kSUqjoEK6`*8Dp+;l(nbUgV$mS3V+m|xksvzW3TtcZ?XkSP zwI=@&w8r#>vs{|JNU1pM zak+ez#l?Bbm3bB_B`Wgmp;DyKmd6PPX@R-o z6|A`jWRD%k_U*t4`EjEbj^tqamXv!31G^YKF~FDgPn1fRsns6xETJQPDX23=WmI}*vJ4I8(-<`?cw&y5_j(3!(M$({@e`R*Q)G!1-Ys7IChg) zFQeCNJSN76dH>-Zuj?;at}f#~dP8pF6j9MMKmJIjy6WW-m)slmjUHnogXrhF)UKtk zpMx(T_3Rt_>d-nq>3QjX{i@foscCmtfy15cgo8d>J3A;04Y9xd0B$7kM_r?r_6Xa4 pBGO-dPE_h1v3y>? + + 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..a67c9410 --- /dev/null +++ b/frontend/src/buildConfig.ts @@ -0,0 +1,115 @@ +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`); + const contentType = res.headers.get('content-type') ?? ''; + if (!res.ok || !contentType.includes('application/json')) + throw new Error(`No schema found for config version "${version}". The config file may be too new or unsupported.`); + let schema: Record; + try { + schema = await res.json() as Record; + } catch { + throw new Error(`No schema found for config version "${version}". The config file may be too new or unsupported.`); + } + 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..9452880b --- /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, custom: 4 }; + +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', custom: '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..a1c737c9 --- /dev/null +++ b/frontend/src/components/BuildInfoModal.tsx @@ -0,0 +1,411 @@ +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); + const canDownload = build?.artifact_available === true; + + 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..8a60d25a --- /dev/null +++ b/frontend/src/components/BuildsTable.tsx @@ -0,0 +1,351 @@ +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, + 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 [loadError, setLoadError] = 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; + setLoadError(null); + setHasMore(data.length > PAGE_SIZE); + setBuilds(data.slice(0, PAGE_SIZE)); + }) + .catch(() => { + if (gen !== fetchGenRef.current) return; + setLoadError('Failed to load builds'); + }) + .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) => ( +
    + ))} +
    + ))} +
    + ) : loadError ? ( +
    + {loadError} +
    + ) : builds.length === 0 ? ( +
    No builds found
    + ) : ( +
    + {builds.map(b => { + const canDownload = b.artifact_available; + 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..8062f5d9 --- /dev/null +++ b/frontend/src/components/DoneStep.tsx @@ -0,0 +1,128 @@ +import { + CheckCircle2, XCircle, AlertTriangle, + Download, Terminal, RotateCcw, +} 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..d5373493 --- /dev/null +++ b/frontend/src/components/FeaturesGraphView.tsx @@ -0,0 +1,1010 @@ +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) { + e.preventDefault(); + e.stopImmediatePropagation(); + setSearch(''); + setDropdownOpen(false); + return; + } + if (focusedId) { + e.preventDefault(); + e.stopImmediatePropagation(); + setFocusedId(null); + } + } + document.addEventListener('keydown', onKeyDown, true); + return () => document.removeEventListener('keydown', onKeyDown, true); + }, [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..646545df --- /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' | 'custom'; + 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( + '; - } -} - -function fetchVehicles() { - // following elemets will be blocked (disabled) when we make the request - let elements_to_block = ['vehicle', 'version', 'board', 'submit', 'reset_def', 'exp_col_button']; - enableDisableElementsById(elements_to_block, false); - let request_url = '/api/v1/vehicles'; - setSpinnerToDiv('vehicle_list', 'Fetching vehicles...'); - sendAjaxRequestForJsonResponse(request_url) - .then((json_response) => { - let all_vehicles = json_response; - - if (preSelectConfig.vehicleId) { - const vehicleExists = all_vehicles.some(v => v.id === preSelectConfig.vehicleId); - if (!vehicleExists) { - const msg = `Vehicle '${preSelectConfig.vehicleId}' is no longer listed for building. Redirecting to new build page...`; - console.warn(msg); - alert(msg); - window.location.href = '/add_build'; - return; - } - } - - let new_vehicle = preSelectConfig.vehicleId || - (all_vehicles.find(vehicle => vehicle.name === "Copter") ? "copter" : all_vehicles[0].id); - updateVehicles(all_vehicles, new_vehicle); - }) - .catch((message) => { - console.log("Vehicle update failed. "+message); - }) - .finally(() => { - enableDisableElementsById(elements_to_block, true); - }); -} - -function updateVehicles(all_vehicles, new_vehicle_id) { - let vehicle_element = document.getElementById('vehicle'); - let old_vehicle_id = vehicle_element ? vehicle_element.value : ''; - fillVehicles(all_vehicles, new_vehicle_id); - if (old_vehicle_id != new_vehicle_id) { - onVehicleChange(new_vehicle_id); - } -} - -function onVehicleChange(new_vehicle_id) { - // following elemets will be blocked (disabled) when we make the request - let elements_to_block = ['vehicle', 'version', 'board', 'submit', 'reset_def', 'exp_col_button']; - enableDisableElementsById(elements_to_block, false); - let request_url = '/api/v1/vehicles/'+new_vehicle_id+'/versions'; - setSpinnerToDiv('version_list', 'Fetching versions...'); - sendAjaxRequestForJsonResponse(request_url) - .then((json_response) => { - let all_versions = json_response; - all_versions = sortVersions(all_versions); - - if (preSelectConfig.versionId) { - const versionExists = all_versions.some(v => v.id === preSelectConfig.versionId); - if (!versionExists) { - const msg = `Version '${preSelectConfig.versionId}' is no longer listed for building. Redirecting to new build page...`; - console.warn(msg); - alert(msg); - window.location.href = '/add_build'; - return; - } - - } - - const new_version = preSelectConfig.versionId || all_versions[0].id; - updateVersions(all_versions, new_version); - }) - .catch((message) => { - console.log("Version update failed. "+message); - }) - .finally(() => { - enableDisableElementsById(elements_to_block, true); - }); -} - -function updateVersions(all_versions, new_version) { - let version_element = document.getElementById('version'); - let old_version = version_element ? version_element.value : ''; - fillVersions(all_versions, new_version); - if (old_version != new_version) { - onVersionChange(new_version); - } -} - -function onVersionChange(new_version) { - // following elemets will be blocked (disabled) when we make the request - let elements_to_block = ['vehicle', 'version', 'board', 'submit', 'reset_def', 'exp_col_button']; - enableDisableElementsById(elements_to_block, false); - let vehicle_id = document.getElementById("vehicle").value; - let version_id = new_version; - - // Fetch boards first - let boards_url = `/api/v1/vehicles/${vehicle_id}/versions/${version_id}/boards`; - setSpinnerToDiv('board_list', 'Fetching boards...'); - - // Clear build options and show loading state - let temp_container = document.createElement('div'); - temp_container.id = "temp_container"; - temp_container.setAttribute('class', 'container-fluid w-25 mt-3'); - let features_list_element = document.getElementById('build_options'); - features_list_element.innerHTML = ""; - features_list_element.appendChild(temp_container); - setSpinnerToDiv('temp_container', 'Fetching features...'); - - // Fetch boards - sendAjaxRequestForJsonResponse(boards_url) - .then((boards_response) => { - // Keep full board objects with id and name - let boards = boards_response; - - if (preSelectConfig.boardId) { - const boardExists = boards.some(b => b.id === preSelectConfig.boardId); - if (!boardExists) { - const msg = `Board '${preSelectConfig.boardId}' is no longer listed for building. Redirecting to new build page...`; - console.warn(msg); - alert(msg); - window.location.href = '/add_build'; - return; - } - - } - let new_board = preSelectConfig.boardId || (boards.length > 0 ? boards[0].id : null); - updateBoards(boards, new_board); - }) - .catch((message) => { - console.log("Boards update failed. "+message); - }) - .finally(() => { - enableDisableElementsById(elements_to_block, true); - }); -} - -function updateBoards(all_boards, new_board) { - let board_element = document.getElementById('board'); - let old_board = board_element ? board_element.value : ''; - fillBoards(all_boards, new_board); - if (old_board != new_board) { - onBoardChange(new_board); - } -} - -function onBoardChange(new_board) { - // When board changes, fetch features for the new board - let vehicle_id = document.getElementById('vehicle').value; - let version_id = document.getElementById('version').value; - - let temp_container = document.createElement('div'); - temp_container.id = "temp_container"; - temp_container.setAttribute('class', 'container-fluid w-25 mt-3'); - let features_list_element = document.getElementById('build_options'); - features_list_element.innerHTML = ""; - features_list_element.appendChild(temp_container); - setSpinnerToDiv('temp_container', 'Fetching features...'); - - let features_url = `/api/v1/vehicles/${vehicle_id}/versions/${version_id}/boards/${new_board}/features`; - sendAjaxRequestForJsonResponse(features_url) - .then((features_response) => { - Features.reset(features_response); - fillBuildOptions(features_response); - - // TODO: Refactor to use a single method to apply both rebuild and default features - if (preSelectConfig.isRebuildMode) { - applyPreSelectedFeatures(preSelectConfig.selectedFeatures); - clearPreSelectConfig(); - } else { - Features.applyDefaults(); - } - }) - .catch((message) => { - console.log("Features update failed. "+message); - }); -} - -function fillBoards(boards, default_board_id) { - let output = document.getElementById('board_list'); - output.innerHTML = '' + - ''; - let boardList = document.getElementById("board") - boards.forEach(board => { - let opt = document.createElement('option'); - opt.value = board.id; - opt.innerHTML = board.name; - opt.selected = (board.id === default_board_id); - boardList.appendChild(opt); - }); -} - - -var toggle_all_categories = (() => { - let all_categories_expanded = init_categories_expanded; - - function toggle_method() { - // toggle global state - all_categories_expanded = !all_categories_expanded; - - let all_collapse_elements = document.getElementsByClassName('feature-group'); - - for (let i=0; i { - options_html += '
    ' + - '' + - '' + - '
    '; - }); - - let id_prefix = Features.getCategoryIdByName(category_name); - let card_element = document.createElement('div'); - card_element.setAttribute('class', 'card ' + (expanded == true ? 'h-100' : '')); - card_element.id = id_prefix + '_card'; - card_element.innerHTML = '
    ' + - '
    ' + - '
    ' + - '' + - '' + - '' + - '' + - '
    ' + - '' + - '
    ' + - '
    '; - let collapse_element = document.createElement('div'); - collapse_element.setAttribute('class', 'feature-group collapse '+(expanded == true ? 'show' : '')); - collapse_element.id = id_prefix + '_collapse'; - collapse_element.innerHTML = '
    '+options_html+'
    '; - card_element.appendChild(collapse_element); - - // add relevent event listeners - collapse_element.addEventListener('hide.bs.collapse', () => { - card_element.classList.remove('h-100'); - document.getElementById(id_prefix+'_icon').setAttribute('class', 'bi bi-chevron-down'); - }); - collapse_element.addEventListener('shown.bs.collapse', () => { - card_element.classList.add('h-100'); - document.getElementById(id_prefix+'_icon').setAttribute('class', 'bi bi-chevron-up'); - }); - - return card_element; -} - -function fillBuildOptions(features) { - let output = document.getElementById('build_options'); - output.innerHTML = `
    -
    -

    Available features for the current selection are:

    -
    - -
    `; - - // Group features by category - let categories_map = {}; - features.forEach(feature => { - const cat_name = feature.category.name; - if (!categories_map[cat_name]) { - categories_map[cat_name] = []; - } - categories_map[cat_name].push(feature); - }); - - // Convert to array and display - let categories = Object.entries(categories_map).map(([name, feats]) => ({name, features: feats})); - - categories.forEach((category, cat_idx) => { - if (cat_idx % 4 == 0) { - let new_row = document.createElement('div'); - new_row.setAttribute('class', 'row'); - new_row.id = 'category_'+parseInt(cat_idx/4)+'_row'; - output.appendChild(new_row); - } - let col_element = document.createElement('div'); - col_element.setAttribute('class', 'col-md-3 col-sm-6 mb-2'); - col_element.appendChild(createCategoryCard(category.name, category.features, init_categories_expanded)); - document.getElementById('category_'+parseInt(cat_idx/4)+'_row').appendChild(col_element); - }); -} - -// returns a Promise -// the promise is resolved when we recieve status code 200 from the AJAX request -// the JSON response for the request is returned in such case -// the promise is rejected when the status code is not 200 -// the status code is returned in such case -function sendAjaxRequestForJsonResponse(url) { - return new Promise((resolve, reject) => { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url); - - // disable cache, thanks to: https://stackoverflow.com/questions/22356025/force-cache-control-no-cache-in-chrome-via-xmlhttprequest-on-f5-reload - xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0"); - xhr.setRequestHeader("Expires", "Tue, 01 Jan 1980 1:00:00 GMT"); - xhr.setRequestHeader("Pragma", "no-cache"); - - xhr.onload = function () { - if (xhr.status == 200) { - resolve(JSON.parse(xhr.response)); - } else { - reject("Got response:"+xhr.response+" (Status Code: "+xhr.status+")"); - } - } - xhr.send(); - }); -} - -function fillVehicles(vehicles, vehicle_id_to_select) { - var output = document.getElementById('vehicle_list'); - output.innerHTML = '' + - ''; - vehicleList = document.getElementById("vehicle"); - vehicles.forEach(vehicle => { - opt = document.createElement('option'); - opt.value = vehicle.id; - opt.innerHTML = vehicle.name; - opt.selected = (vehicle.id === vehicle_id_to_select); - vehicleList.appendChild(opt); - }); -} - -function compareVersionNums(a, b) { - const versionRegex = /(\d+)\.(\d+)\.(\d+)/; - - const [, aMajor, aMinor, aPatch] = a.match(versionRegex).map(Number); - const [, bMajor, bMinor, bPatch] = b.match(versionRegex).map(Number); - - if (aMajor !== bMajor) return bMajor - aMajor; - if (aMinor !== bMinor) return bMinor - aMinor; - return bPatch - aPatch; -} - -function sortVersions(versions) { - const order = { - "beta" : 0, - "latest": 1, - "stable": 2, - "tag" : 3, - } - - versions.sort((a, b) => { - // sort the version types in order mentioned above - if (a.type != b.type) { - return order[a.type] - order[b.type]; - } - - // for numbered versions, do reverse sorting to make sure recent versions come first - if (a.type == "stable" || b.type == "beta") { - return compareVersionNums(a.name.split(" ")[1], b.name.split(" ")[1]); - } - - return a.name.localeCompare(b.name); - }); - - // Push the first stable version in the list to the top - const firstStableIndex = versions.findIndex(v => v.name.split(" ")[0].toLowerCase() === "stable"); - if (firstStableIndex !== -1) { - const stableVersion = versions.splice(firstStableIndex, 1)[0]; - versions.unshift(stableVersion); - } - - return versions; -} - -function fillVersions(versions, version_to_select) { - var output = document.getElementById('version_list'); - output.innerHTML = '' + - ''; - versionList = document.getElementById("version"); - - versions.forEach(version => { - opt = document.createElement('option'); - opt.value = version.id; - opt.innerHTML = version.name; - opt.selected = (version.id === version_to_select); - versionList.appendChild(opt); - }); -} - -// Handle form submission -async function handleFormSubmit(event) { - event.preventDefault(); - - const submitButton = document.getElementById('submit'); - const originalButtonText = submitButton.innerHTML; - - try { - // Disable submit button and show loading state - submitButton.disabled = true; - submitButton.innerHTML = 'Submitting...'; - - // Collect form data - const vehicle_id = document.getElementById('vehicle').value; - const version_id = document.getElementById('version').value; - const board_id = document.getElementById('board').value; - - // Collect selected features - checkboxes now have feature IDs directly - const selected_features = []; - const checkboxes = document.querySelectorAll('.feature-checkbox:checked'); - checkboxes.forEach(checkbox => { - // The checkbox ID is already the feature define (ID) - selected_features.push(checkbox.id); - }); - - // Create build request payload - const buildRequest = { - vehicle_id: vehicle_id, - version_id: version_id, - board_id: board_id, - selected_features: selected_features - }; - - // Send POST request to API - const response = await fetch('/api/v1/builds', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(buildRequest) - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || 'Failed to submit build'); - } - - const result = await response.json(); - - // Redirect to viewlog page - window.location.href = `/?build_id=${result.build_id}`; - - } catch (error) { - console.error('Error submitting build:', error); - alert('Failed to submit build: ' + error.message); - - // Re-enable submit button - submitButton.disabled = false; - submitButton.innerHTML = originalButtonText; - } -} - -// Initialize form submission handler -document.addEventListener('DOMContentLoaded', () => { - const buildForm = document.getElementById('build-form'); - if (buildForm) { - buildForm.addEventListener('submit', handleFormSubmit); - } -}); diff --git a/backend/static/js/index.js b/backend/static/js/index.js deleted file mode 100644 index 3e54b2e7..00000000 --- a/backend/static/js/index.js +++ /dev/null @@ -1,238 +0,0 @@ -function init() { - refresh_builds(); - // initialise tooltips by selector - $('body').tooltip({ - selector: '[data-bs-toggle="tooltip"]' - }); - - const params = new URLSearchParams(window.location.search); - const buildId = params.get("build_id"); - if (buildId) { - launchLogModal(buildId); - autoDownloadIntervalId = setInterval(tryAutoDownload, 5000, buildId); - } -} - -function refresh_builds() { - var xhr = new XMLHttpRequest(); - xhr.open('GET', "/api/v1/builds"); - - // disable cache, thanks to: https://stackoverflow.com/questions/22356025/force-cache-control-no-cache-in-chrome-via-xmlhttprequest-on-f5-reload - xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0"); - xhr.setRequestHeader("Expires", "Tue, 01 Jan 1980 1:00:00 GMT"); - xhr.setRequestHeader("Pragma", "no-cache"); - - xhr.onload = function () { - if (xhr.status === 200) { - updateBuildsTable(JSON.parse(xhr.response)); - } - setTimeout(refresh_builds, 5000); - } - xhr.send(); -} - -function showFeatures(row_num) { - document.getElementById("featureModalBody").innerHTML = document.getElementById(`${row_num}_features_all`).innerHTML; - var feature_modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('featureModal')); - feature_modal.show(); - return; -} - -function timeAgo(timestampStr) { - const timestamp = parseFloat(timestampStr); - const now = Date.now() / 1000; - const diff = now - timestamp; - - if (diff < 0) return "In the future"; - - const hours = Math.floor(diff / 3600); - const minutes = Math.floor((diff % 3600) / 60); - - return `${hours}h ${minutes}m`; -} - -function updateBuildsTable(builds) { - let output_container = document.getElementById('build_table_container'); - if (builds.length == 0) { - output_container.innerHTML = `
    `; - return; - } - - // hide any tooltips which are currently open - // this is needed as they might get stuck - // if the element to which they belong goes out of the dom tree - $('.tooltip-button').tooltip("hide"); - - let table_body_html = ''; - let row_num = 0; - builds.forEach((build_info) => { - let status_color = 'primary'; - if (build_info['progress']['state'] == 'SUCCESS') { - status_color = 'success'; - } else if (build_info['progress']['state'] == 'PENDING') { - status_color = 'warning'; - } else if (build_info['progress']['state'] == 'FAILURE' || build_info['progress']['state'] == 'ERROR' || build_info['progress']['state'] == 'TIMED_OUT') { - status_color = 'danger'; - } - - const features_string = build_info['selected_features'].join(', ') - const build_age = timeAgo(build_info['time_created']) - - const isNonTerminal = (build_info['progress']['state'] == 'PENDING' || build_info['progress']['state'] == 'RUNNING'); - const downloadDisabled = isNonTerminal ? 'disabled' : ''; - const download_button_color = isNonTerminal ? 'secondary' : 'primary'; - - table_body_html += ` - ${build_info['progress']['state']} - ${build_age} - ${build_info['version']['git_hash'].substring(0,8)} - ${build_info['board']['name']} - ${build_info['vehicle']['name']} - - ${features_string.substring(0, 100)}... - - show more - - -
    -
    ${build_info['progress']['percent']}%
    -
    - - - - - - - `; - row_num += 1; - }); - - let table_html = ` - - - - - - - - - - - ${table_body_html} -
    StatusAgeGit HashBoardVehicleFeaturesProgressActions
    `; - output_container.innerHTML = table_html; -} - -const LogFetch = (() => { - var stopFetch = true; - var build_id = null; - var scheduled_fetches = 0; - - function startLogFetch(new_build_id) { - build_id = new_build_id; - stopFetch = false; - if (scheduled_fetches <= 0) { - scheduled_fetches = 1; - fetchLogFile(); - } - } - - function stopLogFetch() { - stopFetch = true; - } - - function getBuildId() { - return build_id; - } - - function fetchLogFile() { - if (stopFetch || !build_id) { - scheduled_fetches -= 1; - return; - } - - var xhr = new XMLHttpRequest(); - xhr.open('GET', `/api/v1/builds/${build_id}/logs`); - - // disable cache, thanks to: https://stackoverflow.com/questions/22356025/force-cache-control-no-cache-in-chrome-via-xmlhttprequest-on-f5-reload - xhr.setRequestHeader("Cache-Control", "no-cache, no-store, max-age=0"); - xhr.setRequestHeader("Expires", "Tue, 01 Jan 1980 1:00:00 GMT"); - xhr.setRequestHeader("Pragma", "no-cache"); - - xhr.onload = () => { - if (xhr.status == 200) { - let logTextArea = document.getElementById('logTextArea'); - let autoScrollSwitch = document.getElementById('autoScrollSwitch'); - logTextArea.textContent = xhr.responseText; - if (autoScrollSwitch.checked) { - logTextArea.scrollTop = logTextArea.scrollHeight; - } - - if (xhr.responseText.includes('BUILD_FINISHED')) { - stopFetch = true; - } - } - if (!stopFetch) { - setTimeout(fetchLogFile, 3000); - } else { - scheduled_fetches -= 1; - } - } - xhr.send(); - } - - return {startLogFetch, stopLogFetch, getBuildId}; -})(); - -function launchLogModal(build_id) { - document.getElementById('logTextArea').textContent = `Fetching build log...\nBuild ID: ${build_id}`; - LogFetch.startLogFetch(build_id); - let logModalElement = document.getElementById('logModal'); - logModalElement.addEventListener('hide.bs.modal', () => { - LogFetch.stopLogFetch(); - }); - let logModal = bootstrap.Modal.getOrCreateInstance(logModalElement); - logModal.show(); -} - -// Trigger auto-download if state changes from "RUNNING" to "SUCCESS" -let previousState = null; -let autoDownloadIntervalId = null; - -async function tryAutoDownload(buildId) { - if (!autoDownloadIntervalId) { - return; - } - - try { - const apiUrl = `/api/v1/builds/${buildId}` - const response = await fetch(apiUrl); - const data = await response.json(); - - const currentState = data.progress?.state; - - if (previousState === "RUNNING" && currentState === "SUCCESS") { - console.log("Build completed successfully. Starting download..."); - window.location.href = `/api/v1/builds/${buildId}/artifact`; - } - - // Stop running if the build is in a terminal state - if (["FAILURE", "SUCCESS", "ERROR", "TIMED_OUT"].includes(currentState)) { - clearInterval(autoDownloadIntervalId); - return; - } - - previousState = currentState; - } catch (err) { - console.error("Failed to fetch build status:", err); - } -}; diff --git a/backend/static/styles/main.css b/backend/static/styles/main.css deleted file mode 100644 index 27ed12c1..00000000 --- a/backend/static/styles/main.css +++ /dev/null @@ -1,191 +0,0 @@ -@charset "utf-8"; -/* CSS Document */ - -/* Background-Styles */ -body { - font-family:Helvetica; - margin:0px; - padding:0px; - background-color: #fff; - background-image: url(/static/images/bg.png); -} - -#logo { - background-image:url(/static/images/logo.png); - background-repeat:no-repeat; - height: 140px; - width: 407px; - -moz-box-shadow: 2px 2px 5px #888; - -webkit-box-shadow: 2px 2px 5px #888; - box-shadow: 2px 2px 5px #888; - background-color: #000; -} - -h2 { - text-shadow: #ccc 0px 1px 0px; - text-decoration:none; - color:#D14836; - padding-top: 0px; - padding-right: 5px; - padding-bottom: 5px; - padding-left: 5px; -} - -#git { - background-color: #FFF; - font-size: 15px; - display: auto; - padding: 10px; - border: 1px solid #CCC; - position: relative; - -moz-box-shadow: 2px 2px 5px #888; - -webkit-box-shadow: 2px 2px 5px #888; - box-shadow: 2px 2px 5px #888; -} - -#git a { - color:#00F; - font-weight: normal; - } - -#git a:hover { - color:#D14836; -} - -.passed-text { - color:green; -} - -.failed-text { - color:red; -} - - -#main { - padding-top:0px; - min-width: 420px; - margin-top: 0; - margin-right: auto; - margin-bottom: 0; - margin-left: auto; - padding-right: 20px; - padding-bottom: 50px; - padding-left: 20px; -} - -ul#testresults { - background-color: #FFF; - display: block; - padding: 10px; - border: 1px solid #CCC; - list-style-type: none; - position: relative; - -moz-box-shadow: 2px 2px 5px #888; - -webkit-box-shadow: 2px 2px 5px #888; - box-shadow: 2px 2px 5px #888; -} - -ul#testresults li { - list-style-type: none; - display: block; - background-color: #eef1f1; - margin: 5px; - padding: 5px; - } - -ul#testlogs { - background-color: #FFF; - display: block; - padding: 10px; - border: 1px solid #CCC; - list-style-type: none; - position: relative; - -moz-box-shadow: 2px 2px 5px #888; - -webkit-box-shadow: 2px 2px 5px #888; - box-shadow: 2px 2px 5px #888; -} - -ul#testlogs li { - text-decoration:none; - list-style-type: none; - display: block; - background-color: #eef1f1; - margin: 5px; - padding: 5px; - font-size: 15px; - } - - -ul#testlogs li a { - text-decoration:none; - color:#8aa19c; - font-weight: bold; - font-size: 12px; - } - -ul#testlogs li a:hover { - color:#D14836; -} - -.status-table { - border-collapse: collapse; - margin: 25px 0; - font-size: 0.9em; - font-family: sans-serif; - min-width: 400px; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); -} - -.status-table thead tr { - background-color: #009879; - color: #ffffff; - text-align: left; -} - -.status-table th, -.status-table td { - padding: 12px 15px; -} - -.status-table tbody tr { - border-bottom: 1px solid #dddddd; -} - -.status-table tbody tr.Failed { - font-weight: bold; - background-color: rgba(255,0,0,0.2); -} - -.status-table tbody tr.Error { - font-weight: bold; - background-color: rgba(255,0,0,0.2); -} - -.status-table tbody tr.Pending { - font-weight: bold; - background-color: rgba(128,128,0,0.1); -} - -.status-table tbody tr.Finished { - font-weight: bold; - background-color: rgba(0,255,0,0.1); -} - -.status-table tbody tr.Running { - font-weight: bold; - background-color: rgba(0,255,0,0.15); -} -.collapsibleList li{ - list-style-image : url('/static/images/bg.png'); - cursor : auto; -} - -li.collapsibleListOpen{ - list-style-image : url('/static/images/button-open.png'); - cursor : pointer; -} - -li.collapsibleListClosed{ - list-style-image : url('/static/images/button-closed.png'); - cursor : pointer; -} diff --git a/backend/templates/add_build.html b/backend/templates/add_build.html deleted file mode 100644 index 6c97488f..00000000 --- a/backend/templates/add_build.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - ArduPilot Custom Firmware Builder - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    - ADD NEW BUILD -
    -
    -
    -
    -
    -
    -
    - Fetching Vehicles... - -
    -
    -
    -
    -
    -
    - Fetching versions... - -
    -
    -
    -
    -
    -
    - Fetching boards... - -
    -
    -
    -
    -
    -
    -
    -
    - Fetching features... - -
    -
    -
    -
    -
    - -
    -
    - - - - - - - diff --git a/backend/templates/index.html b/backend/templates/index.html deleted file mode 100644 index 2a6bc177..00000000 --- a/backend/templates/index.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - ArduPilot Custom Firmware Builder - - - - - - - - - - - - - - - - - - - - - -
    -
    - -
    -
    -
    - Fetching builds... - -
    -
    -
    -
    -
    -
    - Credits: - See Contributors - | - Source: - Ardupilot/CustomBuild - -
    - To try out the newest features of the app, please visit our beta server - custom-beta.ardupilot.org - -
    -
    - - - - - - - - - - - - - - - - diff --git a/backend/ui/__init__.py b/backend/ui/__init__.py deleted file mode 100644 index c2574d30..00000000 --- a/backend/ui/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -UI module for web interface routes. -""" -from backend.ui.router import router - -__all__ = ["router"] diff --git a/backend/ui/router.py b/backend/ui/router.py deleted file mode 100644 index c082c80f..00000000 --- a/backend/ui/router.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Web UI routes for serving HTML templates. -""" -from fastapi import APIRouter, Request -from fastapi.responses import HTMLResponse -from fastapi.templating import Jinja2Templates -from pathlib import Path - -router = APIRouter(tags=["web"]) - -# Setup templates directory -WEB_ROOT = Path(__file__).resolve().parent.parent -templates = Jinja2Templates(directory=str(WEB_ROOT / "templates")) - - -@router.get("/", response_class=HTMLResponse) -async def index(request: Request): - """ - Render the main index page showing all builds. - - Args: - request: FastAPI Request object - - Returns: - Rendered HTML template - """ - return templates.TemplateResponse( - "index.html", - {"request": request} - ) - - -@router.get("/add_build", response_class=HTMLResponse) -async def add_build(request: Request): - """ - Render the add build page for creating new firmware builds. - - Args: - request: FastAPI Request object - - Returns: - Rendered HTML template - """ - return templates.TemplateResponse( - "add_build.html", - {"request": request} - ) From 7db1a8af1a5a475d21ccb020e83d69b46cfcd668 Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Wed, 26 Aug 2026 11:15:22 +0530 Subject: [PATCH 08/11] docker: do not copy whole repo in builder and backend images --- backend/Dockerfile | 14 ++++++++++++-- builder/Dockerfile | 13 ++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 4feb9619..18bd177b 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -9,9 +9,19 @@ RUN groupadd -g 999 ardupilot && \ mkdir /app && \ chown ardupilot:ardupilot /app -COPY --chown=ardupilot:ardupilot . /app +COPY backend/requirements.txt /app/backend/requirements.txt +RUN pip install --no-cache-dir -r /app/backend/requirements.txt + +COPY --chown=ardupilot:ardupilot ap_git /app/ap_git +COPY --chown=ardupilot:ardupilot build_config /app/build_config +COPY --chown=ardupilot:ardupilot build_manager /app/build_manager +COPY --chown=ardupilot:ardupilot builder /app/builder +COPY --chown=ardupilot:ardupilot metadata_manager /app/metadata_manager +COPY --chown=ardupilot:ardupilot utils /app/utils +COPY --chown=ardupilot:ardupilot schemas /app/schemas +COPY --chown=ardupilot:ardupilot backend /app/backend + WORKDIR /app/backend -RUN pip install --no-cache-dir -r requirements.txt ENV PYTHONPATH=/app diff --git a/builder/Dockerfile b/builder/Dockerfile index ad7e3dfd..56248508 100644 --- a/builder/Dockerfile +++ b/builder/Dockerfile @@ -7,10 +7,17 @@ RUN groupadd -g 999 ardupilot && \ mkdir /workdir && \ chown ardupilot:ardupilot /workdir -COPY --chown=ardupilot:ardupilot . /app +COPY builder/requirements.txt /app/builder/requirements.txt +RUN pip install --no-cache-dir -r /app/builder/requirements.txt -WORKDIR /app -RUN pip install --no-cache-dir -r ./builder/requirements.txt +COPY --chown=ardupilot:ardupilot ap_git /app/ap_git +COPY --chown=ardupilot:ardupilot build_config /app/build_config +COPY --chown=ardupilot:ardupilot build_manager /app/build_manager +COPY --chown=ardupilot:ardupilot builder /app/builder +COPY --chown=ardupilot:ardupilot metadata_manager /app/metadata_manager +COPY --chown=ardupilot:ardupilot utils /app/utils +COPY --chown=ardupilot:ardupilot schemas /app/schemas +WORKDIR /app USER ardupilot CMD ["python", "-m", "builder"] From 0a97f034f1609b2f642d7c9df6d6705f60e4a198 Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Sat, 29 Aug 2026 09:55:01 +0530 Subject: [PATCH 09/11] backend: extend build info to show if build artifacts are available --- backend/schemas/builds.py | 4 +++ backend/services/builds.py | 38 ++++++++++++++++++---------- tests/backend/test_builds_api.py | 2 ++ tests/backend/test_builds_service.py | 34 +++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/backend/schemas/builds.py b/backend/schemas/builds.py index 6a733c8a..cfe47e73 100644 --- a/backend/schemas/builds.py +++ b/backend/schemas/builds.py @@ -76,3 +76,7 @@ class BuildOut(BaseModel): time_created: float = Field( ..., description="Unix timestamp when build was created" ) + artifact_available: bool = Field( + False, + description="True when the build archive exists and can be downloaded", + ) diff --git a/backend/services/builds.py b/backend/services/builds.py index ccc9baa7..edae18ea 100644 --- a/backend/services/builds.py +++ b/backend/services/builds.py @@ -257,20 +257,7 @@ def get_artifact_path(self, build_id: str) -> Optional[str]: if build_info is None: return None - # Return early if build is still ongoing - if build_info.progress.state in [ - build_manager.BuildState.PENDING, - build_manager.BuildState.RUNNING, - ]: - return None - - 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 + return self._artifact_path_if_available(build_id, build_info) def get_build_config_yaml(self, build_id: str) -> Optional[tuple]: """ @@ -372,7 +359,30 @@ def _build_info_to_output( selected_features=list(build_info.selected_features), progress=progress, time_created=build_info.time_created, + artifact_available=self._artifact_path_if_available( + build_id, build_info + ) is not None, + ) + + def _artifact_path_if_available( + self, + build_id: str, + build_info, + ) -> Optional[str]: + """Return archive path if the build is finished and the file exists.""" + if build_info.progress.state in [ + build_manager.BuildState.PENDING, + build_manager.BuildState.RUNNING, + ]: + return None + + 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_builds_service(request: Request) -> BuildsService: diff --git a/tests/backend/test_builds_api.py b/tests/backend/test_builds_api.py index 84fdd95d..2edbc651 100644 --- a/tests/backend/test_builds_api.py +++ b/tests/backend/test_builds_api.py @@ -230,6 +230,7 @@ def test_list_builds_response_schema_has_required_fields(self, client): for field in [ "build_id", "vehicle", "board", "version", "selected_features", "progress", "time_created", + "artifact_available", ]: assert field in build assert "id" in build["vehicle"] and "name" in build["vehicle"] @@ -317,6 +318,7 @@ def test_get_build_response_schema_has_required_fields(self, client): for field in [ "build_id", "vehicle", "board", "version", "selected_features", "progress", "time_created", + "artifact_available", ]: assert field in data diff --git a/tests/backend/test_builds_service.py b/tests/backend/test_builds_service.py index 5f50427a..38e9e331 100644 --- a/tests/backend/test_builds_service.py +++ b/tests/backend/test_builds_service.py @@ -571,6 +571,40 @@ def test_get_build_no_selected_features_returns_empty_list( assert result.selected_features == [] + def test_get_build_artifact_available_when_archive_exists( + self, + service, + mock_build_manager, + tmp_path, + ): + """artifact_available is True when the archive is on disk.""" + artifact = tmp_path / "artifact.tar.gz" + artifact.write_bytes(b"firmware") + mock_build_manager.build_exists.return_value = True + mock_build_manager.get_build_info.return_value = make_build_info( + state=bm.BuildState.SUCCESS, + ) + mock_build_manager.get_build_archive_path.return_value = str(artifact) + + result = service.get_build("build-abc123") + + assert result.artifact_available is True + + def test_get_build_artifact_unavailable_when_pending( + self, + service, + mock_build_manager, + ): + """artifact_available is False while the build is still running.""" + mock_build_manager.build_exists.return_value = True + mock_build_manager.get_build_info.return_value = make_build_info( + state=bm.BuildState.PENDING, + ) + + result = service.get_build("build-abc123") + + assert result.artifact_available is False + # Tests for get_build_logs def test_get_build_logs_returns_none_when_build_not_found( From 9a1ad6c7985eb850ba0e254038608e86c9139a3b Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Wed, 26 Aug 2026 11:16:29 +0530 Subject: [PATCH 10/11] .dockerignore: do not pack stuff which is not needed in images --- .dockerignore | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..90b62b92 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# Compose builds all images with context: . — this file is the only ignore Docker reads. + +# VCS / tooling +.git +.github +.vagrant +.direnv +.pytest_cache +.mypy_cache +**/__pycache__ +**/*.pyc +**/*.pyo +**/*.tsbuildinfo + +# Secrets / env +.env +.envrc + +# Local data and virtualenvs +base +redis_data +.venv +venv + +# Frontend install/build artifacts (must not overlay Alpine npm ci) +**/node_modules +frontend/dist +frontend/public/schemas + +# Not needed in any image +tests +scripts +Vagrantfile +examples +*.md +LICENSE From 3f150a8bcce6074113c752a7bc5f82d58f875070 Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Sat, 29 Aug 2026 09:55:59 +0530 Subject: [PATCH 11/11] .github: add basic lint and build workflows for frontend --- .github/workflows/tests.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c8f76209..0b2c7a00 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,3 +32,24 @@ jobs: - name: Run tests run: | pytest tests/ -v --tb=short + + frontend: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install and check frontend + working-directory: frontend + run: | + npm ci + npm run lint + npm run build