diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75245ef..dc42f64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,3 +98,42 @@ jobs: fail-build: true severity-cutoff: high only-fixed: true + + push: + name: Push image to Docker Hub + runs-on: ubuntu-latest + needs: [container-scan] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + concurrency: + group: push-image + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Image tags + id: meta + uses: docker/metadata-action@v5 + with: + images: docker.io/${{ secrets.DOCKERHUB_USERNAME }}/bto + tags: | + type=sha,prefix= + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/README.md b/README.md index 5dec034..1c64a91 100644 --- a/README.md +++ b/README.md @@ -29,21 +29,38 @@ tap-through drilldown: With Docker Compose (both services, hot-reload): ```bash -docker compose up --build # UI on :5173, API on :8000 +docker compose up --build # UI on :5173/tides/, API on :8000/api/tides ``` Or run the two halves yourself: ```bash cd backend && uv run uvicorn app.main:app --reload # :8000 -cd frontend && npm install && npm run dev # :5173, proxies /api +cd frontend && npm install && npm run dev # :5173/tides/, proxies /api ``` For a single production container — frontend built and served as static files by FastAPI: ```bash -docker build -t bto . && docker run -p 8000:8000 bto # :8000 +docker build -t bto . && docker run -p 8000:8000 bto # :8000/tides +``` + +Everything is served under `/tides` (UI) and `/api/tides` (API) so it can share +the sean-michael.dev domain with the main site. + +## Deploy + +Pushes to `main` that pass CI publish `/bto:latest` to Docker Hub +(needs the `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` repo secrets). The container +runs on the main site's EC2 box: its `tides` service is in +[Sean-Michael.dev](https://github.com/Sean-Michael/Sean-Michael.dev)'s +`docker-compose.yaml`, and Traefik routes `/tides` and `/api/tides` to it. To turn +it on, add to `/opt/app/.env` on the box: + +```bash +COMPOSE_PROFILES=tides +TIDES_IMAGE=/bto:latest ``` ## How it works diff --git a/backend/app/api.py b/backend/app/api.py index 8318050..2b0e4e4 100644 --- a/backend/app/api.py +++ b/backend/app/api.py @@ -1,6 +1,7 @@ """Bellingham Tidal Observatory REST API. -A single ``/api/bootstrap`` call returns everything the dashboard needs: station +Mounted under ``/api/tides`` (the site routes that prefix here). A single +``/api/tides/bootstrap`` call returns everything the dashboard needs: station metadata plus real NOAA hi/lo predictions (extrema) spanning a wide window, plus sun/moon facts. The frontend interpolates the smooth tide curve from those extrema client-side, so range/density/hover changes need no further round trips. @@ -17,7 +18,7 @@ from app import astro, noaa, stations -router = APIRouter(prefix="/api") +router = APIRouter(prefix="/api/tides") _TZ = ZoneInfo("America/Los_Angeles") @@ -61,9 +62,15 @@ async def _bootstrap() -> dict[str, Any]: begin = (now - timedelta(days=_PAST_DAYS)).strftime("%Y%m%d") end = (now + timedelta(days=_FUTURE_DAYS)).strftime("%Y%m%d") - payloads = await asyncio.gather( - *(_station_payload(s, begin, end) for s in stations.STATIONS) + # Don't let one flaky station sink the whole dashboard: gather, then drop + # any that failed (each station already falls back to last-known-good data). + results = await asyncio.gather( + *(_station_payload(s, begin, end) for s in stations.STATIONS), + return_exceptions=True, ) + payloads = [r for r in results if not isinstance(r, BaseException)] + if not payloads: + raise next(r for r in results if isinstance(r, BaseException)) sm = astro.sun_moon(now.date()) return { @@ -83,7 +90,7 @@ async def _bootstrap() -> dict[str, Any]: "moon_illum": sm.moon_illum, "moon_glyph": sm.moon_glyph, }, - "stations": list(payloads), + "stations": payloads, } diff --git a/backend/app/main.py b/backend/app/main.py index 2414b78..5a75352 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,16 +1,20 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from app.api import router as api_router from app.config import Settings, get_settings +# Everything lives under /tides (UI) and /api/tides (API) so the app can sit +# behind sean-michael.dev's Traefik next to the main site without path clashes. +BASE = "/tides" + def create_app(settings: Settings | None = None) -> FastAPI: settings = settings or get_settings() - app = FastAPI(title="Bellingham Tidal Observatory", version="0.1.0") + app = FastAPI(title="Bellingham Tidal Observatory", version="0.2.0") app.add_middleware( CORSMiddleware, @@ -20,7 +24,7 @@ def create_app(settings: Settings | None = None) -> FastAPI: allow_headers=["*"], ) - @app.get("/api/health") + @app.get("/api/tides/health") def health() -> dict[str, str]: return {"status": "ok"} @@ -31,14 +35,29 @@ def health() -> dict[str, str]: if settings.static_dir and settings.static_dir.is_dir(): assets = settings.static_dir / "assets" if assets.is_dir(): - app.mount("/assets", StaticFiles(directory=assets), name="assets") + app.mount(f"{BASE}/assets", StaticFiles(directory=assets), name="assets") + + static_root = settings.static_dir.resolve() + index_file = static_root / "index.html" - index_file = settings.static_dir / "index.html" + @app.get("/", include_in_schema=False) + def root() -> RedirectResponse: + return RedirectResponse(BASE) - @app.get("/{full_path:path}") - def spa(full_path: str) -> FileResponse: - # Single-page app fallback: any non-API route returns index.html. - return FileResponse(index_file) + @app.get(BASE, include_in_schema=False) + @app.get(BASE + "/{full_path:path}", include_in_schema=False) + def spa(full_path: str = "") -> FileResponse: + # Top-level files from frontend/public (favicon, background image). + candidate = (static_root / full_path).resolve() + if ( + full_path + and candidate.is_relative_to(static_root) + and candidate.is_file() + ): + return FileResponse(candidate) + # Single-page app fallback. index.html points at the current hashed + # bundles, so always revalidate it. + return FileResponse(index_file, headers={"Cache-Control": "no-cache"}) return app diff --git a/backend/app/noaa.py b/backend/app/noaa.py index a8c3cc0..0e02c7f 100644 --- a/backend/app/noaa.py +++ b/backend/app/noaa.py @@ -2,7 +2,8 @@ ``noaa_coops`` does blocking HTTP + pandas work, so every call is dispatched to a worker thread via :func:`asyncio.to_thread`. Results are memoized in-process -with a TTL so the dashboard's polling doesn't hammer the NOAA API. +with a TTL so the dashboard's polling doesn't hammer the NOAA API, and the +last-known-good data is served if NOAA is down (stale-if-error). """ from __future__ import annotations @@ -26,42 +27,65 @@ class Extremum: kind: str # "H" | "L" -_CACHE: dict[tuple[str, str, str], tuple[float, list[Extremum]]] = {} +# Keyed by station id (the begin/end window is always ~now±20d, so the last +# successful fetch stays usable). Holds the last-known-good extrema indefinitely +# so we can serve stale data when NOAA is flaky (predictions change very slowly). +_CACHE: dict[str, tuple[float, list[Extremum]]] = {} + +_RETRIES = 2 # extra attempts on transient NOAA failures def _fetch_blocking(station_id: str, begin: str, end: str) -> list[Extremum]: - station = CoopsStation(id=station_id) - df = station.get_data( - begin_date=begin, - end_date=end, - product="predictions", - datum="MLLW", - units="english", - time_zone="lst_ldt", - interval="hilo", - ) - out: list[Extremum] = [] - for ts, row in df.iterrows(): - out.append( - Extremum( - t=ts.to_pydatetime(), height=float(row["v"]), kind=str(row["type"]) + last_err: Exception | None = None + for attempt in range(_RETRIES + 1): + try: + station = CoopsStation(id=station_id) + df = station.get_data( + begin_date=begin, + end_date=end, + product="predictions", + datum="MLLW", + units="english", + time_zone="lst_ldt", + interval="hilo", ) - ) - return out + out: list[Extremum] = [] + for ts, row in df.iterrows(): + out.append( + Extremum( + t=ts.to_pydatetime(), + height=float(row["v"]), + kind=str(row["type"]), + ) + ) + return out + except Exception as err: # noqa: BLE001 — retry any upstream hiccup + last_err = err + if attempt < _RETRIES: + time.sleep(0.6 * (attempt + 1)) + assert last_err is not None + raise last_err async def fetch_extrema(station_id: str, begin: str, end: str) -> list[Extremum]: - """Return real NOAA hi/lo predictions for ``[begin, end]`` (YYYYMMDD).""" - key = (station_id, begin, end) - ttl = get_settings().cache_ttl + """Return real NOAA hi/lo predictions for ``[begin, end]`` (YYYYMMDD). + + Serves fresh data within the TTL, refetches after, and falls back to the + last-known-good extrema if NOAA is unavailable (stale-if-error). + """ now = time.monotonic() - cached = _CACHE.get(key) - if cached is not None and now - cached[0] < ttl: + cached = _CACHE.get(station_id) + if cached is not None and now - cached[0] < get_settings().cache_ttl: return cached[1] - extrema = await asyncio.to_thread(_fetch_blocking, station_id, begin, end) - _CACHE[key] = (now, extrema) - return extrema + try: + extrema = await asyncio.to_thread(_fetch_blocking, station_id, begin, end) + _CACHE[station_id] = (now, extrema) + return extrema + except Exception: + if cached is not None: + return cached[1] # stale-if-error + raise def clear_cache() -> None: diff --git a/backend/pyproject.toml b/backend/pyproject.toml index fc54107..c8472d6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -7,8 +7,13 @@ dependencies = [ "astral>=3.2", "fastapi>=0.136.3", "noaa-coops>=0.4.0", - "pydantic-settings>=2.14.1", + "pydantic-settings>=2.14.2", "uvicorn[standard]>=0.48.0", + # Floors for transitive deps flagged by the image scan (grype). + "anyio>=4.14.2", + "starlette>=1.3.1", + "urllib3>=2.8.0", + "zeep>=4.3.3", ] [dependency-groups] diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 9473dd7..11fabc0 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -17,6 +17,7 @@ def client() -> TestClient: def static_dir(tmp_path: Path) -> Path: """A minimal built-frontend directory: index.html + one asset.""" (tmp_path / "index.html").write_text("BTO") + (tmp_path / "favicon.svg").write_text("") assets = tmp_path / "assets" assets.mkdir() (assets / "app.js").write_text("console.log('bto')") diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 360ba5a..cd8d237 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -25,7 +25,7 @@ async def _fake(station_id: str, begin: str, end: str) -> list[noaa.Extremum]: def test_list_stations_returns_all_six(client: TestClient) -> None: - res = client.get("/api/stations") + res = client.get("/api/tides/stations") assert res.status_code == 200 body = res.json() assert len(body) == len(stations.STATIONS) == 6 @@ -33,7 +33,7 @@ def test_list_stations_returns_all_six(client: TestClient) -> None: def test_bootstrap_shape(client: TestClient, fake_noaa: None) -> None: - res = client.get("/api/bootstrap") + res = client.get("/api/tides/bootstrap") assert res.status_code == 200 body = res.json() assert body["datum"] == "MLLW" @@ -47,3 +47,58 @@ def test_bootstrap_shape(client: TestClient, fake_noaa: None) -> None: ex = stn["extrema"][0] assert {"t", "height", "kind"} <= ex.keys() assert isinstance(ex["t"], int) # epoch ms + + +def test_bootstrap_drops_failed_station( + client: TestClient, monkeypatch: pytest.MonkeyPatch, fake_noaa: None +) -> None: + ok = noaa.fetch_extrema + + async def flaky(station_id: str, begin: str, end: str) -> list[noaa.Extremum]: + if station_id == stations.STATIONS[0].id: + raise RuntimeError("NOAA down") + return await ok(station_id, begin, end) + + monkeypatch.setattr(noaa, "fetch_extrema", flaky) + body = client.get("/api/tides/bootstrap").json() + assert len(body["stations"]) == len(stations.STATIONS) - 1 + + +def test_bootstrap_502_when_every_station_fails( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + async def down(station_id: str, begin: str, end: str) -> list[noaa.Extremum]: + raise RuntimeError("NOAA down") + + monkeypatch.setattr(noaa, "fetch_extrema", down) + assert client.get("/api/tides/bootstrap").status_code == 502 + + +def test_fetch_extrema_serves_stale_on_error(monkeypatch: pytest.MonkeyPatch) -> None: + import asyncio + + from app.config import Settings + + noaa.clear_cache() + calls = {"n": 0} + good = [noaa.Extremum(t=datetime(2026, 5, 27, 1, 0), height=8.0, kind="H")] + + def fake_blocking(station_id: str, begin: str, end: str) -> list[noaa.Extremum]: + calls["n"] += 1 + if calls["n"] == 1: + return good + raise RuntimeError("NOAA down") + + monkeypatch.setattr(noaa, "_fetch_blocking", fake_blocking) + # cache_ttl=0 forces a refetch on the 2nd call + monkeypatch.setattr(noaa, "get_settings", lambda: Settings(cache_ttl=0)) + + async def run() -> tuple[list[noaa.Extremum], list[noaa.Extremum]]: + first = await noaa.fetch_extrema("9449211", "20260501", "20260601") + second = await noaa.fetch_extrema("9449211", "20260501", "20260601") + return first, second + + first, second = asyncio.run(run()) + assert first == good + assert second == good # stale-if-error: last-known-good despite the failure + noaa.clear_cache() diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py index 10e5c2f..ccbe22f 100644 --- a/backend/tests/test_health.py +++ b/backend/tests/test_health.py @@ -2,12 +2,12 @@ def test_health_returns_ok(client: TestClient) -> None: - res = client.get("/api/health") + res = client.get("/api/tides/health") assert res.status_code == 200 assert res.json() == {"status": "ok"} def test_cors_headers_present_for_allowed_origin(client: TestClient) -> None: - res = client.get("/api/health", headers={"Origin": "http://localhost:5173"}) + res = client.get("/api/tides/health", headers={"Origin": "http://localhost:5173"}) assert res.status_code == 200 assert res.headers["access-control-allow-origin"] == "http://localhost:5173" diff --git a/backend/tests/test_static.py b/backend/tests/test_static.py index dd138a2..14c4411 100644 --- a/backend/tests/test_static.py +++ b/backend/tests/test_static.py @@ -2,26 +2,40 @@ def test_api_health_still_works_with_static(static_client: TestClient) -> None: - res = static_client.get("/api/health") + res = static_client.get("/api/tides/health") assert res.status_code == 200 assert res.json() == {"status": "ok"} -def test_root_serves_index_html(static_client: TestClient) -> None: - res = static_client.get("/") +def test_root_redirects_to_tides(static_client: TestClient) -> None: + res = static_client.get("/", follow_redirects=False) + assert res.status_code == 307 + assert res.headers["location"] == "/tides" + + +def test_tides_serves_index_html(static_client: TestClient) -> None: + res = static_client.get("/tides") assert res.status_code == 200 assert "BTO" in res.text + assert res.headers["cache-control"] == "no-cache" -def test_unknown_route_falls_back_to_index(static_client: TestClient) -> None: - # SPA client-side routing: any non-API path returns index.html. - res = static_client.get("/tides") +def test_unknown_route_under_tides_falls_back_to_index( + static_client: TestClient, +) -> None: + # SPA client-side routing: any path under /tides returns index.html. + res = static_client.get("/tides/some/view") assert res.status_code == 200 assert "BTO" in res.text +def test_paths_outside_tides_are_not_ours(static_client: TestClient) -> None: + # The main site owns everything else on the domain. + assert static_client.get("/blog").status_code == 404 + + def test_assets_are_served(static_client: TestClient) -> None: - res = static_client.get("/assets/app.js") + res = static_client.get("/tides/assets/app.js") assert res.status_code == 200 assert "bto" in res.text @@ -30,3 +44,14 @@ def test_no_static_dir_has_no_spa_fallback(client: TestClient) -> None: # Without a static dir there is no catch-all; unknown routes 404. res = client.get("/tides") assert res.status_code == 404 + + +def test_public_files_are_served(static_client: TestClient) -> None: + res = static_client.get("/tides/favicon.svg") + assert res.status_code == 200 + assert res.text == "" + + +def test_path_traversal_gets_index_not_file(static_client: TestClient) -> None: + res = static_client.get("/tides/..%2F..%2Fetc%2Fpasswd") + assert "BTO" in res.text diff --git a/backend/uv.lock b/backend/uv.lock index 9fa3d74..6b5c379 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -22,14 +22,14 @@ wheels = [ [[package]] name = "anyio" -version = "4.13.0" +version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] @@ -58,11 +58,15 @@ name = "bto" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "anyio" }, { name = "astral" }, { name = "fastapi" }, { name = "noaa-coops" }, { name = "pydantic-settings" }, + { name = "starlette" }, + { name = "urllib3" }, { name = "uvicorn", extra = ["standard"] }, + { name = "zeep" }, ] [package.dev-dependencies] @@ -74,11 +78,15 @@ dev = [ [package.metadata] requires-dist = [ + { name = "anyio", specifier = ">=4.14.2" }, { name = "astral", specifier = ">=3.2" }, { name = "fastapi", specifier = ">=0.136.3" }, { name = "noaa-coops", specifier = ">=0.4.0" }, - { name = "pydantic-settings", specifier = ">=2.14.1" }, + { name = "pydantic-settings", specifier = ">=2.14.2" }, + { name = "starlette", specifier = ">=1.3.1" }, + { name = "urllib3", specifier = ">=2.8.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.48.0" }, + { name = "zeep", specifier = ">=4.3.3" }, ] [package.metadata.requires-dev] @@ -550,16 +558,17 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.1" +version = "2.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, + { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/3b/a5d2294799b53b448319978cfb5bd139d5a9d45e862af91661614f14c922/pydantic_settings-2.16.0.tar.gz", hash = "sha256:5b6c578049ede4db0e2ef3b4eaa4ad4069cfa9211f83fb38df899dfade50a614", size = 301031, upload-time = "2026-10-14T12:44:09.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, + { url = "https://files.pythonhosted.org/packages/53/f4/b987bf8c51e5b19a95fa66d1ee596074141e085d9c2ddf97920803c7029b/pydantic_settings-2.16.0-py3-none-any.whl", hash = "sha256:7e73acf7f61936a15e5a3b6eedaea29f133357faf7272f2607ba479b049dd7f2", size = 80364, upload-time = "2026-10-14T12:44:08.233Z" }, ] [[package]] @@ -728,14 +737,14 @@ wheels = [ [[package]] name = "starlette" -version = "1.1.0" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/66/4d20cdf39a8d6a51e663b7038e3b828ff211d3891a43a713fe7e4643f3a8/starlette-1.1.0.tar.gz", hash = "sha256:e83c7fe0ddecd8719c5b840080325aec0260acec86e9832899e377b91d65e90f", size = 2660060, upload-time = "2026-05-23T16:55:41.376Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/0c/6efb252d091ecccd7d62048ae11f0ea35cd75a4fbaeea5e30f9c3bf91d10/starlette-1.8.0.tar.gz", hash = "sha256:1565dc0b35d5737a271ed1e0e04e949f4e81198799f216d2667b0a0fb9cf9522", size = 2730457, upload-time = "2026-10-13T07:54:39.53Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/79/920b8e0a8b20f793e8d64855095cb8febabf6175b8550b6f7a547d813891/starlette-1.1.0-py3-none-any.whl", hash = "sha256:7f0dfd38e428aad5cb6f9f667f0ca1d2d8ca3f3385dccac8305f79ec98458382", size = 72899, upload-time = "2026-05-23T16:55:39.201Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b0/5742e4ac7af5eb58ec3470a537a49d7aa507e5539413e504b3a65ef50ba8/starlette-1.8.0-py3-none-any.whl", hash = "sha256:dfdd6b29c26483288088d990eee59631dedadd66ce20d203402a7ca8e3c4656f", size = 79612, upload-time = "2026-10-13T07:54:38.019Z" }, ] [[package]] @@ -770,11 +779,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]] @@ -937,19 +946,18 @@ wheels = [ [[package]] name = "zeep" -version = "4.3.2" +version = "4.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "isodate" }, { name = "lxml" }, { name = "platformdirs" }, - { name = "pytz" }, { name = "requests" }, { name = "requests-file" }, { name = "requests-toolbelt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/06/4f1d3ff61e930163565fb73616c6251e412a4d2fc7ed18214e1c2107258d/zeep-4.3.2.tar.gz", hash = "sha256:1a23a667ce9d73a0dbfdf15745bfa2b7ab0b6402135c0cd5067574838398e0e6", size = 166687, upload-time = "2025-09-15T10:26:03.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/c2/e06e5f177d818d0fc34fcbe2f98c175af2cba63b7d90f4bfcb6fe360d343/zeep-4.3.3.tar.gz", hash = "sha256:99d5059f92f721020998695fd9c85289ccb03ec5a2398ad49c6dbe43f19cfb94", size = 167372, upload-time = "2026-06-18T17:17:58.994Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/78/f43f3feb70d67cbe260ec5b682ecc3c1850c8f437f1df707495126e51817/zeep-4.3.2-py3-none-any.whl", hash = "sha256:ed08c3179709172bfaaa9b76a6a545f8a57043ec6218e64e9deb81ff1e0ff79b", size = 101853, upload-time = "2025-09-15T10:26:02.12Z" }, + { url = "https://files.pythonhosted.org/packages/b5/87/5c1d29f52fc32da98b75dc8a681cd0de78c9cfd5c10493cd7fb730e1d4b0/zeep-4.3.3-py3-none-any.whl", hash = "sha256:5adfae35582848819f8bb97b6ea9f1a34a2d4851a539f830e14dbab09961dd18", size = 102054, upload-time = "2026-06-18T17:17:57.223Z" }, ] diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev index 846c763..7807de8 100644 --- a/frontend/Dockerfile.dev +++ b/frontend/Dockerfile.dev @@ -3,15 +3,17 @@ FROM node:22-slim WORKDIR /app -# Patch base-image OS packages and the bundled npm (clears fixable CVEs the -# scan flags in node:22-slim, including npm's vendored picomatch). +# Patch base-image OS packages (clears fixable CVEs the scan flags in node:22-slim). RUN apt-get update \ && apt-get upgrade -y \ - && rm -rf /var/lib/apt/lists/* \ - && npm install -g npm@latest + && rm -rf /var/lib/apt/lists/* COPY package.json package-lock.json ./ -RUN npm ci +# npm is only needed to install. It vendors its own copies of undici and +# brace-expansion that lag behind security fixes, so remove it afterwards and +# run Vite with node directly. +RUN npm ci \ + && rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx EXPOSE 5173 -CMD ["npm", "run", "dev", "--", "--host"] +CMD ["node", "node_modules/vite/bin/vite.js", "--host"] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 17c7205..75eb6b4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -145,14 +145,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -251,27 +251,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.8" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -281,9 +281,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", "dev": true, "license": "MIT", "engines": { @@ -306,18 +306,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", + "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", + "@babel/parser": "^7.29.9", "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -325,9 +325,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { @@ -339,22 +339,46 @@ } }, "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.3.tgz", + "integrity": "sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==", "dev": true, "license": "MIT", "dependencies": { - "css-tree": "^3.0.0" + "css-tree": "^3.2.1" }, "bin": { "specificity": "bin/cli.js" } }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", + "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", "dev": true, "funding": [ { @@ -372,9 +396,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", + "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", "dev": true, "funding": [ { @@ -391,14 +415,14 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", - "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz", + "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==", "dev": true, "funding": [ { @@ -412,21 +436,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.1" + "@csstools/color-helpers": "^6.1.2", + "@csstools/css-calc": "^3.4.3" }, "engines": { "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", + "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", "dev": true, "funding": [ { @@ -443,13 +467,13 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", - "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", + "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", "dev": true, "funding": [ { @@ -472,9 +496,9 @@ } }, "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", + "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", "dev": true, "funding": [ { @@ -491,44 +515,10 @@ "node": ">=20.19.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", "dependencies": { @@ -583,9 +573,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -640,9 +630,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -654,9 +644,9 @@ } }, "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", + "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", "dev": true, "license": "MIT", "engines": { @@ -770,9 +760,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -787,39 +777,61 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "hashery": "^1.4.0", + "hookified": "^1.15.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "engines": { + "node": ">= 18" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "keyv": "^5.6.0" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@oxc-project/types": { - "version": "0.132.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", - "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", "dev": true, "license": "MIT", "funding": { - "url": "https://github.com/sponsors/Boshen" + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", - "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", "cpu": [ "arm64" ], @@ -834,9 +846,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", "cpu": [ "arm64" ], @@ -851,9 +863,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", - "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", "cpu": [ "x64" ], @@ -868,9 +880,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", - "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", "cpu": [ "x64" ], @@ -885,9 +897,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", - "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", "cpu": [ "arm" ], @@ -902,9 +914,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", - "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", "cpu": [ "arm64" ], @@ -922,9 +934,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", - "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", "cpu": [ "arm64" ], @@ -942,9 +954,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", - "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", "cpu": [ "ppc64" ], @@ -962,9 +974,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", - "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", "cpu": [ "s390x" ], @@ -982,9 +994,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", - "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", "cpu": [ "x64" ], @@ -1002,9 +1014,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", - "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", "cpu": [ "x64" ], @@ -1022,9 +1034,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", - "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", "cpu": [ "arm64" ], @@ -1038,29 +1050,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", - "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", - "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", "cpu": [ "arm64" ], @@ -1075,9 +1068,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", - "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", "cpu": [ "x64" ], @@ -1106,9 +1099,9 @@ "license": "MIT" }, "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", "dev": true, "license": "MIT", "peer": true, @@ -1154,9 +1147,9 @@ "license": "MIT" }, "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", "dev": true, "license": "MIT", "dependencies": { @@ -1181,17 +1174,6 @@ } } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -1240,19 +1222,19 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.12.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", - "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/react": { - "version": "19.2.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", - "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "dev": true, "license": "MIT", "dependencies": { @@ -1260,27 +1242,27 @@ } }, "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.2.0" + "@types/react": "^19.3.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz", - "integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", + "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.60.0", - "@typescript-eslint/type-utils": "8.60.0", - "@typescript-eslint/utils": "8.60.0", - "@typescript-eslint/visitor-keys": "8.60.0", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/type-utils": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1293,15 +1275,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.60.0", + "@typescript-eslint/parser": "^8.71.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", "dev": true, "license": "MIT", "engines": { @@ -1309,16 +1291,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz", - "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz", + "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.60.0", - "@typescript-eslint/types": "8.60.0", - "@typescript-eslint/typescript-estree": "8.60.0", - "@typescript-eslint/visitor-keys": "8.60.0", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", "debug": "^4.4.3" }, "engines": { @@ -1334,14 +1316,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz", - "integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz", + "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.0", - "@typescript-eslint/types": "^8.60.0", + "@typescript-eslint/tsconfig-utils": "^8.71.2", + "@typescript-eslint/types": "^8.71.2", "debug": "^4.4.3" }, "engines": { @@ -1356,14 +1338,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz", - "integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz", + "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.0", - "@typescript-eslint/visitor-keys": "8.60.0" + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1374,9 +1356,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz", - "integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz", + "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==", "dev": true, "license": "MIT", "engines": { @@ -1391,15 +1373,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz", - "integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz", + "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.0", - "@typescript-eslint/typescript-estree": "8.60.0", - "@typescript-eslint/utils": "8.60.0", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1416,9 +1398,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz", - "integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz", + "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==", "dev": true, "license": "MIT", "engines": { @@ -1430,16 +1412,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz", - "integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz", + "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.60.0", - "@typescript-eslint/tsconfig-utils": "8.60.0", - "@typescript-eslint/types": "8.60.0", - "@typescript-eslint/visitor-keys": "8.60.0", + "@typescript-eslint/project-service": "8.71.2", + "@typescript-eslint/tsconfig-utils": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1458,9 +1440,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "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": { @@ -1471,16 +1453,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz", - "integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz", + "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.60.0", - "@typescript-eslint/types": "8.60.0", - "@typescript-eslint/typescript-estree": "8.60.0" + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1495,13 +1477,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz", - "integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz", + "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/types": "8.71.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1513,13 +1495,13 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", - "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "^1.0.0" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1527,6 +1509,7 @@ "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.152.0", "vite": "^8.0.0" }, "peerDependenciesMeta": { @@ -1535,20 +1518,23 @@ }, "babel-plugin-react-compiler": { "optional": true + }, + "oxc-transform-react": { + "optional": true } } }, "node_modules/@vitest/expect": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", - "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.7", - "@vitest/utils": "4.1.7", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -1557,13 +1543,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", - "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.7", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1584,9 +1570,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", - "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -1597,13 +1583,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", - "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.7", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -1611,14 +1597,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", - "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.7", - "@vitest/utils": "4.1.7", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1627,9 +1613,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", - "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -1637,13 +1623,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", - "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.7", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1652,9 +1638,9 @@ } }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "dev": true, "license": "MIT", "bin": { @@ -1747,9 +1733,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.32", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", - "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "version": "2.11.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz", + "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1760,9 +1746,9 @@ } }, "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", "dev": true, "license": "MIT", "dependencies": { @@ -1770,22 +1756,22 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "dev": true, "funding": [ { @@ -1803,11 +1789,11 @@ ], "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" + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -1816,10 +1802,24 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "version": "1.0.30001816", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001816.tgz", + "integrity": "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==", "dev": true, "funding": [ { @@ -1838,9 +1838,9 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", "engines": { @@ -1972,16 +1972,16 @@ "peer": true }, "node_modules/electron-to-chromium": { - "version": "1.5.362", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.362.tgz", - "integrity": "sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w==", + "version": "1.5.456", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.456.tgz", + "integrity": "sha512-8hboqI6xesozteFb8aa7OGknvxUtTeRJLz0FoePysUEh/oWbddykIGdgpEJRbPRg/nIMfBf2DFos+H2MtvJl2A==", "dev": true, "license": "ISC" }, "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -1992,9 +1992,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -2022,18 +2022,21 @@ } }, "node_modules/eslint": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", - "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz", + "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", + "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.1", + "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2048,14 +2051,14 @@ "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", + "file-entry-cache": "11.1.5 || >11.1.6 <12", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2098,9 +2101,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", - "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.7.tgz", + "integrity": "sha512-XhJSzLljuYD4UjNuFGJu2v7aD3sqNVK12w5/DCUfrNfHWegZo2+TsavNx8MuxMwEquuFAzNbNfXKU0WoZ+YUIg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2214,9 +2217,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2263,16 +2266,13 @@ } }, "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" + "flat-cache": "^6.1.23" } }, "node_modules/find-up": { @@ -2293,23 +2293,21 @@ } }, "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -2352,9 +2350,9 @@ } }, "node_modules/globals": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", - "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", "dev": true, "license": "MIT", "engines": { @@ -2364,6 +2362,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -2381,6 +2392,13 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -2510,9 +2528,9 @@ } }, "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -2532,13 +2550,6 @@ "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": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2567,13 +2578,13 @@ } }, "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==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.1.1" } }, "node_modules/levn": { @@ -2591,9 +2602,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -2607,23 +2618,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -2642,9 +2653,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -2663,9 +2674,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -2684,9 +2695,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -2705,9 +2716,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -2726,9 +2737,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], @@ -2750,9 +2761,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], @@ -2774,9 +2785,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], @@ -2798,9 +2809,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], @@ -2822,9 +2833,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -2843,9 +2854,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -2928,13 +2939,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -2951,9 +2962,9 @@ "license": "MIT" }, "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==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -2977,9 +2988,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.46", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", - "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "version": "2.0.58", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz", + "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==", "dev": true, "license": "MIT", "engines": { @@ -2987,15 +2998,18 @@ } }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/optionator": { "version": "0.9.4", @@ -3095,9 +3109,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -3108,9 +3122,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", "dev": true, "funding": [ { @@ -3128,9 +3142,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -3172,25 +3186,45 @@ "node": ">=6" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "scheduler": "^0.28.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.3.1" } }, "node_modules/react-is": { @@ -3226,13 +3260,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", - "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.132.0", + "@oxc-project/types": "=0.154.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3242,21 +3276,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.2", - "@rolldown/binding-darwin-arm64": "1.0.2", - "@rolldown/binding-darwin-x64": "1.0.2", - "@rolldown/binding-freebsd-x64": "1.0.2", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", - "@rolldown/binding-linux-arm64-gnu": "1.0.2", - "@rolldown/binding-linux-arm64-musl": "1.0.2", - "@rolldown/binding-linux-ppc64-gnu": "1.0.2", - "@rolldown/binding-linux-s390x-gnu": "1.0.2", - "@rolldown/binding-linux-x64-gnu": "1.0.2", - "@rolldown/binding-linux-x64-musl": "1.0.2", - "@rolldown/binding-openharmony-arm64": "1.0.2", - "@rolldown/binding-wasm32-wasi": "1.0.2", - "@rolldown/binding-win32-arm64-msvc": "1.0.2", - "@rolldown/binding-win32-x64-msvc": "1.0.2" + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" } }, "node_modules/saxes": { @@ -3273,9 +3307,9 @@ } }, "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", "license": "MIT" }, "node_modules/semver": { @@ -3319,9 +3353,9 @@ "license": "ISC" }, "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==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -3336,9 +3370,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "dev": true, "license": "MIT" }, @@ -3370,9 +3404,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz", - "integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "dev": true, "license": "MIT", "engines": { @@ -3380,9 +3414,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -3397,9 +3431,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", "dev": true, "license": "MIT", "engines": { @@ -3407,29 +3441,29 @@ } }, "node_modules/tldts": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.0.tgz", - "integrity": "sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==", + "version": "7.4.18", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.18.tgz", + "integrity": "sha512-somxA8xKAUzlAxl0TX/jMSYIbRNFIVGnI96pVGJPeMc5BsHmfVXn6mOJT6U++ELnl04eGfzpfcEuZe8WiASkXg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.0" + "tldts-core": "^7.4.18" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.0.tgz", - "integrity": "sha512-/mb9kRld+x1sIMXxWNOAp5m6C+D4GrAORWlJkOJ5dElvxdN1eutz/o7qHLp9gFvDF4Y3/L2xeScoxz6AbEo8rQ==", + "version": "7.4.18", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.18.tgz", + "integrity": "sha512-KI2PKvCmvQI/xD6ypPqkU0i7oxNXR/O58RLRaHR4svsgVUQ2JE/XC8BtMbG6PSehD27I8sZJRcC1ASnCy6lp3g==", "dev": true, "license": "MIT" }, "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3465,14 +3499,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3501,16 +3527,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.60.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.0.tgz", - "integrity": "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==", + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz", + "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.60.0", - "@typescript-eslint/parser": "8.60.0", - "@typescript-eslint/typescript-estree": "8.60.0", - "@typescript-eslint/utils": "8.60.0" + "@typescript-eslint/eslint-plugin": "8.71.2", + "@typescript-eslint/parser": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3525,9 +3551,9 @@ } }, "node_modules/undici": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz", - "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==", + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", "dev": true, "license": "MIT", "engines": { @@ -3535,16 +3561,16 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "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==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", "dev": true, "funding": [ { @@ -3583,17 +3609,17 @@ } }, "node_modules/vite": { - "version": "8.0.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", - "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.2", - "tinyglobby": "^0.2.16" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -3609,7 +3635,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -3661,19 +3687,19 @@ } }, "node_modules/vitest": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", - "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.7", - "@vitest/mocker": "4.1.7", - "@vitest/pretty-format": "4.1.7", - "@vitest/runner": "4.1.7", - "@vitest/snapshot": "4.1.7", - "@vitest/spy": "4.1.7", - "@vitest/utils": "4.1.7", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -3701,12 +3727,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.7", - "@vitest/browser-preview": "4.1.7", - "@vitest/browser-webdriverio": "4.1.7", - "@vitest/coverage-istanbul": "4.1.7", - "@vitest/coverage-v8": "4.1.7", - "@vitest/ui": "4.1.7", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -3879,9 +3905,9 @@ } }, "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "dev": true, "license": "MIT", "funding": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4a80059..80572af 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,28 +1,68 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { fetchBootstrap } from './api' import type { Bootstrap } from './types' import { Dashboard } from './Dashboard' +import { MobileApp } from './mobile/MobileApp' + +// Below this width the desktop Ganglia grid is unusable; render the phone app. +const MOBILE_QUERY = '(max-width: 768px)' + +function useIsMobile(): boolean { + const [isMobile, setIsMobile] = useState(() => window.matchMedia(MOBILE_QUERY).matches) + useEffect(() => { + const mq = window.matchMedia(MOBILE_QUERY) + const onChange = (e: MediaQueryListEvent) => setIsMobile(e.matches) + mq.addEventListener('change', onChange) + return () => mq.removeEventListener('change', onChange) + }, []) + return isMobile +} + +// NOAA's API is occasionally flaky; auto-retry a few times before giving up. +const MAX_AUTO_RETRIES = 3 export default function App() { const [data, setData] = useState(null) const [error, setError] = useState(null) + const isMobile = useIsMobile() + const attempt = useRef(0) + + const load = useCallback((signal?: AbortSignal) => { + const run = () => { + setError(null) + fetchBootstrap(signal) + .then((d) => { + attempt.current = 0 + setData(d) + }) + .catch((e) => { + if (e.name === 'AbortError') return + if (attempt.current < MAX_AUTO_RETRIES) { + attempt.current += 1 + setTimeout(run, 1200 * attempt.current) + } else { + setError(String(e.message ?? e)) + } + }) + } + run() + }, []) useEffect(() => { const ctrl = new AbortController() - fetchBootstrap(ctrl.signal) - .then(setData) - .catch((e) => { - if (e.name !== 'AbortError') setError(String(e.message ?? e)) - }) + load(ctrl.signal) return () => ctrl.abort() - }, []) + }, [load]) if (error) { return (
◆
Bellingham Tidal Observatory
-
Failed to load NOAA data — {error}
+
Couldn’t reach NOAA — {error}
+
) } @@ -37,5 +77,5 @@ export default function App() { ) } - return + return isMobile ? : } diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 30b45e8..0a0b67a 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -16,5 +16,5 @@ async function json(res: Response): Promise { /** Load station metadata + real NOAA hi/lo predictions + sun/moon in one call. */ export async function fetchBootstrap(signal?: AbortSignal): Promise { - return json(await fetch('/api/bootstrap', { signal })) + return json(await fetch('/api/tides/bootstrap', { signal })) } diff --git a/frontend/src/components/Charts.tsx b/frontend/src/components/Charts.tsx index f5c6a60..350a094 100644 --- a/frontend/src/components/Charts.tsx +++ b/frontend/src/components/Charts.tsx @@ -140,6 +140,9 @@ export function BigChart({ const xOf = (t: number) => padL + ((t - from) / (to - from)) * innerW const yOf = (v: number) => padT + (1 - (v - lo) / spanV) * innerH const yTicks = niceTicks(lo, hi, 5) + // Sub-foot steps need a decimal, else labels collapse to duplicates (2,2,1,1). + const yStep = yTicks.length > 1 ? Math.abs(yTicks[1] - yTicks[0]) : 1 + const yPrec = yStep < 1 ? 1 : 0 const meanV = (lo + hi) / 2 const colorFor = (i: number) => { @@ -218,21 +221,31 @@ export function BigChart({ ) })} - {showEvents && - single && - eventsIn(series[0].extrema, from, to).map((e, i) => { + {/* Annotate hi/lo only when there are few enough to stay legible. Beyond + that (multi-day ranges) the labels overlap into noise, so just dot the + peaks; past an even higher count, show nothing and let the curve speak. */} + {(() => { + if (!showEvents || !single) return null + const events = eventsIn(series[0].extrema, from, to) + const labeled = events.length <= 14 + const dotted = events.length <= 60 + if (!dotted) return null + return events.map((e, i) => { const x = xOf(e.t) const y = yOf(e.height) const yOff = e.kind === 'H' ? -8 : 14 return ( - - {e.kind === 'H' ? '▲' : '▼'} {e.height.toFixed(1)}ft · {fmtTime(e.t)} - + {labeled && ( + + {e.kind === 'H' ? '▲' : '▼'} {e.height.toFixed(1)}ft · {fmtTime(e.t)} + + )} ) - })} + }) + })()} {showCurrent && single && now >= from && now <= to && ( @@ -247,7 +260,7 @@ export function BigChart({ {yTicks.map((v, i) => ( - {v.toFixed(0)}ft + {v.toFixed(yPrec)}ft ))} {xTicks.map((t, i) => { diff --git a/frontend/src/components/FocusPanel.tsx b/frontend/src/components/FocusPanel.tsx index 2a15efb..81f31d2 100644 --- a/frontend/src/components/FocusPanel.tsx +++ b/frontend/src/components/FocusPanel.tsx @@ -1,7 +1,8 @@ -import { useMemo, useRef, useState } from 'react' +import { useMemo } from 'react' import type { Bootstrap, StationData, SunMoon } from '../types' import { fmtTime, heightAt, RANGES, rateAt, stationSnapshot } from '../tides' -import { BigChart, type SunMarks, TideDial } from './Charts' +import { type SunMarks, TideDial } from './Charts' +import { InteractiveChart } from './InteractiveChart' const MIN = 60 * 1000 @@ -145,20 +146,6 @@ export function FocusPanel({ const to = now + mins * 0.6 * MIN const trendGlyph = snap.trend === 'rising' ? '↗' : snap.trend === 'falling' ? '↘' : '→' - const [hoverT, setHoverT] = useState(null) - const chartRef = useRef(null) - const onMove = (e: React.MouseEvent) => { - const r = chartRef.current!.getBoundingClientRect() - const x = e.clientX - r.left - const ratio = (x - 38) / (r.width - 38 - 14) - if (ratio < 0 || ratio > 1) { - setHoverT(null) - return - } - setHoverT(from + ratio * (to - from)) - } - const hoverH = hoverT != null ? heightAt(stn.extrema, hoverT) : null - return (
@@ -170,7 +157,7 @@ export function FocusPanel({
- +
{snap.height.toFixed(2)} @@ -188,17 +175,8 @@ export function FocusPanel({
-
setHoverT(null)}> - - {hoverT != null && hoverH != null && ( -
- {fmtTime(hoverT)} - - {hoverH.toFixed(2)} - ft - -
- )} +
+
diff --git a/frontend/src/components/InteractiveChart.tsx b/frontend/src/components/InteractiveChart.tsx new file mode 100644 index 0000000..668ac5d --- /dev/null +++ b/frontend/src/components/InteractiveChart.tsx @@ -0,0 +1,183 @@ +// Interactive wrapper around BigChart: hover/drag for a point-in-time readout +// (like NOAA's tide page) and click-drag to zoom into a time window. Works with +// mouse and touch. Measures its own width so pointer-x maps 1:1 to chart time. +import { useLayoutEffect, useMemo, useRef, useState } from 'react' +import type { Extremum } from '../types' +import { fmtTime, heightAt } from '../tides' +import { BigChart, type SunMarks } from './Charts' + +const PAD_L = 38 +const PAD_R = 14 +const MIN_SPAN = 10 * 60 * 1000 // don't zoom tighter than 10 minutes + +export function InteractiveChart({ + series, + from, + to, + now, + resetKey, + height = 280, + showGrid = true, + showSun = true, + sun, + style = 'area', + showEvents = true, +}: { + series: { extrema: Extremum[]; code?: string }[] + from: number + to: number + now: number + // Changing this clears any zoom (e.g. the selected time range). Without it the + // live clock would constantly change `from`/`to` and wipe the user's zoom. + resetKey: string + height?: number + showGrid?: boolean + showSun?: boolean + sun?: SunMarks + style?: 'line' | 'area' + showEvents?: boolean +}) { + const ref = useRef(null) + // null until measured — we don't render the chart at a guessed width (which + // would briefly render tiny/letterboxed before correcting). + const [width, setWidth] = useState(null) + // null = follow the live [from, to] window; otherwise a pinned absolute window. + // The zoom remembers which range it was made in, so changing the range (but + // not every clock tick) drops it. + const [pinned, setPinned] = useState<{ from: number; to: number; key: string } | null>(null) + const zoom = pinned && pinned.key === resetKey ? pinned : null + const setZoom = (z: { from: number; to: number } | null) => + setPinned(z && { ...z, key: resetKey }) + const [cursorPx, setCursorPx] = useState(null) + // Refs for the pointer handlers; state mirrors for rendering the drag band. + const dragStart = useRef(null) + const dragNowRef = useRef(null) + const [dragStartPx, setDragStartPx] = useState(null) + const [dragNow, setDragNow] = useState(null) + + useLayoutEffect(() => { + const el = ref.current + if (!el) return + const measure = () => setWidth(el.clientWidth) + measure() + const ro = new ResizeObserver(measure) + ro.observe(el) + return () => ro.disconnect() + }, []) + + const winFrom = zoom ? zoom.from : from + const winTo = zoom ? zoom.to : to + const plotW = Math.max(1, (width ?? 600) - PAD_L - PAD_R) + const single = series.length === 1 + const zoomed = zoom != null + + const tOfX = (px: number) => winFrom + ((px - PAD_L) / plotW) * (winTo - winFrom) + + const pxFromEvent = (clientX: number) => { + const rect = ref.current!.getBoundingClientRect() + return clientX - rect.left + } + + const onPointerDown = (e: React.PointerEvent) => { + ;(e.target as Element).setPointerCapture?.(e.pointerId) + const px = pxFromEvent(e.clientX) + dragStart.current = px + dragNowRef.current = px + setDragStartPx(px) + setDragNow(px) + setCursorPx(px) + } + const onPointerMove = (e: React.PointerEvent) => { + const px = pxFromEvent(e.clientX) + setCursorPx(px) + if (dragStart.current != null) { + dragNowRef.current = px + setDragNow(px) + } + } + const endDrag = () => { + if (dragStart.current != null && dragNowRef.current != null) { + const a = Math.min(dragStart.current, dragNowRef.current) + const b = Math.max(dragStart.current, dragNowRef.current) + if (b - a > 6) { + const nf = Math.max(winFrom, tOfX(a)) + let nt = Math.min(winTo, tOfX(b)) + if (nt - nf < MIN_SPAN) nt = nf + MIN_SPAN + if (nt - nf >= MIN_SPAN) setZoom({ from: nf, to: nt }) + } + } + dragStart.current = null + dragNowRef.current = null + setDragStartPx(null) + setDragNow(null) + } + const onLeave = () => { + if (dragStart.current == null) setCursorPx(null) + } + + const cursorT = cursorPx != null ? tOfX(cursorPx) : null + const cursorClamped = cursorT != null ? Math.max(winFrom, Math.min(winTo, cursorT)) : null + const cursorH = cursorClamped != null && single ? heightAt(series[0].extrema, cursorClamped) : null + + const dragging = dragStartPx != null && dragNow != null + const bandLeft = dragging ? Math.min(dragStartPx, dragNow) : 0 + const bandWidth = dragging ? Math.abs(dragNow - dragStartPx) : 0 + + // Readout box position, kept inside the plot. + const readoutLeft = useMemo(() => { + if (cursorPx == null) return 0 + return Math.max(PAD_L, Math.min((width ?? 0) - 96, cursorPx + 8)) + }, [cursorPx, width]) + + return ( +
+
+ {width != null && ( + + )} + {dragging && bandWidth > 1 && ( +
+ )} + {cursorClamped != null && cursorH != null && ( +
+ {fmtTime(cursorClamped)} + + {cursorH.toFixed(2)} + ft + +
+ )} +
+
+ {zoomed ? 'zoomed in' : 'drag across to zoom · hover to read'} + {zoomed && ( + + )} +
+
+ ) +} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 23f344d..e2d12b1 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -2,11 +2,11 @@ import type { StationData } from '../types' import { stationSnapshot } from '../tides' const NAV = [ - { icon: '~', label: 'Home' }, - { icon: '/', label: 'Blog' }, - { icon: '%', label: 'Digests' }, - { icon: '#', label: 'Projects' }, - { icon: '@', label: 'About' }, + { icon: '~', label: 'Home', href: '/' }, + { icon: '/', label: 'Blog', href: '/blog' }, + { icon: '%', label: 'Digests', href: '/digest' }, + { icon: '#', label: 'Projects', href: '/projects' }, + { icon: '@', label: 'About', href: '/about' }, ] export interface SavedView { @@ -42,7 +42,7 @@ export function Sidebar({
PAGES
{NAV.map((n) => ( - + {n.icon} {n.label} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index fcbf291..3815017 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -4,6 +4,7 @@ import './styles/tokens.css' import './styles/glacier.css' import './styles/tides.css' import './styles/tweaks.css' +import './styles/mobile.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/mobile/MobileApp.tsx b/frontend/src/mobile/MobileApp.tsx new file mode 100644 index 0000000..8fe7892 --- /dev/null +++ b/frontend/src/mobile/MobileApp.tsx @@ -0,0 +1,575 @@ +// Phone-optimized tides observatory — ported from the design's tides-mobile.jsx, +// driven by real NOAA data. Single-column list, hero card, drilldown, filters +// sheet, saved views, and a bottom tab bar. Rendered below the desktop breakpoint. +import { useEffect, useMemo, useState } from 'react' +import type { Bootstrap, StationData } from '../types' +import { + RANGE_KEYS, + fmtClock, + fmtTime, + heightAt, + pacificWallMs, + rateAt, + RANGES, + stationSnapshot, +} from '../tides' +import { Spark, TideDial, type SunMarks } from '../components/Charts' +import { InteractiveChart } from '../components/InteractiveChart' + +const MIN = 60 * 1000 + +type View = 'stations' | 'drill' | 'filters' | 'saved' +type SortBy = 'name' | 'height' | 'range' | 'trend' +type GroupBy = 'none' | 'region' | 'trend' + +const trendGlyph = (t: string) => (t === 'rising' ? '↗' : t === 'falling' ? '↘' : '→') + +// ── Header ──────────────────────────────────────────────────────────────── +function Header({ view, clock, onSearch, onSetting }: { view: View; clock: string; onSearch: () => void; onSetting: () => void }) { + const title = view === 'stations' ? 'Stations' : view === 'drill' ? 'Drilldown' : view === 'saved' ? 'Saved' : 'Filters' + return ( +
+
+
~/TIDES · BELLINGHAM
+
{title}
+
+
+ + + {clock} + + + +
+
+ ) +} + +// ── Range pills ───────────────────────────────────────────────────────────── +function RangePills({ value, onChange }: { value: string; onChange: (r: string) => void }) { + return ( +
+ RANGE +
+ {RANGE_KEYS.map((r) => ( + + ))} +
+
+ ) +} + +// ── Hero card ──────────────────────────────────────────────────────────────── +function Hero({ stn, now, range, onTap }: { stn: StationData; now: number; range: string; onTap: () => void }) { + const snap = useMemo(() => stationSnapshot(stn, now), [stn, now]) + const mins = RANGES[range].mins + const from = now - mins * 0.6 * MIN + const to = now + mins * 0.4 * MIN + return ( +
+
+
+
{stn.code} · NOW
+
{stn.name}
+
+ + + {snap.trend} + +
+
+ +
+
+ {snap.height.toFixed(1)} + ft +
+
+ {trendGlyph(snap.trend)} {Math.abs(snap.rate).toFixed(2)} + ft/h +
+
+ {stn.predictions.toLowerCase()} · range {(snap.dayMax - snap.dayMin).toFixed(1)}ft +
+
+
+
+ +
+ {fmtTime(from)} + now + {fmtTime(to)} +
+
+
+ {snap.nextHigh && ( +
+ ▲ +
+
next high
+
+ {snap.nextHigh.height.toFixed(1)} + ft · {fmtTime(snap.nextHigh.t)} +
+
+
+ )} + {snap.nextLow && ( +
+ ▼ +
+
next low
+
+ {snap.nextLow.height.toFixed(1)} + ft · {fmtTime(snap.nextLow.t)} +
+
+
+ )} +
+
+ ) +} + +// ── KPI chips ──────────────────────────────────────────────────────────────── +function Kpis({ data, now }: { data: Bootstrap; now: number }) { + const cells = useMemo(() => { + const snaps = data.stations.map((s) => stationSnapshot(s, now)) + const meanH = snaps.reduce((a, b) => a + b.height, 0) / snaps.length + const rising = snaps.filter((s) => s.trend === 'rising').length + const falling = snaps.filter((s) => s.trend === 'falling').length + const slack = snaps.filter((s) => s.trend === 'slack').length + const dayMin = Math.min(...snaps.map((s) => s.dayMin)) + const dayMax = Math.max(...snaps.map((s) => s.dayMax)) + const nextH = snaps.map((s) => s.nextHigh).filter((e): e is NonNullable => !!e).sort((a, b) => a.t - b.t)[0] + const nextL = snaps.map((s) => s.nextLow).filter((e): e is NonNullable => !!e).sort((a, b) => a.t - b.t)[0] + const sm = data.sun_moon + return [ + { k: 'MEAN', v: meanH.toFixed(1), u: 'ft', s: `${snaps.length} stns` }, + { k: 'RANGE', v: (dayMax - dayMin).toFixed(1), u: 'ft', s: `${dayMin.toFixed(0)}–${dayMax.toFixed(0)}` }, + { k: 'NEXT ▲', v: nextH ? fmtTime(nextH.t) : '—', u: '', s: nextH ? `${nextH.height.toFixed(1)}ft` : '' }, + { k: 'NEXT ▼', v: nextL ? fmtTime(nextL.t) : '—', u: '', s: nextL ? `${nextL.height.toFixed(1)}ft` : '' }, + { k: 'MOON', v: sm.moon_glyph, u: '', s: `${Math.round(sm.moon_illum * 100)}%`, glyph: true }, + { k: 'SUN', v: sm.sunrise, u: '↗', s: `set ${sm.sunset}` }, + { k: 'TREND', v: `${rising}↗`, u: '', s: `${falling}↘ ${slack}·` }, + ] + }, [data, now]) + return ( +
+ {cells.map((c, i) => ( +
+
{c.k}
+
+ {c.v} + {c.u && {c.u}} +
+
{c.s}
+
+ ))} +
+ ) +} + +// ── Station row ────────────────────────────────────────────────────────────── +function StationRow({ stn, range, now, isFocus, onTap }: { stn: StationData; range: string; now: number; isFocus: boolean; onTap: (id: string) => void }) { + const snap = useMemo(() => stationSnapshot(stn, now), [stn, now]) + const mins = RANGES[range].mins + const from = now - mins * 0.6 * MIN + const to = now + mins * 0.4 * MIN + return ( + + ) +} + +const REGION_ORDER = ['central', 'south', 'islands', 'north'] as const + +function sortStations(stations: StationData[], now: number, sortBy: SortBy): StationData[] { + const arr = stations.map((s) => [s, stationSnapshot(s, now)] as const) + arr.sort((a, b) => { + if (sortBy === 'name') return a[0].name.localeCompare(b[0].name) + if (sortBy === 'height') return b[1].height - a[1].height + if (sortBy === 'range') return b[1].dayMax - b[1].dayMin - (a[1].dayMax - a[1].dayMin) + const order = { rising: 0, slack: 1, falling: 2 } + return order[a[1].trend] - order[b[1].trend] + }) + return arr.map(([s]) => s) +} + +// ── Stations screen ───────────────────────────────────────────────────────── +function StationsScreen({ data, now, range, setRange, sortBy, groupBy, focusId, onTapStation }: { data: Bootstrap; now: number; range: string; setRange: (r: string) => void; sortBy: SortBy; groupBy: GroupBy; focusId: string; onTapStation: (id: string) => void }) { + const focusStn = data.stations.find((s) => s.id === focusId) ?? data.stations[0] + const minuteBucket = Math.floor(now / 60000) + const groups = useMemo(() => { + const sorted = sortStations(data.stations, now, sortBy) + if (groupBy === 'region') { + const map: Record = {} + for (const s of sorted) (map[s.group] ||= []).push(s) + return REGION_ORDER.map((k) => ({ label: k.toUpperCase(), items: map[k] || [] })).filter((g) => g.items.length) + } + if (groupBy === 'trend') { + const map: Record = { rising: [], slack: [], falling: [] } + for (const s of sorted) map[stationSnapshot(s, now).trend].push(s) + return (['rising', 'slack', 'falling'] as const).map((k) => ({ label: k.toUpperCase(), items: map[k] })).filter((g) => g.items.length) + } + return [{ label: null as string | null, items: sorted }] + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data.stations, sortBy, groupBy, minuteBucket]) + + return ( +
+ onTapStation(focusStn.id)} /> + +
+ KPI · ALL STATIONS + scroll → +
+ + {groups.map((g, gi) => ( +
+
+ {g.label ? g.label : `STATIONS · ${g.items.length}`} + tap to drill +
+
+ {g.items.map((s) => ( + + ))} +
+
+ ))} +
+ ) +} + +// ── Drill screen ───────────────────────────────────────────────────────────── +function DrillScreen({ data, stn, now, range, setRange, sun, showSun, showGrid, showMoon, compare, onBack }: { data: Bootstrap; stn: StationData; now: number; range: string; setRange: (r: string) => void; sun: SunMarks; showSun: boolean; showGrid: boolean; showMoon: boolean; compare: boolean; onBack: () => void }) { + const snap = useMemo(() => stationSnapshot(stn, now), [stn, now]) + const mins = RANGES[range].mins + const from = now - mins * 0.4 * MIN + const to = now + mins * 0.6 * MIN + const minuteBucket = Math.floor(now / 60000) + + const rows = useMemo(() => { + const out = [] + for (let i = 0; i < 8; i++) { + const t = now + i * 60 * MIN + out.push({ t, h: heightAt(stn.extrema, t), r: rateAt(stn.extrema, t), isNow: i === 0 }) + } + return out + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [stn, minuteBucket]) + const hi = Math.max(...rows.map((r) => r.h)) + const lo = Math.min(...rows.map((r) => r.h)) + const span = hi - lo || 1 + const sm = data.sun_moon + const series = compare ? data.stations : [stn] + + return ( +
+
+ + +
+ +
+
{stn.code}
+
{stn.name}
+
{stn.lat}, {stn.lon} · {stn.area}
+
+ +
+
+ {snap.height.toFixed(2)} + ft +
+
+ {trendGlyph(snap.trend)} {snap.trend} · {Math.abs(snap.rate).toFixed(2)}ft/h +
+
+ day range {(snap.dayMax - snap.dayMin).toFixed(1)}ft · {stn.predictions.toLowerCase()} +
+
+
+
+ + + +
+ +
+ +
+ UPCOMING · 8H + {fmtTime(now)} → +
+
+ {rows.map((r, i) => { + const pct = ((r.h - lo) / span) * 100 + const g = Math.abs(r.r) < 0.15 ? '—' : r.r > 0 ? '↗' : '↘' + return ( +
+ {r.isNow ? 'now' : fmtTime(r.t)} + + + + + {r.h.toFixed(2)} + ft + + + {g} {Math.abs(r.r).toFixed(2)} + +
+ ) + })} +
+ +
+ SUN · MOON + {sm.moon_glyph} today +
+
+
+
SUN
+
↑ rise{sm.sunrise}
+
☀ noon{sm.noon}
+
↓ set{sm.sunset}
+
+
+
MOON
+
phase{sm.moon_glyph}
+
illum{Math.round(sm.moon_illum * 100)}%
+
{sm.moon_phase}
+
+
+ +
+ STATION + {stn.id} +
+
+ {[ + ['datum', data.datum, 'ref'], + ['type', stn.predictions === 'Harmonic' ? 'harm' : 'sub', stn.predictions.toLowerCase()], + ['lat', stn.lat.toFixed(3), '°N'], + ['lon', stn.lon.toFixed(3), '°W'], + ['next ▲', snap.nextHigh ? snap.nextHigh.height.toFixed(1) : '—', snap.nextHigh ? fmtTime(snap.nextHigh.t) : ''], + ['next ▼', snap.nextLow ? snap.nextLow.height.toFixed(1) : '—', snap.nextLow ? fmtTime(snap.nextLow.t) : ''], + ].map((c, i) => ( +
+
{c[0]}
+
{c[1]}
+
{c[2]}
+
+ ))} +
+
+ ) +} + +// ── Filters screen ─────────────────────────────────────────────────────────── +function FiltersScreen(props: { + range: string; setRange: (r: string) => void + sortBy: SortBy; setSortBy: (s: SortBy) => void + groupBy: GroupBy; setGroupBy: (g: GroupBy) => void + showCompare: boolean; setShowCompare: (v: boolean) => void + showSun: boolean; setShowSun: (v: boolean) => void + showMoon: boolean; setShowMoon: (v: boolean) => void + showGrid: boolean; setShowGrid: (v: boolean) => void +}) { + const { range, setRange, sortBy, setSortBy, groupBy, setGroupBy } = props + return ( +
+
TIME RANGE
+
+ {RANGE_KEYS.map((r) => ( + + ))} +
+ +
SORT BY
+
+ {([['name', 'alphabetical'], ['height', 'current ft, high → low'], ['range', 'tidal range, big → small'], ['trend', 'rising · slack · falling']] as const).map(([k, s]) => ( + + ))} +
+ +
GROUP BY
+
+ {([['none', 'one big list'], ['region', 'central · islands · north'], ['trend', 'rising · slack · falling']] as const).map(([k, s], i) => ( + + ))} +
+ +
OVERLAYS
+
+ {([ + ['compare overlay', props.showCompare, props.setShowCompare, 'all stations on one chart'], + ['sun rise/set', props.showSun, props.setShowSun, 'shaded daylight, dawn/dusk markers'], + ['moon markers', props.showMoon, props.setShowMoon, 'lunar phase in drilldown'], + ['grid lines', props.showGrid, props.setShowGrid, 'subtle background grid'], + ] as const).map(([lbl, val, on, sub], i) => ( + + ))} +
+
+ ) +} + +// ── Saved screen ───────────────────────────────────────────────────────────── +interface SavedView { + name: string + meta: string + desc: string + glyph: string + apply: () => void +} +function SavedScreen({ views }: { views: SavedView[] }) { + return ( +
+
SAVED VIEWStap to load
+
+ {views.map((v, i) => ( + + ))} +
+
+ ) +} + +// ── Bottom tabs ────────────────────────────────────────────────────────────── +function Tabs({ view, setView }: { view: View; setView: (v: View) => void }) { + const tabs: { id: View; icon: string; lbl: string }[] = [ + { id: 'stations', icon: '▦', lbl: 'Stations' }, + { id: 'drill', icon: '◉', lbl: 'Drilldown' }, + { id: 'filters', icon: '⚑', lbl: 'Filters' }, + { id: 'saved', icon: '★', lbl: 'Saved' }, + ] + return ( + + ) +} + +// ── App ────────────────────────────────────────────────────────────────────── +export function MobileApp({ data }: { data: Bootstrap }) { + const [view, setView] = useState('stations') + const [range, setRange] = useState('24H') + const [focusId, setFocusId] = useState(data.stations[0]?.id ?? '') + const [sortBy, setSortBy] = useState('name') + const [groupBy, setGroupBy] = useState('none') + const [showCompare, setShowCompare] = useState(false) + const [showSun, setShowSun] = useState(true) + const [showMoon, setShowMoon] = useState(true) + const [showGrid, setShowGrid] = useState(true) + const [now, setNow] = useState(data.now) + + useEffect(() => { + const id = setInterval(() => setNow((n) => n + 1000), 1000) + return () => clearInterval(id) + }, []) + + const focusStn = data.stations.find((s) => s.id === focusId) ?? data.stations[0] + const sun: SunMarks = useMemo( + () => ({ + rise: pacificWallMs(data.date.iso, data.sun_moon.sunrise), + set: pacificWallMs(data.date.iso, data.sun_moon.sunset), + riseLabel: data.sun_moon.sunrise, + setLabel: data.sun_moon.sunset, + }), + [data.date.iso, data.sun_moon], + ) + + const openDrill = (id: string) => { + setFocusId(id) + setView('drill') + } + + const savedViews: SavedView[] = [ + { name: 'All stations · 24h', meta: 'default', desc: 'everything on the inner bay, last day', glyph: '▦', apply: () => { setRange('24H'); setGroupBy('none'); setShowCompare(false); setView('stations') } }, + { name: 'Bellingham · 7d', meta: 'focus', desc: 'main waterfront, week trend', glyph: '◉', apply: () => { setRange('7D'); setShowCompare(false); openDrill('9449211') } }, + { name: 'San Juans compare', meta: 'compare', desc: 'all stations stacked on one chart', glyph: '▭▭', apply: () => { setRange('24H'); setShowCompare(true); openDrill(focusId) } }, + { name: 'By region', meta: 'group', desc: 'central · islands · north', glyph: '⊞', apply: () => { setGroupBy('region'); setView('stations') } }, + { name: 'Biggest tides', meta: 'events', desc: 'sort by tidal range', glyph: '△', apply: () => { setSortBy('range'); setView('stations') } }, + ] + + return ( +
+
+
+
setView('stations')} onSetting={() => setView('filters')} /> +
+ {view === 'stations' && ( + + )} + {view === 'drill' && focusStn && ( + setView('stations')} /> + )} + {view === 'filters' && ( + + )} + {view === 'saved' && } +
+ +
+ ) +} diff --git a/frontend/src/styles/mobile.css b/frontend/src/styles/mobile.css new file mode 100644 index 0000000..1e78795 --- /dev/null +++ b/frontend/src/styles/mobile.css @@ -0,0 +1,641 @@ +/* ============================================================ + TIDES MOBILE — phone-optimized shell. + Inherits ice tokens from styles.css + chart vocabulary. + ============================================================ */ + +.tm-app { + position: relative; + width: 100%; height: 100%; + display: flex; flex-direction: column; + font-family: var(--sans); + color: oklch(0.95 0.015 225); + background: #060b14; + overflow: hidden; +} +.tm-bg { + position: absolute; inset: 0; + background: url('/shuksan-close.jpg') center/cover no-repeat; + filter: saturate(1.05) brightness(0.30); + z-index: 0; +} +.tm-bg-tint { + position: absolute; inset: 0; + background: + radial-gradient(ellipse at 50% 0%, rgba(80, 130, 190, 0.30), transparent 60%), + linear-gradient(180deg, rgba(8, 16, 30, 0.55) 0%, rgba(6, 11, 20, 0.92) 100%); + z-index: 1; +} + +/* ============================================================ + HEADER (glass, sticky) + ============================================================ */ +.tm-hd { + position: relative; + z-index: 3; + display: flex; align-items: flex-end; justify-content: space-between; + gap: 10px; + padding: 14px 16px 12px; + background: linear-gradient(180deg, rgba(8, 16, 30, 0.92), rgba(8, 16, 30, 0.55)); + -webkit-backdrop-filter: blur(18px) saturate(160%); + backdrop-filter: blur(18px) saturate(160%); + border-bottom: 1px solid rgba(140, 180, 220, 0.18); +} +.tm-hd-l { display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.tm-hd-eyebrow { + font-family: var(--mono); + font-size: 9px; + letter-spacing: 0.14em; + color: rgba(170, 210, 245, 0.65); +} +.tm-hd-title { + font-family: var(--sans); + font-weight: 300; + font-size: 24px; + letter-spacing: -0.02em; + line-height: 1.05; +} +.tm-hd-r { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } +.tm-live { + display: inline-flex; align-items: center; gap: 6px; + padding: 4px 8px; + border-radius: 4px; + font-family: var(--mono); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.04em; +} +.tm-live.is-live { background: rgba(120, 200, 150, 0.20); color: oklch(0.85 0.12 165); } +.tm-live.is-pause { background: rgba(180, 180, 180, 0.18); color: rgba(220, 232, 245, 0.70); } +.tm-live-dot { + width: 6px; height: 6px; border-radius: 50%; + background: currentColor; + animation: tm-pulse 2s infinite; +} +.tm-live.is-pause .tm-live-dot { animation: none; } +@keyframes tm-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } } + +.tm-icbtn { + appearance: none; + width: 30px; height: 30px; + border-radius: 6px; + border: 1px solid rgba(140, 180, 220, 0.22); + background: rgba(8, 16, 30, 0.55); + color: rgba(220, 232, 245, 0.85); + font-size: 13px; + font-family: var(--mono); + cursor: pointer; +} +.tm-icbtn:hover { color: oklch(0.95 0.015 225); border-color: rgba(150, 195, 235, 0.45); } + +/* ============================================================ + SCROLL container + ============================================================ */ +.tm-scroll { + position: relative; z-index: 2; + flex: 1; overflow-y: auto; + padding: 14px 14px 18px; + display: flex; flex-direction: column; + gap: 14px; + scrollbar-width: thin; + scrollbar-color: rgba(170, 210, 245, 0.25) transparent; +} +.tm-scroll::-webkit-scrollbar { width: 4px; } +.tm-scroll::-webkit-scrollbar-thumb { background: rgba(170, 210, 245, 0.25); border-radius: 2px; } + +.tm-screen { display: flex; flex-direction: column; gap: 14px; } + +/* ============================================================ + SECTION HEADER + ============================================================ */ +.tm-sec-hd { + display: flex; justify-content: space-between; align-items: center; + font-family: var(--mono); + font-size: 9.5px; + letter-spacing: 0.12em; + text-transform: uppercase; + padding: 4px 2px 2px; +} +.tm-sec-lbl { color: rgba(220, 232, 245, 0.85); font-weight: 600; } +.tm-sec-meta { color: rgba(220, 232, 245, 0.40); } + +/* ============================================================ + HERO CARD + ============================================================ */ +.tm-hero { + display: flex; flex-direction: column; gap: 10px; + padding: 14px 16px 12px; + background: linear-gradient(180deg, rgba(20, 45, 80, 0.58), rgba(15, 30, 55, 0.45)); + border: 1px solid rgba(150, 195, 235, 0.30); + border-radius: 10px; + box-shadow: inset 0 1px 0 rgba(150, 195, 235, 0.10), 0 8px 24px -16px rgba(0, 0, 0, 0.6); + cursor: pointer; +} +.tm-hero-top { + display: flex; justify-content: space-between; align-items: flex-start; +} +.tm-hero-code { + font-family: var(--mono); + font-size: 10px; + letter-spacing: 0.14em; + color: oklch(0.78 0.08 220); + font-weight: 600; +} +.tm-hero-name { + font-family: var(--sans); + font-weight: 300; + font-size: 20px; + letter-spacing: -0.01em; + margin-top: 2px; +} +.tm-pin { + display: inline-flex; align-items: center; gap: 5px; + padding: 3px 8px; + border-radius: 4px; + font-family: var(--mono); + font-size: 9.5px; + letter-spacing: 0.06em; + text-transform: uppercase; +} +.tm-pin-dot { width: 6px; height: 6px; border-radius: 50%; } +.tm-pin-rising { color: oklch(0.85 0.12 165); background: rgba(120, 200, 150, 0.15); } +.tm-pin-rising .tm-pin-dot { background: oklch(0.78 0.10 165); box-shadow: 0 0 6px oklch(0.78 0.10 165); } +.tm-pin-falling { color: oklch(0.82 0.12 30); background: rgba(220, 140, 110, 0.15); } +.tm-pin-falling .tm-pin-dot { background: oklch(0.74 0.10 35); box-shadow: 0 0 6px oklch(0.74 0.10 35); } +.tm-pin-slack { color: rgba(220, 232, 245, 0.7); background: rgba(160, 180, 200, 0.15); } +.tm-pin-slack .tm-pin-dot { background: oklch(0.78 0.04 240); } + +.tm-hero-figrow { + display: flex; align-items: center; gap: 12px; + padding: 2px 0; +} +.tm-hero-figs { display: flex; flex-direction: column; gap: 2px; } +.tm-hero-h { + display: flex; align-items: baseline; gap: 4px; + font-family: var(--sans); + font-weight: 200; + font-size: 40px; + line-height: 0.95; + font-variant-numeric: tabular-nums; + letter-spacing: -0.03em; + color: oklch(0.95 0.015 225); +} +.tm-hero-h i { font-style: normal; font-family: var(--mono); font-size: 13px; color: oklch(0.78 0.08 220); } +.tm-hero-rate { + font-family: var(--mono); + font-size: 12px; + color: rgba(220, 232, 245, 0.85); +} +.tm-hero-rate i { font-style: normal; font-size: 10px; color: rgba(220, 232, 245, 0.55); margin-left: 1px; } +.tm-hero-sub { + font-family: var(--mono); + font-size: 10px; + color: rgba(220, 232, 245, 0.55); + letter-spacing: 0.02em; +} + +.tm-hero-chart { + margin-top: 4px; + padding-top: 8px; + border-top: 1px solid rgba(140, 180, 220, 0.18); +} +.tm-hero-axis { + display: flex; justify-content: space-between; + font-family: var(--mono); + font-size: 9px; + color: rgba(220, 232, 245, 0.45); + margin-top: 2px; +} +.tm-now-pip { color: oklch(0.78 0.10 220); font-weight: 600; } + +.tm-hero-events { + display: grid; grid-template-columns: 1fr 1fr; gap: 8px; + margin-top: 2px; +} +.tm-evt-chip { + display: flex; align-items: center; gap: 8px; + padding: 8px 10px; + background: rgba(8, 16, 30, 0.5); + border: 1px solid rgba(140, 180, 220, 0.18); + border-radius: 6px; +} +.tm-evt-glyph { font-size: 11px; } +.tm-evt-glyph.tm-h { color: oklch(0.78 0.08 220); } +.tm-evt-glyph.tm-l { color: oklch(0.74 0.10 35); } +.tm-evt-k { font-family: var(--mono); font-size: 9px; color: rgba(220, 232, 245, 0.45); letter-spacing: 0.08em; text-transform: uppercase; } +.tm-evt-v { font-family: var(--mono); font-size: 11px; color: oklch(0.95 0.015 225); font-variant-numeric: tabular-nums; } +.tm-evt-v i { font-style: normal; font-size: 9px; color: rgba(220, 232, 245, 0.55); margin-left: 1px; } + +/* ============================================================ + RANGE PILLS + ============================================================ */ +.tm-range { + display: flex; align-items: center; gap: 8px; + padding: 6px 0; + position: sticky; top: 0; + z-index: 3; +} +.tm-range-lbl { + font-family: var(--mono); + font-size: 9px; + letter-spacing: 0.12em; + color: rgba(220, 232, 245, 0.45); + flex-shrink: 0; +} +.tm-range-pills { + display: flex; gap: 4px; + padding: 3px; + background: rgba(8, 16, 30, 0.75); + border: 1px solid rgba(140, 180, 220, 0.20); + border-radius: 6px; + flex: 1; + overflow-x: auto; + scrollbar-width: none; +} +.tm-range-pills::-webkit-scrollbar { display: none; } +.tm-range-pill { + appearance: none; + background: transparent; + border: 0; + padding: 5px 10px; + border-radius: 4px; + color: rgba(220, 232, 245, 0.65); + font-family: var(--mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.04em; + cursor: pointer; + flex: 1; + min-width: 40px; +} +.tm-range-pill.is-on { + background: linear-gradient(180deg, rgba(150, 195, 235, 0.30), rgba(150, 195, 235, 0.10)); + color: oklch(0.95 0.015 225); + box-shadow: inset 0 0 0 1px rgba(150, 195, 235, 0.32); +} + +/* ============================================================ + KPIs (horizontal scroll) + ============================================================ */ +.tm-kpis { + display: flex; gap: 8px; + overflow-x: auto; + margin: 0 -14px; + padding: 2px 14px 6px; + scrollbar-width: none; +} +.tm-kpis::-webkit-scrollbar { display: none; } +.tm-kpi { + flex-shrink: 0; + min-width: 92px; + display: flex; flex-direction: column; gap: 2px; + padding: 10px 12px; + background: rgba(15, 30, 55, 0.55); + border: 1px solid rgba(140, 180, 220, 0.18); + border-radius: 8px; +} +.tm-kpi-k { font-family: var(--mono); font-size: 9px; letter-spacing: 0.10em; color: rgba(220, 232, 245, 0.45); } +.tm-kpi-v { display: flex; align-items: baseline; gap: 3px; font-family: var(--sans); font-weight: 300; font-size: 22px; color: oklch(0.95 0.015 225); font-variant-numeric: tabular-nums; line-height: 1; } +.tm-kpi-v.is-glyph span { font-size: 24px; } +.tm-kpi-v i { font-style: normal; font-family: var(--mono); font-size: 10px; color: oklch(0.78 0.08 220); } +.tm-kpi-s { font-family: var(--mono); font-size: 9.5px; color: rgba(220, 232, 245, 0.55); } + +/* ============================================================ + STATION LIST ROWS + ============================================================ */ +.tm-list { + display: flex; flex-direction: column; + background: rgba(8, 16, 30, 0.55); + border: 1px solid rgba(140, 180, 220, 0.18); + border-radius: 8px; + overflow: hidden; +} +.tm-row { + appearance: none; + position: relative; + display: grid; + grid-template-columns: minmax(0, 1fr) 120px auto; + align-items: center; + gap: 10px; + padding: 10px 12px 10px 16px; + background: transparent; + border: 0; + border-bottom: 1px solid rgba(140, 180, 220, 0.10); + color: oklch(0.95 0.015 225); + text-align: left; + cursor: pointer; + transition: background 0.12s; +} +.tm-row:last-child { border-bottom: 0; } +.tm-row::before { + content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; + background: oklch(0.78 0.04 240); opacity: 0.5; +} +.tm-row-rising::before { background: oklch(0.78 0.10 165); } +.tm-row-falling::before { background: oklch(0.74 0.10 35); } +.tm-row-slack::before { background: oklch(0.78 0.04 240); } +.tm-row:hover { background: rgba(120, 170, 220, 0.08); } +.tm-row.is-focus { + background: rgba(120, 170, 220, 0.14); +} +.tm-row-l { display: flex; flex-direction: column; gap: 1px; min-width: 0; } +.tm-row-code { + display: inline-flex; align-items: center; gap: 6px; + font-family: var(--mono); font-size: 9.5px; font-weight: 600; letter-spacing: 0.10em; + color: rgba(220, 232, 245, 0.95); +} +.tm-pin-rising-d { background: oklch(0.78 0.10 165); box-shadow: 0 0 4px oklch(0.78 0.10 165); } +.tm-pin-falling-d { background: oklch(0.74 0.10 35); box-shadow: 0 0 4px oklch(0.74 0.10 35); } +.tm-pin-slack-d { background: oklch(0.78 0.04 240); } +.tm-row-code .tm-pin-dot { width: 6px; height: 6px; border-radius: 50%; } +.tm-row-name { font-family: var(--sans); font-size: 13px; font-weight: 500; line-height: 1.25; } +.tm-row-area { font-family: var(--mono); font-size: 9.5px; color: rgba(220, 232, 245, 0.45); } +.tm-row-m { overflow: hidden; } +.tm-row-r { text-align: right; display: flex; flex-direction: column; gap: 1px; } +.tm-row-h { font-family: var(--sans); font-weight: 400; font-size: 17px; font-variant-numeric: tabular-nums; line-height: 1; } +.tm-row-h i { font-style: normal; font-family: var(--mono); font-size: 9.5px; color: oklch(0.78 0.08 220); margin-left: 1px; } +.tm-row-rate { font-family: var(--mono); font-size: 9.5px; color: rgba(220, 232, 245, 0.55); } + +/* ============================================================ + DRILLDOWN screen + ============================================================ */ +.tm-back { + display: flex; justify-content: space-between; align-items: center; + font-family: var(--mono); font-size: 11px; +} +.tm-back-btn, .tm-back-pick { + appearance: none; + background: transparent; + border: 0; + color: oklch(0.78 0.08 220); + font-family: var(--mono); + font-size: 11px; + cursor: pointer; + padding: 4px 0; +} +.tm-back-pick { color: rgba(220, 232, 245, 0.55); } + +.tm-drill-hd { + display: flex; flex-direction: column; gap: 3px; + padding: 11px 14px; + background: linear-gradient(180deg, rgba(20, 45, 80, 0.58), rgba(15, 30, 55, 0.45)); + border: 1px solid rgba(150, 195, 235, 0.30); + border-radius: 10px; +} +.tm-drill-code { font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: 0.14em; color: oklch(0.78 0.08 220); } +.tm-drill-name { font-family: var(--sans); font-weight: 300; font-size: 20px; letter-spacing: -0.02em; } +.tm-drill-loc { font-family: var(--mono); font-size: 10px; color: rgba(220, 232, 245, 0.45); letter-spacing: 0.02em; } +.tm-drill-figrow { display: flex; align-items: center; gap: 14px; margin-top: 6px; padding-top: 8px; border-top: 1px solid rgba(140, 180, 220, 0.18); } +.tm-drill-h { + display: flex; align-items: baseline; gap: 4px; + font-family: var(--sans); font-weight: 200; font-size: 32px; line-height: 0.95; + font-variant-numeric: tabular-nums; letter-spacing: -0.03em; +} +.tm-drill-h i { font-style: normal; font-family: var(--mono); font-size: 12px; color: oklch(0.78 0.08 220); } +.tm-drill-state { font-family: var(--mono); font-size: 11px; color: rgba(220, 232, 245, 0.85); letter-spacing: 0.04em; } +.tm-drill-sub { font-family: var(--mono); font-size: 10px; color: rgba(220, 232, 245, 0.45); } + +.tm-drill-chart { + padding: 8px 6px 4px; + background: rgba(8, 16, 30, 0.55); + border: 1px solid rgba(140, 180, 220, 0.18); + border-radius: 8px; +} + +/* hours */ +.tm-hours { + display: flex; flex-direction: column; + background: rgba(8, 16, 30, 0.55); + border: 1px solid rgba(140, 180, 220, 0.18); + border-radius: 8px; + overflow: hidden; +} +.tm-hr { + display: grid; + grid-template-columns: 46px 1fr 60px 50px; + align-items: center; + gap: 10px; + padding: 6px 12px; + border-bottom: 1px solid rgba(140, 180, 220, 0.08); + font-family: var(--mono); + font-size: 11px; + color: oklch(0.95 0.015 225); +} +.tm-hr:last-child { border-bottom: 0; } +.tm-hr.is-now { background: rgba(150, 195, 235, 0.10); } +.tm-hr.is-now .tm-hr-t { color: oklch(0.78 0.08 220); font-weight: 600; } +.tm-hr-t { color: rgba(220, 232, 245, 0.65); } +.tm-hr-spark { height: 3px; background: rgba(120, 170, 220, 0.12); border-radius: 1.5px; overflow: hidden; } +.tm-hr-spark span { display: block; height: 100%; background: linear-gradient(90deg, oklch(0.78 0.08 220 / 0.45), oklch(0.78 0.08 220)); } +.tm-hr-h { font-variant-numeric: tabular-nums; text-align: right; } +.tm-hr-h i { font-style: normal; font-size: 9px; color: rgba(220, 232, 245, 0.45); margin-left: 1px; } +.tm-hr-r { color: rgba(220, 232, 245, 0.55); text-align: right; } + +/* solunar */ +.tm-rating { + font-family: var(--mono); + font-size: 9.5px; + padding: 2px 7px; + border-radius: 3px; + letter-spacing: 0.06em; +} +.tm-rating-good { background: rgba(120, 200, 150, 0.20); color: oklch(0.85 0.12 165); } +.tm-sol { + display: grid; grid-template-columns: 1fr 1fr; gap: 8px; +} +.tm-sol-col { + padding: 10px 12px; + background: rgba(8, 16, 30, 0.55); + border: 1px solid rgba(140, 180, 220, 0.18); + border-radius: 8px; +} +.tm-sol-k { font-family: var(--mono); font-size: 9px; letter-spacing: 0.10em; color: rgba(220, 232, 245, 0.45); margin-bottom: 6px; } +.tm-sol-row { + display: grid; grid-template-columns: 78px 1fr; + gap: 8px; align-items: center; + font-family: var(--mono); font-size: 10px; + color: oklch(0.95 0.015 225); + padding: 2px 0; +} +.tm-sol-bar { height: 4px; background: rgba(120, 170, 220, 0.12); border-radius: 2px; overflow: hidden; } +.tm-sol-bar span { display: block; height: 100%; background: linear-gradient(90deg, oklch(0.78 0.08 220 / 0.45), oklch(0.78 0.08 220)); } + +/* weather grid */ +.tm-wx { + display: grid; grid-template-columns: 1fr 1fr 1fr; + gap: 1px; + background: rgba(140, 180, 220, 0.18); + border: 1px solid rgba(140, 180, 220, 0.18); + border-radius: 8px; + overflow: hidden; +} +.tm-wx-cell { + display: flex; flex-direction: column; gap: 2px; + padding: 8px 10px; + background: rgba(8, 16, 30, 0.65); +} +.tm-wx-k { font-family: var(--mono); font-size: 9px; letter-spacing: 0.08em; color: rgba(220, 232, 245, 0.45); text-transform: uppercase; } +.tm-wx-v { font-family: var(--sans); font-weight: 300; font-size: 18px; line-height: 1; font-variant-numeric: tabular-nums; } +.tm-wx-v i { font-style: normal; font-family: var(--mono); font-size: 10px; color: oklch(0.78 0.08 220); margin-left: 2px; } +.tm-wx-s { font-family: var(--mono); font-size: 9px; color: rgba(220, 232, 245, 0.45); } + +/* ============================================================ + FILTERS screen + ============================================================ */ +.tm-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } +.tm-bigbtn { + appearance: none; + display: flex; flex-direction: column; gap: 3px; align-items: flex-start; + padding: 12px 14px; + background: rgba(8, 16, 30, 0.55); + border: 1px solid rgba(140, 180, 220, 0.20); + border-radius: 8px; + color: oklch(0.95 0.015 225); + text-align: left; + cursor: pointer; + transition: border-color 0.12s, background 0.12s; +} +.tm-bigbtn:hover { border-color: rgba(150, 195, 235, 0.40); } +.tm-bigbtn.is-on { + background: linear-gradient(180deg, rgba(150, 195, 235, 0.25), rgba(150, 195, 235, 0.08)); + border-color: rgba(150, 195, 235, 0.55); + box-shadow: inset 0 0 0 1px rgba(150, 195, 235, 0.15); +} +.tm-bigbtn-v { font-family: var(--mono); font-size: 13px; font-weight: 600; letter-spacing: 0.04em; } +.tm-bigbtn-s { font-family: var(--mono); font-size: 10px; color: rgba(220, 232, 245, 0.55); letter-spacing: 0.02em; } + +.tm-toggles { display: flex; flex-direction: column; gap: 6px; } +.tm-tog { + appearance: none; + display: flex; justify-content: space-between; align-items: center; gap: 12px; + padding: 10px 14px; + background: rgba(8, 16, 30, 0.55); + border: 1px solid rgba(140, 180, 220, 0.20); + border-radius: 8px; + color: oklch(0.95 0.015 225); + text-align: left; + cursor: pointer; +} +.tm-tog-v { font-family: var(--mono); font-size: 12px; font-weight: 600; letter-spacing: 0.02em; } +.tm-tog-s { font-family: var(--mono); font-size: 10px; color: rgba(220, 232, 245, 0.45); } +.tm-tog-switch { + width: 36px; height: 20px; + background: rgba(120, 170, 220, 0.18); + border-radius: 10px; + position: relative; + flex-shrink: 0; + transition: background 0.18s; +} +.tm-tog-switch span { + position: absolute; top: 2px; left: 2px; + width: 16px; height: 16px; + background: oklch(0.95 0.015 225); + border-radius: 50%; + transition: left 0.18s; +} +.tm-tog.is-on .tm-tog-switch { background: oklch(0.78 0.10 220); } +.tm-tog.is-on .tm-tog-switch span { left: 18px; } + +/* ============================================================ + SAVED screen + ============================================================ */ +.tm-saved { + display: flex; flex-direction: column; + background: rgba(8, 16, 30, 0.55); + border: 1px solid rgba(140, 180, 220, 0.20); + border-radius: 8px; + overflow: hidden; +} +.tm-saved-row { + appearance: none; + display: grid; + grid-template-columns: 24px 1fr auto; + align-items: center; + gap: 12px; + padding: 12px 14px; + background: transparent; + border: 0; + border-bottom: 1px solid rgba(140, 180, 220, 0.10); + color: oklch(0.95 0.015 225); + text-align: left; + cursor: pointer; +} +.tm-saved-row:last-child { border-bottom: 0; } +.tm-saved-row:hover { background: rgba(120, 170, 220, 0.08); } +.tm-saved-g { font-family: var(--mono); font-size: 14px; color: oklch(0.78 0.08 220); } +.tm-saved-body { display: flex; flex-direction: column; gap: 2px; } +.tm-saved-name { font-family: var(--sans); font-size: 13px; font-weight: 500; } +.tm-saved-desc { font-family: var(--mono); font-size: 10px; color: rgba(220, 232, 245, 0.50); } +.tm-saved-meta { + font-family: var(--mono); font-size: 9px; letter-spacing: 0.08em; + background: rgba(150, 195, 235, 0.14); + color: rgba(220, 232, 245, 0.75); + padding: 2px 7px; border-radius: 3px; + text-transform: uppercase; +} + +.tm-newview { + appearance: none; + display: flex; justify-content: space-between; align-items: center; + padding: 14px 16px; + background: rgba(8, 16, 30, 0.55); + border: 1px dashed rgba(150, 195, 235, 0.40); + border-radius: 8px; + color: oklch(0.78 0.08 220); + font-family: var(--mono); + font-size: 12px; + cursor: pointer; +} +.tm-newview-meta { color: rgba(220, 232, 245, 0.45); font-size: 10px; } + +/* ============================================================ + TAB BAR + ============================================================ */ +.tm-tabs { + position: relative; z-index: 3; + display: grid; grid-template-columns: repeat(4, 1fr); + gap: 4px; + padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px)); + background: linear-gradient(180deg, rgba(8, 16, 30, 0.55), rgba(8, 16, 30, 0.92)); + -webkit-backdrop-filter: blur(18px) saturate(160%); + backdrop-filter: blur(18px) saturate(160%); + border-top: 1px solid rgba(140, 180, 220, 0.18); +} +.tm-tab { + appearance: none; + display: flex; flex-direction: column; align-items: center; gap: 2px; + padding: 6px 4px; + background: transparent; + border: 0; + border-radius: 8px; + color: rgba(220, 232, 245, 0.55); + cursor: pointer; +} +.tm-tab.is-on { + background: rgba(150, 195, 235, 0.14); + color: oklch(0.95 0.015 225); +} +.tm-tab-icon { font-family: var(--mono); font-size: 13px; color: inherit; } +.tm-tab.is-on .tm-tab-icon { color: oklch(0.78 0.10 220); } +.tm-tab-lbl { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.06em; } + +/* Caption beneath each device when arranged on a wall */ +.tm-device-label { + position: absolute; + left: 0; right: 0; bottom: -28px; + text-align: center; + font-family: var(--mono); + font-size: 11px; + letter-spacing: 0.10em; + color: rgba(220, 232, 245, 0.55); + text-transform: uppercase; + pointer-events: none; +} + +/* ============================================================ + FULL-SCREEN HOST — on a real phone the app fills the viewport + (the design rendered inside a fixed 402×874 iOS frame). + ============================================================ */ +.tm-app { + height: 100dvh; + max-height: 100dvh; +} diff --git a/frontend/src/styles/tides.css b/frontend/src/styles/tides.css index cb63079..9230ecb 100644 --- a/frontend/src/styles/tides.css +++ b/frontend/src/styles/tides.css @@ -641,7 +641,7 @@ display: flex; align-items: baseline; gap: 4px; font-family: var(--sans); font-weight: 200; - font-size: 56px; + font-size: 40px; line-height: 0.95; color: var(--tt-fg); font-variant-numeric: tabular-nums; @@ -855,3 +855,63 @@ .bto-splash-title { font-family: var(--sans); font-weight: 200; font-size: 24px; letter-spacing: -0.02em; } .bto-splash-sub { font-size: 11px; color: rgba(220, 232, 245, 0.55); letter-spacing: 0.06em; } .bto-splash-err { font-size: 11px; color: oklch(0.78 0.12 30); letter-spacing: 0.04em; max-width: 60ch; text-align: center; } + +/* ============================================================ + INTERACTIVE CHART — hover readout + drag-to-zoom + ============================================================ */ +.tic-wrap { display: flex; flex-direction: column; gap: 6px; } +.tic-plot { position: relative; cursor: crosshair; user-select: none; -webkit-user-select: none; } +.tic-band { + position: absolute; + background: rgba(150, 195, 235, 0.16); + border-left: 1px solid rgba(150, 195, 235, 0.55); + border-right: 1px solid rgba(150, 195, 235, 0.55); + pointer-events: none; +} +.tic-readout { + position: absolute; + top: 6px; + display: flex; flex-direction: column; align-items: flex-start; + background: rgba(8, 16, 30, 0.9); + border: 1px solid rgba(150, 195, 235, 0.40); + border-radius: 4px; + padding: 3px 8px; + font-family: var(--mono); + pointer-events: none; + white-space: nowrap; +} +.tic-readout-t { font-size: 10px; color: var(--tt-fg-mute, rgba(220, 232, 245, 0.62)); } +.tic-readout-h { font-size: 13px; font-weight: 500; color: var(--tt-fg, #eaf0f7); font-variant-numeric: tabular-nums; } +.tic-readout-h i { font-style: normal; font-size: 9px; color: var(--tt-stroke, oklch(0.78 0.08 220)); margin-left: 2px; } +.tic-bar { + display: flex; align-items: center; justify-content: space-between; + font-family: var(--mono); font-size: 10px; + color: var(--tt-fg-faint, rgba(220, 232, 245, 0.38)); + letter-spacing: 0.04em; +} +.tic-reset { + appearance: none; + background: rgba(8, 16, 30, 0.6); + border: 1px solid rgba(150, 195, 235, 0.35); + color: var(--tt-fg, #eaf0f7); + border-radius: 4px; + padding: 3px 9px; + font-family: var(--mono); font-size: 10px; + cursor: pointer; +} +.tic-reset:hover { border-color: rgba(150, 195, 235, 0.6); } + +.bto-splash-retry { + margin-top: 16px; + appearance: none; + background: rgba(8, 16, 30, 0.6); + border: 1px solid rgba(150, 195, 235, 0.4); + color: var(--ice-100, #eaf4ff); + border-radius: 6px; + padding: 8px 18px; + font-family: var(--mono); + font-size: 12px; + letter-spacing: 0.04em; + cursor: pointer; +} +.bto-splash-retry:hover { border-color: rgba(150, 195, 235, 0.7); } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index a861e4f..05a1886 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -6,7 +6,9 @@ import react from '@vitejs/plugin-react' // localhost. Override with VITE_API_PROXY when needed. const apiProxy = process.env.VITE_API_PROXY ?? 'http://127.0.0.1:8000' +// Served at sean-michael.dev/tides, so assets and the dev server live under /tides/. export default defineConfig({ + base: '/tides/', plugins: [react()], server: { host: true,