diff --git a/.dockerignore b/.dockerignore index e37dc8373ed235e2d8701f7845c710645bc05ef8..ac3befa303e9e164fb856b3d06dabfc09781d005 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,8 @@ .venv .hypothesis .git +.env +.env.local **/__pycache__ .pytest_cache data/ diff --git a/.gitignore b/.gitignore index ccdfb677985a419c21237a7fae921e013a9150e1..4388c451ed06329c2e4a381dbb8e2f495e9a8442 100644 --- a/.gitignore +++ b/.gitignore @@ -19,10 +19,16 @@ data/recsys/ __pycache__/ *.pyc *.egg-info/ +mutants/ +.mutmut-cache/ # Node node_modules/ audit/node_modules/ +frontend/dist/ +.stryker-tmp/ +frontend/.stryker-tmp/ +stryker.log # IDE (Cursor): track `.cursor/hooks`, `.cursor/rules`, and `hooks.json`; ignore volatile state only. .cursor/hooks/state/ diff --git a/.ruff_cache/0.15.11/14315220232248816887 b/.ruff_cache/0.15.11/14315220232248816887 index 7ff11290111acfb3f53a59eaeb634b81ce4630bc..436c58b12326af820e86f41c603f8123afa42eb5 100644 Binary files a/.ruff_cache/0.15.11/14315220232248816887 and b/.ruff_cache/0.15.11/14315220232248816887 differ diff --git a/.ruff_cache/0.15.11/1920814609139379236 b/.ruff_cache/0.15.11/1920814609139379236 index 2e481ab9913fdfe3deb139812c03bbc30377f221..a6556f5f48625359eaf5a2ae41ace7a9c85460cf 100644 Binary files a/.ruff_cache/0.15.11/1920814609139379236 and b/.ruff_cache/0.15.11/1920814609139379236 differ diff --git a/.ruff_cache/0.15.11/5434035610807442273 b/.ruff_cache/0.15.11/5434035610807442273 index 0062053160f037e67fbc5d3d2daa88d5d37e5112..612b92c1d27be30e6f304dd63c047f7fb5933658 100644 Binary files a/.ruff_cache/0.15.11/5434035610807442273 and b/.ruff_cache/0.15.11/5434035610807442273 differ diff --git a/.ruff_cache/0.15.11/7290032257184656914 b/.ruff_cache/0.15.11/7290032257184656914 index ac0faf2306576201c1030bbbd9e4f5f544380b79..404c3c616600bb6beff3f8d4ed4444764222770f 100644 Binary files a/.ruff_cache/0.15.11/7290032257184656914 and b/.ruff_cache/0.15.11/7290032257184656914 differ diff --git a/.semgrepignore b/.semgrepignore index 550bb5c59ebb179eca01dbda0518629be274e16f..e550aabd533098f7230ab20705aaf163c57e1677 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -1,6 +1,9 @@ .venv/ +node_modules/ data/ audit/ +.cursor/ +frontend/dist/ frontend/node_modules/ **/__pycache__/ *.egg-info/ diff --git a/Dockerfile b/Dockerfile index 08d370563c4d65ef14b173b54195bb9decbbf0a0..ba1c11758fd6ddfc8af3623eb5a8b0e9518489da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,12 @@ ENV KINK_MEDIA_STRICT=0 ENV KINK_HF_SPACE_IMAGE=1 COPY . /app -RUN pip install --no-cache-dir pip setuptools wheel \ +RUN apt-get update \ + && apt-get install -y --no-install-recommends nodejs npm \ + && rm -rf /var/lib/apt/lists/* \ + && npm ci \ + && npm run build \ + && pip install --no-cache-dir pip setuptools wheel \ && pip install --no-cache-dir . \ && chmod +x /app/deploy/hf/entrypoint.sh diff --git a/Makefile b/Makefile index 1aa9cf8dd98feeb14f09673eb36f3f5184a3ecb4..a829e750f2f993ba40a11128b89572952ee65e50 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,11 @@ PARSER_ROOT ?= $(HOME)/PycharmProjects/parser PYTHON ?= $(shell if test -x "$(CURDIR)/.venv/bin/python"; then echo "$(CURDIR)/.venv/bin/python"; else command -v python3; fi) SEMGREP_BIN ?= $(CURDIR)/.venv/bin/semgrep SEMGREP_CONFIG ?= $(CURDIR)/semgrep.yml +MUTMUT_STATS ?= $(CURDIR)/mutants/mutmut-cicd-stats.json +MUTMUT_MIN_SCORE ?= 100 +MUTMUT_MAX_SURVIVED ?= 0 +MUTMUT_MAX_TIMEOUT ?= 0 +MUTMUT_MAX_NO_TESTS ?= 0 # Broad-scan excludes: parser-parity rules that are not yet adopted repo-wide (trim over time). SEMGREP_KINK_EXCLUDE_RULES ?= \ python-enforce-readable-line-length \ @@ -26,7 +31,7 @@ SEMGREP_KINK_EXCLUDE_RULES ?= \ SEMGREP_EXCLUDE_FLAGS := $(foreach r,$(SEMGREP_KINK_EXCLUDE_RULES),--exclude-rule $(r)) -.PHONY: semgrep-kink semgrep-recsys-core semgrep-recsys-strict ruff-kink dead-kink policy-kink test recsys-benchmark adv-probes-http prove-hf-media-placeholder publish-hf-verify verify-hf-live +.PHONY: semgrep-kink semgrep-recsys-core semgrep-recsys-strict ruff-kink dead-kink policy-kink test mutation-python recsys-benchmark adv-probes-http prove-hf-media-placeholder publish-hf-verify verify-hf-live # Python sources only (exclude data/, audit/node_modules, deploy artifacts). semgrep-kink: @@ -79,6 +84,15 @@ policy-kink: ruff-kink semgrep-recsys-strict test: "$(PYTHON)" -m pytest -q +mutation-python: + "$(PYTHON)" -m mutmut run + "$(PYTHON)" -m mutmut export-cicd-stats + "$(PYTHON)" "$(CURDIR)/scripts/check_mutmut_stats.py" "$(MUTMUT_STATS)" \ + --min-score "$(MUTMUT_MIN_SCORE)" \ + --max-survived "$(MUTMUT_MAX_SURVIVED)" \ + --max-timeout "$(MUTMUT_MAX_TIMEOUT)" \ + --max-no-tests "$(MUTMUT_MAX_NO_TESTS)" + # Build HF Docker image and assert missing ``/media/fetlife_fetishes/...`` returns JPEG fallback # (``KINK_HF_SPACE_IMAGE`` + strict env ignored). Requires Docker. prove-hf-media-placeholder: diff --git a/README.md b/README.md index d71712d473e851d3bb7e711f4a3a7729bd5cdb03..55480e6a264e09f4efd77266d7bd990693119671 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,9 @@ This repo runs as a **Docker Space** with the **same FastAPI backend** as local | `KINK_CATALOG_URL_BEARER` | Optional bearer for private **HTTP** catalog URLs. | | `B2_KEY_ID`, `B2_APPLICATION_KEY`, `B2_BUCKET`, `B2_REGION` | B2 S3 credentials (Space secrets; **parser**-compatible). With `KINK_B2_OBJECT_KEY`, downloads the catalog without a public URL. | | `KINK_B2_OBJECT_KEY` | Object path in the bucket (default `kink/catalog/store_slim.db`). | -| `KINK_HF_DATASET_REPO` | Hub dataset id (set with full-catalog mode when you do not use URL or B2). | +| `KINK_HF_DATASET_REPO` | Hub dataset id (with `KINK_HF_REQUIRE_FULL_CATALOG=1`, or with `KINK_HF_USE_HUB_DATASET=1` when full catalog is off). | | `KINK_HF_DATASET_FILENAME` | Remote filename in the dataset (e.g. `store_slim.db`). | +| `KINK_HF_USE_HUB_DATASET` | **`1`** = Hub download even when `KINK_HF_REQUIRE_FULL_CATALOG=0`. If unset, a leftover `KINK_HF_DATASET_REPO` variable is ignored and the **bundled** seed is used. | | `HF_TOKEN` | Required if the Hub dataset is **private** (not needed for public HTTPS URLs). | | `KINK_CORS_ORIGINS` | Only if you split frontend/API origins. Same-origin Space UI usually needs no change. | | `KINK_RECSYS_SOURCE` | Optional. When set and the app is **not** constructed with `recsys_settings=`, overrides `recsys/settings.json` for `source` (`graph` / `ots` / `hybrid`). | diff --git a/api.py b/api.py index 7be26459190aefc197e329ea46d1c7767e66ec5e..1cdb06446dda42d3c36883ffc6df6d66a5de6578 100644 --- a/api.py +++ b/api.py @@ -18,13 +18,15 @@ from sse_starlette.sse import EventSourceResponse from backend import Backend, VALID_RATINGS from backend.hf_bootstrap import ensure_store_db -_data_dir = Path(__file__).resolve().parent / "data" +_app_dir = Path(__file__).resolve().parent +_data_dir = _app_dir / "data" # App uses the slim DB (local: scripts/build_slim_store.py). Override with KINK_STORE_PATH. # On HF: point KINK_STORE_PATH at a mounted bucket path; Dockerfile defaults to bundled seed (set KINK_HF_DATASET_* for Hub catalog). _default_store = Path(os.environ["KINK_STORE_PATH"]) if os.environ.get("KINK_STORE_PATH") else _data_dir / "store_slim.db" -FRONTEND_PATH = Path(__file__).resolve().parent / "frontend" / "index.html" -FRONTEND_DIR = Path(__file__).resolve().parent / "frontend" -MEDIA_ROOT = Path(__file__).resolve().parent / "data" / "cached_assets" +FRONTEND_DIR = _app_dir / "frontend" +FRONTEND_DIST_DIR = FRONTEND_DIR / "dist" +FRONTEND_PATH = FRONTEND_DIST_DIR / "index.html" +MEDIA_ROOT = _app_dir / "data" / "cached_assets" ADMIN_SECRET = os.environ.get("KINK_ADMIN_SECRET", "") _MEDIA_CACHE = "public, max-age=86400, immutable" @@ -32,14 +34,23 @@ _MEDIA_FALLBACK_CACHE = "public, max-age=3600" def _frontend_static_cache_control(target: Path) -> str: - """Browser e2e and local iteration can set ``KINK_FRONTEND_NO_CACHE=1`` so module graphs always match disk.""" + """Browser e2e and local iteration can disable static caching for built assets.""" if os.environ.get("KINK_FRONTEND_NO_CACHE") == "1": return "no-store" - if target.suffix in {".js", ".css"}: - return "public, max-age=300, stale-while-revalidate=60" + if target.suffix in {".js", ".css"} and target.parent.name == "assets": + return "public, max-age=31536000, immutable" return "no-store" +def _require_frontend_build() -> None: + if FRONTEND_PATH.is_file(): + return + raise HTTPException( + status_code=503, + detail="Frontend build missing. Run `npm ci && npm run build` before serving the app.", + ) + + def _hf_space_published_image() -> bool: """True in the Hugging Face Space Docker image (``Dockerfile`` sets ``KINK_HF_SPACE_IMAGE=1``). @@ -172,21 +183,19 @@ app.add_middleware( @app.middleware("http") async def add_security_headers(request: Request, call_next): response = await call_next(request) - # connect-src: ES module graph (esm.sh), dynamic import (e.g. web-vitals from unpkg), fetch, beacon, WS. + # connect-src covers fetch/beacon/WS. Frontend dependencies are bundled by Vite and served same-origin. connect_src = " ".join( sorted( { "'self'", - "https://esm.sh", - "https://unpkg.com", *(origin.strip() for origin in cors_origins if origin.strip()), } ) ) csp = ( - "default-src 'self' https://esm.sh https://unpkg.com data:; " + "default-src 'self' data:; " "base-uri 'self'; frame-ancestors 'none'; form-action 'self'; frame-src 'none'; object-src 'none'; " - "script-src 'self' https://esm.sh https://unpkg.com; script-src-attr 'none'; " + "script-src 'self'; script-src-attr 'none'; " "style-src 'self' 'unsafe-inline'; style-src-attr 'unsafe-inline'; " "img-src 'self' data: https:; media-src 'self' data: https:; font-src 'self' data: https:; " f"connect-src {connect_src}; " @@ -361,13 +370,15 @@ async def run_blocking(func, /, *args, **kwargs): @app.get("/", response_class=HTMLResponse, responses={200: {"content": {"text/html": {}}}}) def frontend() -> FileResponse: + _require_frontend_build() return FileResponse(FRONTEND_PATH, headers={"Cache-Control": "no-store"}) @app.get("/frontend/{filepath:path}") def frontend_static(filepath: str) -> FileResponse: - target = (FRONTEND_DIR / filepath).resolve() - if FRONTEND_DIR.resolve() not in target.parents and target != FRONTEND_DIR.resolve(): + _require_frontend_build() + target = (FRONTEND_DIST_DIR / filepath).resolve() + if FRONTEND_DIST_DIR.resolve() not in target.parents and target != FRONTEND_DIST_DIR.resolve(): raise HTTPException(status_code=404, detail="Not found") if not target.exists() or not target.is_file(): raise HTTPException(status_code=404, detail="Not found") diff --git a/backend/enrichment.py b/backend/enrichment.py index 54efb22214d62a09b1616369448db56b3a40466e..65e05d80e7c507fc3924f0143d03c733270926c6 100644 --- a/backend/enrichment.py +++ b/backend/enrichment.py @@ -218,7 +218,7 @@ def _detail_summary_text(self, definition: str, summary: str, examples: list[str ex = _finalize_kink_card_blurb(kink_name, example.strip()) if ex: return ex - return "No sourced summary yet." + return "" def _image_trust_state(self, raw_asset_count: int, filtered_asset_count: int) -> str: if filtered_asset_count > 0: diff --git a/backend/hf_bootstrap.py b/backend/hf_bootstrap.py index d7e4ef34aac2f7bbacd65ab0dab3df30518677b9..0289ce6fdeb053e611b9b1dc18a8f6de07ec9f08 100644 --- a/backend/hf_bootstrap.py +++ b/backend/hf_bootstrap.py @@ -6,8 +6,9 @@ Priority when the store file is missing: 2. **Backblaze B2 (S3 API)** — if ``B2_KEY_ID``, ``B2_APPLICATION_KEY``, ``B2_BUCKET``, ``B2_REGION`` are set (same names as ``parser`` ``hf_deploy.py`` Space secrets), download ``KINK_B2_OBJECT_KEY`` (default ``kink/catalog/store_slim.db``). Works with **private** buckets; requires ``boto3``. -3. ``KINK_HF_DATASET_REPO`` — Hub **dataset** download. -4. Bundled seed (only when ``KINK_HF_REQUIRE_FULL_CATALOG`` is off). +3. ``KINK_HF_DATASET_REPO`` — Hub **dataset** download (only when ``KINK_HF_REQUIRE_FULL_CATALOG`` is on, or when + ``KINK_HF_USE_HUB_DATASET=1`` while full catalog is off — see below). +4. Bundled seed when full catalog is off and Hub dataset is not explicitly requested. On Spaces: mount persistent storage and set ``KINK_STORE_PATH`` so the file survives restarts. """ @@ -80,8 +81,9 @@ def _download_store_from_http(url: str, dest: Path) -> None: if token: headers["Authorization"] = f"Bearer {token}" + timeout = httpx.Timeout(connect=30.0, read=None, write=30.0, pool=30.0) try: - with httpx.Client(timeout=None, follow_redirects=True, headers=headers) as client: + with httpx.Client(timeout=timeout, follow_redirects=True, headers=headers) as client: with client.stream("GET", url) as response: response.raise_for_status() with partial.open("wb") as out: @@ -122,6 +124,10 @@ def ensure_store_db(target: Path) -> Path: return target repo = os.environ.get("KINK_HF_DATASET_REPO", "").strip() + if not require_full and repo and not _env_truthy("KINK_HF_USE_HUB_DATASET"): + # HF Space "Variables" often keep KINK_HF_DATASET_REPO after switching the image to bundled-only. + # That triggered anonymous hf_hub_download (slow / OOM) and ignored the bundled seed. + repo = "" if require_full and not repo: repo = ( @@ -139,7 +145,8 @@ def ensure_store_db(target: Path) -> Path: f"SQLite database not found: {target}. " "Locally: run python scripts/build_slim_store.py. " "On Hugging Face: set KINK_CATALOG_URL (public HTTPS), or B2_KEY_ID/B2_APPLICATION_KEY/B2_BUCKET/B2_REGION " - "(and KINK_B2_OBJECT_KEY), or KINK_HF_DATASET_REPO (Hub dataset), " + "(and KINK_B2_OBJECT_KEY), or KINK_HF_REQUIRE_FULL_CATALOG=1 plus KINK_HF_DATASET_REPO (Hub dataset), " + "or KINK_HF_USE_HUB_DATASET=1 with KINK_HF_DATASET_REPO when full catalog is off, " "KINK_STORE_PATH on a mounted bucket if you need persistence, " "or upload an existing DB to the bucket first. " "For the Docker image, ensure deploy/hf/seed/hf_bundled_store.db is included in the build context." diff --git a/deploy/hf/README.md b/deploy/hf/README.md index f2f3bfaadf0897ce718a60971bccfac5aed26f3f..c23aa7bed6e3d02c99d7550dd4c489a73db8829a 100644 --- a/deploy/hf/README.md +++ b/deploy/hf/README.md @@ -53,8 +53,9 @@ For the Space, set **`KINK_CATALOG_URL`** (public HTTPS), or **`B2_*`** + **`KIN | `KINK_CATALOG_URL_BEARER` | Optional bearer token for private HTTP object URLs | | `B2_KEY_ID`, `B2_APPLICATION_KEY`, `B2_BUCKET`, `B2_REGION` | Backblaze S3 credentials (Space secrets; same names as **parser**). Used when `KINK_CATALOG_URL` is unset. | | `KINK_B2_OBJECT_KEY` | Object key inside the bucket (default `kink/catalog/store_slim.db`) | -| `KINK_HF_DATASET_REPO` | Hub dataset id (set on the Space with `KINK_HF_REQUIRE_FULL_CATALOG=1` when using Hub instead of URL/B2) | +| `KINK_HF_DATASET_REPO` | Hub dataset id (used when `KINK_HF_REQUIRE_FULL_CATALOG=1`, or with `KINK_HF_USE_HUB_DATASET=1` while full catalog is off) | | `KINK_HF_DATASET_FILENAME` | Remote filename in the dataset (e.g. `store_slim.db`) | +| `KINK_HF_USE_HUB_DATASET` | Set **`1`** only if you want a Hub download while **`KINK_HF_REQUIRE_FULL_CATALOG=0`**. Otherwise a leftover `KINK_HF_DATASET_REPO` Space variable is **ignored** so the bundled seed is used (avoids anonymous Hub traffic and OOMs). | | `HF_TOKEN` | If the Hub dataset is private | | `KINK_SKIP_HEAVY_WARM` | **`1` in the published Docker image** skips only **PPR / similarity-graph** warm-up (saves RAM on **cpu-basic**). The **in-memory catalog** is still built at process start so the first recommendations request is not blocked. Set to **`0`** on larger hardware for full graph warm-up (better cold recs for heavy graph users, higher memory). | | `KINK_CORS_ORIGINS` | Your Space URL for browser API calls | diff --git a/deploy/hf/entrypoint.sh b/deploy/hf/entrypoint.sh index 345df779e0592c6b207ee13e168aa5e62cf3a0ab..0bd26f74c37e1f5a4b486b701accf14f42afbe75 100644 --- a/deploy/hf/entrypoint.sh +++ b/deploy/hf/entrypoint.sh @@ -8,7 +8,9 @@ export KINK_STORE_PATH if [ ! -f "$KINK_STORE_PATH" ]; then mkdir -p "$(dirname "$KINK_STORE_PATH")" # Full-catalog images: never copy the bundled seed. If Hub dataset or HTTPS URL is set, leave DB missing for bootstrap. - if [ "${KINK_HF_REQUIRE_FULL_CATALOG:-0}" != "1" ] && [ -z "${KINK_HF_DATASET_REPO:-}" ] && [ -z "${KINK_CATALOG_URL:-}" ] && [ -z "${B2_BUCKET:-}" ] && [ -z "${B2_KEY_ID:-}" ] && [ -f "$KINK_SEED_PATH" ]; then + # Do not require KINK_HF_DATASET_REPO to be unset: Spaces often keep a stale repo id; bundled mode still wins in api. + # Skip copy when opting into Hub dataset without full-catalog flag (KINK_HF_USE_HUB_DATASET=1). + if [ "${KINK_HF_REQUIRE_FULL_CATALOG:-0}" != "1" ] && [ "${KINK_HF_USE_HUB_DATASET:-0}" != "1" ] && [ -z "${KINK_CATALOG_URL:-}" ] && [ -z "${B2_BUCKET:-}" ] && [ -z "${B2_KEY_ID:-}" ] && [ -f "$KINK_SEED_PATH" ]; then cp "$KINK_SEED_PATH" "$KINK_STORE_PATH" fi fi diff --git a/deploy/hf/seed/hf_bundled_store.db b/deploy/hf/seed/hf_bundled_store.db index b125d895883e832acbc891420f45bb00d5e2be4c..a954be24898ec351855a55b9b081b4b455aa2952 100644 --- a/deploy/hf/seed/hf_bundled_store.db +++ b/deploy/hf/seed/hf_bundled_store.db @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab62f5134733efd6d8dec33f0a4d034549e3ca255eef494a4802ba7d3eae7267 +oid sha256:c2984f10e868979a832daedf7c51ae45c1e37214fe312d700140a457227eeb2a size 376832 diff --git a/frontend/api.js b/frontend/api.js index d3fd13f72c1ab8d3fc6145fad4cb4b133b58ddd3..193cddec71132d607b07f44db06a1d41e8e26600 100644 --- a/frontend/api.js +++ b/frontend/api.js @@ -75,4 +75,4 @@ export function currentDirectionsFor(plays, kinkId) { return plays?.[kinkId]?.directions || []; } -export { directionColumnsForPlay, directionsAfterColumnDrop } from "./direction-logic.js?v=32"; +export { directionColumnsForPlay, directionsAfterColumnDrop } from "./direction-logic.js"; diff --git a/frontend/app.js b/frontend/app.js index cb753ee2d2ce9b5952354ca6217468907dcc57a2..ad53979223732986645f5823a5ed08d2b4a58c33 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1,21 +1,21 @@ -import React, { useEffect, useMemo, useRef, useState, useCallback } from "https://esm.sh/react@18.3.1"; -import { createRoot } from "https://esm.sh/react-dom@18.3.1/client"; +import React, { useEffect, useMemo, useRef, useState, useCallback } from "react"; +import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider, useQueryClient, -} from "https://esm.sh/@tanstack/react-query@5.59.20?deps=react@18.3.1"; +} from "@tanstack/react-query"; -import { html, VIEWS } from "./constants.js?v=33"; -import { currentDirectionsFor } from "./api.js?v=33"; -import { directionsAfterColumnDrop } from "./direction-logic.js?v=32"; -import { StatusText, RoleChip } from "./components.js?v=32"; +import { html, VIEWS } from "./constants.js"; +import { currentDirectionsFor } from "./api.js"; +import { directionsAfterColumnDrop } from "./direction-logic.js"; +import { StatusText, RoleChip } from "./components.js"; import { useUserQuery, useGroupsQuery, useRolesQuery, useRoleSearchQuery, -} from "./queries.js?v=41"; +} from "./queries.js"; import { useLoginMutation, useCreateMutation, @@ -28,17 +28,17 @@ import { useRemoveGroupMemberMutation, useShareToggleMutation, usePartnerRequestMutations, -} from "./mutations.js?v=52"; -import { optimisticBoard, removeKinkFromItemsPayload, playCommitKey } from "./board-utils.js?v=33"; -import { DiscoverView } from "./discover.js?v=65"; -import { normalizeDiscoverSheetDirections } from "./discover-flow.js?v=1"; -import { ScenariosView } from "./scenarios.js?v=1"; -import { MyPlaysView } from "./my-plays.js?v=37"; -import { TogetherView } from "./together.js?v=37"; -import { SettingsView } from "./settings.js?v=32"; -import { initTelemetry } from "./telemetry.js?v=33"; -import { preferredPartnerGroupId, resolvedPartnerGroups } from "./partner-groups.js?v=33"; -import { nextStarterCompleted, starterViewState } from "./starter-flow.js?v=34"; +} from "./mutations.js"; +import { optimisticBoard, removeKinkFromItemsPayload, playCommitKey } from "./board-utils.js"; +import { DiscoverView } from "./discover.js"; +import { normalizeDiscoverSheetDirections } from "./discover-flow.js"; +import { ScenariosView } from "./scenarios.js"; +import { MyPlaysView } from "./my-plays.js"; +import { TogetherView } from "./together.js"; +import { SettingsView } from "./settings.js"; +import { initTelemetry } from "./telemetry.js"; +import { preferredPartnerGroupId, resolvedPartnerGroups } from "./partner-groups.js"; +import { nextStarterCompleted, starterViewState } from "./starter-flow.js"; const queryClient = new QueryClient(); const EMPTY_BOARD = { to_me: [], by_me: [], together: [], hidden: [], no_go: [] }; diff --git a/frontend/board-utils.test.mjs b/frontend/board-utils.test.mjs index 3a526c9368748ad9ab3350b2d4eff91f43c70f79..be38c314b68dc66df2fc09eebddc0c8faab20e32 100644 --- a/frontend/board-utils.test.mjs +++ b/frontend/board-utils.test.mjs @@ -5,7 +5,24 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { optimisticBoard } from "./board-utils.js"; +import { + findBoardItemInData, + optimisticBoard, + playCommitKey, + removeKinkFromItemsPayload, + sortedBoardItems, +} from "./board-utils.js"; + +test("sortedBoardItems orders by popularity descending then name ascending without mutating input", () => { + const input = [ + { id: "low", name: "Zed", popularity: 5 }, + { id: "b", name: "Beta", popularity: 10 }, + { id: "a", name: "Alpha", popularity: 10 }, + ]; + + assert.deepEqual(sortedBoardItems(input).map((item) => item.id), ["a", "b", "low"]); + assert.deepEqual(input.map((item) => item.id), ["low", "b", "a"]); +}); test("optimisticBoard places stub when no server board and no query cache", () => { const b = optimisticBoard(null, "fetlife_999", "like", ["by_me", "to_me"], null, null); @@ -13,3 +30,176 @@ test("optimisticBoard places stub when no server board and no query cache", () = const inBy = b.by_me.some((x) => x.id === "fetlife_999"); assert.ok(inTo && inBy, "stub row must appear in every direction bucket requested"); }); + +test("optimisticBoard removes stale copies and routes hard_no / not_interested to private buckets", () => { + const previous = { + to_me: [{ id: "k1", name: "Alpha", popularity: 1 }], + by_me: [{ id: "k1", name: "Alpha", popularity: 1 }], + together: [{ id: "k2", name: "Beta", popularity: 2 }], + hidden: [], + no_go: [], + }; + + const hidden = optimisticBoard(previous, "k1", "not_interested", ["to_me"], null, null); + assert.deepEqual(hidden.to_me, []); + assert.deepEqual(hidden.by_me, []); + assert.deepEqual(hidden.hidden.map((item) => item.id), ["k1"]); + + const noGo = optimisticBoard(previous, "k1", "hard_no", ["to_me"], null, null); + assert.deepEqual(noGo.to_me, []); + assert.deepEqual(noGo.by_me, []); + assert.deepEqual(noGo.no_go.map((item) => item.id), ["k1"]); +}); + +test("optimisticBoard uses together when positive directions are empty", () => { + const b = optimisticBoard(null, "k1", "love", [], null, null); + assert.deepEqual(b.together.map((item) => item.id), ["k1"]); + assert.deepEqual(b.to_me, []); + assert.deepEqual(b.by_me, []); +}); + +test("optimisticBoard can hydrate item details from query caches", () => { + const queries = [ + { queryKey: ["search", "Bond"], state: { data: { items: [{ kink: { id: "k1", name: "Bondage", definition: "Fixture", popularity: 42 } }] } } }, + ]; + const qc = { + getQueryData(key) { + if (JSON.stringify(key) === JSON.stringify(["kink", "k2"])) { + return { id: "k2", name: "Spanking", detail_summary: "Fixture", popularity: 11 }; + } + return null; + }, + getQueryCache() { + return { findAll: ({ predicate }) => queries.filter(predicate) }; + }, + }; + + const fromSearch = optimisticBoard(null, "k1", "like", ["to_me"], qc, { userId: "u1" }); + assert.deepEqual(fromSearch.to_me[0], { + id: "k1", + name: "Bondage", + summary: "Fixture", + popularity: 42, + interest_state: "like", + directions: ["to_me"], + }); + + const fromDetail = optimisticBoard(null, "k2", "like", ["by_me"], qc, { userId: "u1" }); + assert.equal(fromDetail.by_me[0].name, "Spanking"); + assert.equal(fromDetail.by_me[0].summary, "Fixture"); +}); + +test("optimisticBoard hydrates from scoped rec and prompt caches before falling back to global scans", () => { + const scoped = new Map([ + [ + JSON.stringify(["recs", "u1", ""]), + { items: [{ kink: { id: "rec-hit", name: "Rec Hit", summary: "rec", popularity: 9 } }] }, + ], + [ + JSON.stringify(["prompts", "u1", ""]), + { items: [{ kink: { id: "prompt-hit", name: "Prompt Hit", definition: "prompt", popularity: 8 } }] }, + ], + ]); + const qc = { + getQueryData(key) { + return scoped.get(JSON.stringify(key)) || null; + }, + getQueryCache() { + return { findAll: () => [] }; + }, + }; + + const fromRec = optimisticBoard(null, "rec-hit", "like", ["to_me"], qc, { userId: "u1" }); + const fromPrompt = optimisticBoard(null, "prompt-hit", "like", ["by_me"], qc, { userId: "u1" }); + + assert.equal(fromRec.to_me[0].name, "Rec Hit"); + assert.equal(fromRec.to_me[0].summary, "rec"); + assert.equal(fromPrompt.by_me[0].name, "Prompt Hit"); + assert.equal(fromPrompt.by_me[0].summary, "prompt"); +}); + +test("optimisticBoard hydrates from group-overlap direct and similar caches only", () => { + const queries = [ + { + queryKey: ["group-overlap", "u1", "g1"], + state: { + data: { + direct_matches: [{ id: "direct-hit", name: "Direct Hit", summary: "direct", popularity: 7 }], + similar_matches: [{ kink: { id: "similar-hit", name: "Similar Hit", detail_summary: "similar", popularity: 6 } }], + }, + }, + }, + { + queryKey: ["other", "u1"], + state: { + data: { + direct_matches: [{ id: "wrong-hit", name: "Wrong Hit", popularity: 99 }], + }, + }, + }, + ]; + const qc = { + getQueryData() { + return null; + }, + getQueryCache() { + return { findAll: ({ predicate }) => queries.filter(predicate) }; + }, + }; + + const direct = optimisticBoard(null, "direct-hit", "love", ["together"], qc, { userId: "u1" }); + const similar = optimisticBoard(null, "similar-hit", "like", ["to_me"], qc, { userId: "u1" }); + const wrong = optimisticBoard(null, "wrong-hit", "like", ["by_me"], qc, { userId: "u1" }); + + assert.equal(direct.together[0].name, "Direct Hit"); + assert.equal(direct.together[0].summary, "direct"); + assert.equal(similar.to_me[0].name, "Similar Hit"); + assert.equal(similar.to_me[0].summary, "similar"); + assert.equal(wrong.by_me[0].name, "wrong-hit"); +}); + +test("findBoardItemInData scans every bucket and tolerates missing boards", () => { + const board = { + to_me: [], + by_me: [{ id: 7, name: "Seven" }], + together: [], + hidden: [], + no_go: [], + }; + assert.equal(findBoardItemInData(null, 7), null); + assert.deepEqual(findBoardItemInData(board, "7"), { id: 7, name: "Seven" }); +}); + +test("removeKinkFromItemsPayload filters by kink id while preserving payload metadata", () => { + const payload = { + cursor: "next", + items: [{ kink: { id: "keep" } }, { kink: { id: 12 } }, { kink: { id: "12" } }], + }; + + assert.equal(removeKinkFromItemsPayload(null, "x"), null); + assert.deepEqual(removeKinkFromItemsPayload(payload, 12), { + cursor: "next", + items: [{ kink: { id: "keep" } }], + }); +}); + +test("removeKinkFromItemsPayload preserves malformed rows that are not the removed kink", () => { + const payload = { + items: [ + {}, + { kink: null }, + { kink: { id: "remove" } }, + { kink: { id: "keep" } }, + ], + }; + + assert.deepEqual(removeKinkFromItemsPayload(payload, "remove"), { + items: [{}, { kink: null }, { kink: { id: "keep" } }], + }); +}); + +test("playCommitKey is stable for direction ordering and empty directions", () => { + assert.equal(playCommitKey("k1", "like", ["by_me", "to_me"]), "k1::like::by_me|to_me"); + assert.equal(playCommitKey("k1", "like", ["to_me", "by_me"]), "k1::like::by_me|to_me"); + assert.equal(playCommitKey("k1", "hard_no"), "k1::hard_no::"); +}); diff --git a/frontend/components.js b/frontend/components.js index 7e186df67b1c5367ad371e4fd944e955a7f2590b..b8f5e7e8bacfe718c0de0d7616cd7265c041a276 100644 --- a/frontend/components.js +++ b/frontend/components.js @@ -1,6 +1,6 @@ -import React, { useState, useCallback, useEffect } from "https://esm.sh/react@18.3.1"; -import { html, UI_REACTIONS, DIRECTION_PILLS } from "./constants.js?v=32"; -import { kinkCategoryLabel } from "./ui-labels.js?v=32"; +import React, { useState, useCallback, useEffect } from "react"; +import { html, UI_REACTIONS, DIRECTION_PILLS } from "./constants.js"; +import { kinkCategoryLabel } from "./ui-labels.js"; /** One photo at a time; tap left/right edges to change. */ function KinkMediaScroll({ assets, imageRevealed, kinkName, onReveal, resetKey }) { @@ -178,10 +178,13 @@ export function KinkCard({ if (layout === "tinder") { const expanded = Boolean(infoExpanded); - const tappable = typeof onToggleInfo === "function"; + const notes = (kink.notes || "").trim(); const blurb = discoverCollapsedBlurb(kink); const fullDef = discoverFullDefinition(kink); - const notes = (kink.notes || "").trim(); + /** No "Tap for full info" when collapsed blurb, full text, and notes are all empty (or full text equals blurb). */ + const tinderExpandable = + Boolean(notes) || (Boolean(fullDef) && (!blurb || fullDef !== blurb)); + const tappable = typeof onToggleInfo === "function" && tinderExpandable; const tinderBody = html` ${category ? html`
${category}
` : null}

${kink.name}

diff --git a/frontend/constants.js b/frontend/constants.js index 2baa472251901349c9ef19b846063cb192b6cf56..7d45d3807faf294df1a9a015a1a7cdb996bee182 100644 --- a/frontend/constants.js +++ b/frontend/constants.js @@ -1,5 +1,5 @@ -import React from "https://esm.sh/react@18.3.1"; -import htm from "https://esm.sh/htm@3.1.1"; +import React from "react"; +import htm from "htm"; export const html = htm.bind(React.createElement); diff --git a/frontend/direction-logic.test.mjs b/frontend/direction-logic.test.mjs index 57f44abde225161088d0230f340ead1352135fe2..50e00656cca825a7cfc41ced9678f96cf3b34dc2 100644 --- a/frontend/direction-logic.test.mjs +++ b/frontend/direction-logic.test.mjs @@ -22,6 +22,11 @@ test("direction columns: empty directions defaults to together", () => { assert.deepEqual(directionColumnsForPlay("x", {}), ["together"]); }); +test("direction columns: invalid stored directions are ignored", () => { + assert.deepEqual(directionColumnsForPlay("x", { x: { directions: ["sideways"] } }), ["together"]); + assert.deepEqual(directionColumnsForPlay("x", { x: { directions: ["sideways", "by_me"] } }), ["by_me"]); +}); + test("REGRESSION: primary-first-column collapses dual directions — must NOT match contract", () => { /** Old UI bug: only the first DIR_ORDER hit was used, hiding by_me when to_me was also set. */ function legacyPrimaryDirectionColumn(playId, currentPlays) { @@ -46,3 +51,9 @@ test("directionsAfterColumnDrop: dragging onto by_me merges with existing to_me" test("directionsAfterColumnDrop: together column replaces specifics", () => { assert.deepEqual(directionsAfterColumnDrop(["to_me", "by_me"], "together"), ["together"]); }); + +test("directionsAfterColumnDrop: together and invalid existing directions do not survive specific drops", () => { + assert.deepEqual(directionsAfterColumnDrop(["together"], "to_me"), ["to_me"]); + assert.deepEqual(directionsAfterColumnDrop(["sideways", "to_me"], "by_me"), ["by_me", "to_me"]); + assert.deepEqual(directionsAfterColumnDrop(["by_me", "to_me"], "by_me"), ["by_me", "to_me"]); +}); diff --git a/frontend/discover-flow.test.mjs b/frontend/discover-flow.test.mjs index c2ef50bd6cfc093fbebb6ec380b51843668fea68..4bff7e80f473bb7640008788dfa226d67ecf3cf8 100644 --- a/frontend/discover-flow.test.mjs +++ b/frontend/discover-flow.test.mjs @@ -11,6 +11,7 @@ import { test("positive discover reactions require direction follow-up", () => { assert.equal(isPositiveDiscoverRating("love"), true); + assert.equal(isPositiveDiscoverRating(" like "), true); assert.equal(isPositiveDiscoverRating("like"), true); assert.equal(isPositiveDiscoverRating("curious"), true); assert.equal(isPositiveDiscoverRating("not_interested"), false); @@ -57,3 +58,7 @@ test("applyDiscoverDirectionChoice: together to together then to_me", () => { assert.deepEqual(applyDiscoverDirectionChoice(["together"], "to_me"), ["to_me"]); assert.deepEqual(applyDiscoverDirectionChoice(["to_me"], "together"), ["together"]); }); + +test("applyDiscoverDirectionChoice: invalid keys reset to together", () => { + assert.deepEqual(applyDiscoverDirectionChoice(["to_me"], "sideways"), ["together"]); +}); diff --git a/frontend/discover-scenarios.test.mjs b/frontend/discover-scenarios.test.mjs index 7281e952bc95b6bd4635f767490f476bffaefa45..99f88267512a8b53be7da688f9c05962c348bedf 100644 --- a/frontend/discover-scenarios.test.mjs +++ b/frontend/discover-scenarios.test.mjs @@ -28,3 +28,24 @@ test("discoverScenarioWrites only emits changed draft scenario choices", () => { { scenarioKinkId: "scene_a", interestState: "love", directions: ["together"] }, ]); }); + +test("discoverScenarioWrites accepts kink/raw scenario shapes and normalizes directions", () => { + const writes = discoverScenarioWrites({ + draftStates: { scene_a: "love", scene_b: "like", scene_c: "curious" }, + scenarioItems: [ + { kink: { id: "scene_a" } }, + { id: "scene_b" }, + { scenario: { id: "" } }, + null, + ], + scenarioPreferences: { + scene_c: { interest_state: "hard_no" }, + }, + directions: ["to_me", "", "to_me", "by_me"], + }); + + assert.deepEqual(writes, [ + { scenarioKinkId: "scene_a", interestState: "love", directions: ["to_me", "by_me"] }, + { scenarioKinkId: "scene_b", interestState: "like", directions: ["to_me", "by_me"] }, + ]); +}); diff --git a/frontend/discover-state.test.mjs b/frontend/discover-state.test.mjs index afa7aa88e3c6fe129592e3ccff537684c65e2022..ca4f4e3ab419a959e6231813b8d24b72f5ae8912 100644 --- a/frontend/discover-state.test.mjs +++ b/frontend/discover-state.test.mjs @@ -27,6 +27,19 @@ test("visibleDiscoveryItems drops already-rated raw recommendation rows", () => assert.deepEqual(visibleDiscoveryItems(rawItems, currentPlays).map((row) => row.kink.id), ["b"]); }); +test("visibleDiscoveryItems skips malformed rows, blank ids, and duplicate ids", () => { + const rawItems = [ + null, + { kink: null }, + { kink: { id: "" } }, + { kink: { id: "a" } }, + { kink: { id: "a" } }, + { kink: { id: 9 } }, + ]; + + assert.deepEqual(visibleDiscoveryItems(rawItems, {}).map((row) => row.kink.id), ["a", 9]); +}); + test("empty visible deck refetches even when raw cache still has only already-rated rows", () => { const rawItems = [{ kink: { id: "a" } }, { kink: { id: "b" } }]; const currentPlays = { diff --git a/frontend/discover.js b/frontend/discover.js index 650e03ef1b84de80947218633b53305d463638a7..4475341c51d279bef8164ae598c7e3a9abae5b65 100644 --- a/frontend/discover.js +++ b/frontend/discover.js @@ -1,12 +1,12 @@ -import React, { useState, useCallback, useEffect, useRef, useMemo } from "https://esm.sh/react@18.3.1"; -import { useQueryClient } from "https://esm.sh/@tanstack/react-query@5.59.20?deps=react@18.3.1"; -import { html, partnerGroupChipLabel, partnerGroupPartnerIdsTitle, DIRECTION_PILLS } from "./constants.js?v=33"; -import { KinkCard, ActionBar, SearchOverlay } from "./components.js?v=41"; -import { normalizeDiscoverSheetDirections, applyDiscoverDirectionChoice } from "./discover-flow.js?v=1"; -import { useRecsQuery, useSearchQuery, useDetailQuery } from "./queries.js?v=41"; -import { shouldRefetchEmptyDeck, visibleDiscoveryItems } from "./discover-state.js?v=33"; -import { api } from "./api.js?v=33"; -import { sendTelemetry } from "./telemetry.js?v=33"; +import React, { useState, useCallback, useEffect, useRef, useMemo } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { html, partnerGroupChipLabel, partnerGroupPartnerIdsTitle, DIRECTION_PILLS } from "./constants.js"; +import { KinkCard, ActionBar, SearchOverlay } from "./components.js"; +import { normalizeDiscoverSheetDirections, applyDiscoverDirectionChoice } from "./discover-flow.js"; +import { useRecsQuery, useSearchQuery, useDetailQuery } from "./queries.js"; +import { shouldRefetchEmptyDeck, visibleDiscoveryItems } from "./discover-state.js"; +import { api } from "./api.js"; +import { sendTelemetry } from "./telemetry.js"; export function DiscoverView({ auth, diff --git a/frontend/dist/assets/index-B1No5j6D.css b/frontend/dist/assets/index-B1No5j6D.css new file mode 100644 index 0000000000000000000000000000000000000000..1ff972fc0c1b4613326d624a024ede309f43ecab --- /dev/null +++ b/frontend/dist/assets/index-B1No5j6D.css @@ -0,0 +1 @@ +:root{--bg: #f5efe7;--bg-2: #efe5d7;--surface: rgba(255, 251, 246, .92);--surface-strong: #fffaf3;--ink: #1d1815;--muted: #6d6157;--line: #dbcdbd;--accent: #b33636;--accent-2: #264653;--accent-soft: #f0ddd0;--ok: #236b45;--radius: 18px;--radius-sm: 12px;--radius-pill: 999px;--tab-height: 56px}*{box-sizing:border-box}body{margin:0;color:var(--ink);font-family:IBM Plex Sans,Inter,system-ui,sans-serif;background:radial-gradient(circle at top left,rgba(179,54,54,.14),transparent 24%),radial-gradient(circle at bottom right,rgba(38,70,83,.12),transparent 22%),linear-gradient(180deg,var(--bg-2),var(--bg));min-height:100vh;padding-bottom:calc(var(--tab-height) + 12px)}button,input,textarea{font:inherit}button{border:0;border-radius:var(--radius-pill);background:var(--accent);color:#fff;padding:10px 16px;cursor:pointer;transition:transform .12s ease,opacity .12s ease;font-size:.88rem}button:hover{transform:translateY(-1px)}button:active{transform:scale(.97)}button.secondary{background:var(--accent-2)}button.ghost{background:transparent;color:var(--ink);border:1px solid var(--line)}button.sm{padding:6px 12px;font-size:.82rem}input,textarea{width:100%;border:1px solid var(--line);border-radius:14px;padding:12px 14px;background:var(--surface-strong);color:var(--ink)}.page{max-width:680px;margin:0 auto;padding:16px}.app-global-bar{display:flex;justify-content:flex-end;align-items:center;margin:0 0 10px;min-height:36px}.app-graph-debug-toggle--on{border-color:var(--accent-2);background:#2646531a;color:var(--accent-2)}.tab-bar{position:fixed;bottom:0;left:0;right:0;display:flex;justify-content:center;gap:0;height:var(--tab-height);background:var(--surface-strong);border-top:1px solid var(--line);z-index:90}.tab-btn{flex:1;max-width:180px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:transparent;color:var(--muted);border:none;border-radius:0;padding:4px 0;font-size:.72rem;font-weight:600;letter-spacing:.02em;transition:color .1s ease}.tab-btn:hover{transform:none}.tab-btn.active{color:var(--accent)}.tab-icon{font-size:1.3rem;line-height:1}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;border-radius:var(--radius-pill);background:var(--accent);color:#fff;font-size:.66rem;font-weight:700;padding:0 5px;position:absolute;top:4px;right:calc(50% - 20px)}.top-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:16px}.top-bar-title{font-size:1.4rem;font-weight:800;letter-spacing:-.03em;line-height:1}.top-bar-right{display:flex;align-items:center;gap:8px;font-size:.82rem;color:var(--muted)}.top-bar-right strong{color:var(--ink)}.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:20px;box-shadow:0 8px 24px #0000000a}.card-lg{padding:28px 24px;text-align:center}.card-title{margin:0 0 8px;font-size:1.8rem;font-weight:800;letter-spacing:-.03em;line-height:1.1}.card-definition{color:var(--muted);font-size:1rem;line-height:1.5;max-width:480px;margin:0 auto 16px}.kink-pill,.card-cluster{display:inline-block;padding:5px 11px;border-radius:var(--radius-pill);background:#26465314;color:var(--accent-2);font-size:.7rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin-bottom:12px}.card-image-area{width:100%;aspect-ratio:16 / 9;border-radius:var(--radius-sm);background:linear-gradient(135deg,#c9b8a7,#a89584);overflow:hidden;margin-bottom:16px;position:relative;cursor:pointer}.card-image-area img{width:100%;height:100%;object-fit:cover;filter:blur(20px);transition:filter .3s ease}.card-image-area.revealed img{filter:none}.card-counter{font-size:.84rem;color:var(--muted);font-weight:600}.card-image-area--preview{cursor:pointer}.card-image-area--preview .card-image-blur{width:100%;height:100%;object-fit:cover;filter:blur(20px);transform:scale(1.04);display:block}.card-image-cta{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;color:#fff;font-size:.95rem;font-weight:700;text-shadow:0 1px 8px rgba(0,0,0,.75);pointer-events:none}.card-image-scroll{max-height:340px;overflow-y:auto;overflow-x:hidden;scroll-snap-type:y mandatory;border-radius:var(--radius-sm);margin-bottom:16px;border:1px solid var(--line);-webkit-overflow-scrolling:touch;overscroll-behavior:contain;scrollbar-width:thin}.card-image-slide{min-height:220px;scroll-snap-align:start;scroll-snap-stop:always}.card-image-slide img{width:100%;height:100%;min-height:220px;object-fit:cover;display:block}.discover-card-wrap{margin:0 auto 10px;max-width:420px;border-radius:22px;box-shadow:0 12px 40px #0000001f}.discover-photo-hint{text-align:center;line-height:1.45;max-width:420px;margin:0 auto 8px;padding:0 8px}.discover-photo-hint strong{color:var(--ink);font-weight:800}.discover-reason-row{max-width:420px;margin:0 auto 8px;padding:0 8px}.discover-reason-pill{display:inline-flex;align-items:center;padding:6px 12px;border-radius:var(--radius-pill);background:#1d18150f;color:var(--ink);font-size:.78rem;font-weight:600;line-height:1.3}.discover-reason-pill--partner_influenced{background:#b336361f;color:var(--accent)}.discover-reason-pill--starter{background:#2646531a;color:var(--accent-2)}.discover-group-row{max-width:420px;margin:0 auto 14px;padding:0 4px}.discover-group-label{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:8px}.partner-group-picker{margin-bottom:8px}.discover-photo-toggle{display:flex;align-items:center;gap:10px;margin:0 auto 12px;max-width:420px;font-size:.86rem;color:var(--muted);cursor:pointer;-webkit-user-select:none;user-select:none}.discover-photo-toggle input{width:1.05rem;height:1.05rem;accent-color:var(--accent, #c62828)}.discover-directions{max-width:420px;margin:4px auto 6px;padding:0 10px}.discover-dir-label{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);margin-bottom:8px}.discover-direction-sheet{max-width:420px;margin:0 auto 14px;padding:14px;border:1px solid var(--line);border-radius:var(--radius);background:#ffffffe0;display:grid;gap:10px}.discover-direction-actions{display:flex;gap:8px;flex-wrap:wrap}.discover-scenario-hint{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.discover-scenario-hint strong{display:block;margin-bottom:2px}.tinder-card{background:var(--surface-strong);border:1px solid var(--line);border-radius:22px;overflow:hidden;text-align:left}.tinder-card__media{position:relative;background:linear-gradient(145deg,#3d3430,#1a1513)}.tinder-card__media-inner{position:relative;height:min(58vh,520px);height:min(58dvh,520px);min-height:280px;overflow:hidden}.tinder-card__carousel{position:relative;width:100%;height:100%}.tinder-card__slide--solo{height:100%}.tinder-card__slide--solo img{width:100%;height:100%;min-height:min(58vh,520px);min-height:min(58dvh,520px);object-fit:cover;display:block}.tinder-edge{position:absolute;top:0;bottom:calc(104px + env(safe-area-inset-bottom,0px));width:30%;max-width:150px;z-index:4;cursor:pointer;background:transparent;border:none;padding:0;-webkit-tap-highlight-color:transparent}.tinder-edge--left{left:0;background:linear-gradient(90deg,rgba(0,0,0,.14),transparent)}.tinder-edge--right{right:0;background:linear-gradient(270deg,rgba(0,0,0,.14),transparent)}.tinder-edge:active{opacity:.85}.tinder-dots{position:absolute;bottom:14px;left:50%;transform:translate(-50%);display:flex;gap:7px;z-index:5;pointer-events:none}.tinder-dots span{width:7px;height:7px;border-radius:50%;background:#ffffff61;transition:transform .12s ease,background .12s ease}.tinder-dots span.on{background:#fff;transform:scale(1.12)}.tinder-card__media-scroll{height:100%;overflow-y:auto;overflow-x:hidden;scroll-snap-type:y mandatory;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;scrollbar-width:thin}.tinder-card__slide{min-height:100%;scroll-snap-align:start;scroll-snap-stop:always;box-sizing:border-box}.tinder-card__slide img{width:100%;height:100%;min-height:min(58vh,520px);object-fit:cover;display:block}.tinder-card__img-blur{filter:blur(22px);transform:scale(1.06)}.tinder-card__media--placeholder{display:flex;align-items:center;justify-content:center;aspect-ratio:3 / 4;max-height:58vh;min-height:280px}.tinder-card__placeholder-letters{font-size:3.5rem;font-weight:800;color:#ffffff38;letter-spacing:-.06em}.tinder-card__photo-badge{position:absolute;top:12px;right:12px;left:auto;bottom:auto;transform:none;z-index:4;padding:5px 12px;border-radius:var(--radius-pill);background:#00000080;color:#fff;font-size:.74rem;font-weight:700;letter-spacing:.04em;pointer-events:none}.tinder-card__gradient{position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(180deg,transparent 38%,rgba(0,0,0,.55) 100%);pointer-events:none;z-index:2}.tinder-card__reveal-row{position:absolute;left:0;right:0;bottom:0;height:calc(104px + env(safe-area-inset-bottom,0px));display:flex;align-items:center;justify-content:center;z-index:6;pointer-events:none;box-sizing:border-box;padding-bottom:env(safe-area-inset-bottom,0px)}.tinder-card__reveal{position:relative;flex:0 0 auto;width:max-content;max-width:min(280px,calc(100% - 32px));margin:0;padding:11px 20px;border-radius:var(--radius-pill);border:1px solid rgba(255,255,255,.5);background:#00000080;color:#fff;font-size:.84rem;font-weight:700;cursor:pointer;box-shadow:0 6px 24px #00000059;touch-action:manipulation;-webkit-user-select:none;user-select:none;pointer-events:auto;outline:none}@media (hover: hover) and (pointer: fine){.tinder-card__reveal:hover{background:#0000009e}}.tinder-card__reveal:focus-visible{box-shadow:0 6px 24px #00000059,0 0 0 2px #ffffff73}.tinder-card__body{padding:16px 18px 20px}button.tinder-card__body.tinder-card__body--tappable{display:block;width:100%;border:none;margin:0;padding:16px 18px 20px;font:inherit;color:inherit;text-align:left;background:var(--surface-strong);-moz-appearance:none;appearance:none;-webkit-appearance:none;border-radius:0;box-sizing:border-box}.tinder-card__body--tappable{cursor:pointer;border-radius:0 0 18px 18px;transition:background .14s ease;touch-action:manipulation;-webkit-tap-highlight-color:transparent}.tinder-card__body--tappable:focus-visible{outline:none;box-shadow:inset 0 0 0 2px #6450c859}.tinder-card__body--tappable:active{background:#0000000b}.tinder-card__body--expanded{max-height:min(52vh,420px);overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}.tinder-card__body .kink-pill{margin-bottom:8px}.tinder-card__body .card-title{font-size:1.55rem;text-align:left;margin-bottom:6px}.tinder-card__body:not(.tinder-card__body--expanded) .card-definition{text-align:left;font-size:.92rem;line-height:1.45;display:block;overflow:visible;margin-bottom:0;color:var(--ink)}.tinder-card__body--expanded .card-definition--discover-full{text-align:left;font-size:.92rem;line-height:1.5;display:block;margin-bottom:0;overflow:visible;color:var(--ink)}.tinder-card__notes{margin-top:10px;font-size:.84rem;line-height:1.45;white-space:pre-wrap;word-break:break-word;color:#5c5c5c}.tinder-card__info-cue{margin-top:10px;font-size:.72rem;font-weight:600;letter-spacing:.02em;color:#888;text-align:center}.tinder-card .safety-badge{margin-top:8px}.action-bar{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;padding:16px 8px;max-width:520px;margin:0 auto}.action-btn{display:flex;flex-direction:column;align-items:center;gap:4px;padding:12px 20px;border-radius:16px;border:1px solid var(--line);background:#fff;color:var(--ink);font-size:.76rem;font-weight:600;min-width:64px;transition:transform .1s ease,background .1s ease,border-color .1s ease}.action-btn:hover{transform:translateY(-2px)}.action-btn:active{transform:scale(.94)}.action-btn .action-icon{font-size:1.4rem;line-height:1}.action-btn.love{border-color:#e57373;background:#fff5f5}.action-btn.like{border-color:#81c784;background:#f5fff5}.action-btn.maybe{border-color:#b39ddb;background:#f3e5f5}.action-btn.skip{border-color:var(--line)}.action-btn.nogo{border-color:#bbb;background:#fafafa}.chip-flow{display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start}.chip-flow--with-dnd{flex-direction:column;align-items:stretch}.plays-board-card--inline{width:100%}.play-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#ffffffd1;font-size:.88rem;font-weight:600;cursor:pointer;transition:border-color .1s ease,box-shadow .1s ease;max-width:100%}.chip-top-row{display:inline-flex;align-items:center;gap:6px;min-width:0;flex:1}.chip-near-title{flex-shrink:0;opacity:.55;font-size:.85em;font-weight:600;cursor:help}.play-chip:hover{border-color:var(--accent)}.play-chip .chip-emoji{flex-shrink:0}.play-chip .chip-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.play-chip.expanded{width:100%;max-width:100%;min-width:0;box-sizing:border-box;flex-direction:column;align-items:stretch;gap:10px;padding:12px;background:#fff;border-color:var(--accent);box-shadow:0 4px 16px #0000000f}.play-chip.expanded .chip-top-row{align-items:flex-start}.play-chip.expanded .chip-name{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;text-overflow:ellipsis;white-space:normal;word-break:break-word}.plays-inspector{scroll-margin-top:12px}.plays-inspector-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.plays-inspector-copy{min-width:0}.plays-inspector-label{text-transform:uppercase;letter-spacing:.05em;font-weight:700;color:var(--muted)}.plays-inspector-title-row{display:flex;align-items:flex-start;gap:8px;min-width:0}.plays-inspector-title{margin:0;font-size:1.2rem;line-height:1.2;overflow-wrap:anywhere;word-break:break-word}.plays-inspector-emoji{flex-shrink:0;font-size:1.2rem;line-height:1}.plays-inspector-definition{margin:0}.plays-inspector-actions,.plays-inspector-actions-row,.plays-inspector-section{display:grid;gap:10px}.inline-detail{display:grid;gap:10px;min-width:0;max-width:100%}.inline-detail .detail-def{font-size:.9rem;line-height:1.45;color:var(--muted);font-weight:400;overflow-wrap:anywhere;word-break:break-word}.scenario-panel{display:grid;gap:8px;padding:10px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#00000005}.scenario-panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.scenario-panel-mother{display:grid;gap:2px;min-width:0}.scenario-panel-mother-label{text-transform:uppercase;letter-spacing:.05em;font-weight:700;color:var(--muted)}.scenario-panel-mother-name{font-size:.95rem;font-weight:700;line-height:1.35;overflow-wrap:anywhere;word-break:break-word}.scenario-panel-hint{margin:0 0 4px;line-height:1.4}.scenario-list{display:grid;gap:8px}.scenario-list--page{gap:10px}.scenario-row{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px;min-width:0}.scenario-row-text{display:grid;gap:2px;min-width:0}.scenario-name{font-size:.84rem;font-weight:650;line-height:1.4;overflow-wrap:anywhere;word-break:break-word;white-space:normal}.scenario-match-chip .chip-name{white-space:normal;overflow-wrap:anywhere}.scenario-row--page{padding:12px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#ffffffd1}.scenario-row--highlight{border-color:var(--accent);box-shadow:0 0 0 2px #b336361f}.scenario-row-actions{display:flex;align-items:center;justify-content:flex-end}.scenarios-parent-head,.scenarios-detail-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}.scenarios-parent-search{max-width:240px}.scenarios-parent-list{display:grid;gap:8px}.scenarios-parent-item{width:100%;display:grid;gap:3px;justify-items:start;text-align:left;border-radius:var(--radius-sm);border:1px solid var(--line);background:#ffffffd1;color:var(--ink);padding:12px 14px}.scenarios-parent-item.active{border-color:var(--accent);background:#fff}.scenarios-parent-name{font-weight:700;overflow-wrap:anywhere;word-break:break-word}.scenarios-parent-meta{font-size:.78rem;color:var(--muted)}.scenarios-detail-label{text-transform:uppercase;letter-spacing:.05em;font-weight:700;color:var(--muted)}.scenarios-detail-title{margin:4px 0;line-height:1.15}@media (max-width: 560px){.discover-scenario-hint{align-items:stretch;flex-direction:column}.scenario-row{grid-template-columns:1fr}.scenario-row-actions{justify-content:flex-start}}.dir-pills{display:flex;gap:6px;flex-wrap:wrap}.dir-pill{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border:1px solid var(--line);border-radius:var(--radius-pill);background:#fff;color:var(--ink);font-size:.8rem;font-weight:500;touch-action:manipulation;-webkit-tap-highlight-color:transparent}.dir-pill.active{background:var(--accent-2);color:#fff;border-color:transparent}.dir-pill:disabled{opacity:.55;cursor:not-allowed}.discover-dir-strip{margin:.65rem 0 .35rem;padding:0 2px}.discover-dir-strip__label{margin-bottom:6px;text-align:center;line-height:1.35}.discover-dir-strip__pills{justify-content:center}.reaction-bar{display:flex;gap:6px;flex-wrap:wrap}.react-btn{display:inline-flex;align-items:center;justify-content:center;padding:5px 8px;border-radius:var(--radius-pill);border:1px solid var(--line);background:#fff;font-size:.9rem;min-width:32px;min-height:32px;transition:transform 80ms ease,background 80ms ease}.react-btn:active{transform:scale(.92)}.react-btn.active{background:var(--accent-soft);border-color:transparent}.section-header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-title{margin:0;font-size:.88rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}.section-count{font-size:.76rem;color:var(--muted);font-weight:600}.plays-direction-board{margin-bottom:20px}.plays-board-hint{margin:0 0 12px;line-height:1.45}.plays-board-columns{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;align-items:stretch}.plays-board-columns>.plays-board-column{min-width:0}@media (max-width: 780px){.plays-board-columns{grid-template-columns:1fr}.plays-board-card{flex-wrap:wrap;align-items:flex-start}.plays-board-card .plays-dnd-handle{margin-top:4px}.plays-board-card-body{flex:1 1 calc(100% - 48px);min-width:0}.plays-touch-moves{display:flex;order:3}}.plays-board-column{display:flex;flex-direction:column;min-height:140px;border:2px dashed var(--line);border-radius:var(--radius);background:#ffffff8c;padding:8px;transition:border-color .12s ease,background .12s ease,box-shadow .12s ease}.plays-board-column--over{border-color:var(--accent);background:#ffecec73;box-shadow:0 0 0 1px #c6282826}.plays-board-column-head{display:flex;align-items:center;gap:6px;margin-bottom:8px;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}.plays-board-column-icon{font-size:1rem}.plays-board-column-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:8px}.plays-board-empty{flex:1;display:flex;align-items:center;justify-content:center;padding:16px 8px;text-align:center;color:var(--muted);opacity:.9}.plays-board-card{display:flex;gap:6px;align-items:flex-start;min-width:0;max-width:100%;width:100%;box-sizing:border-box}.plays-dnd-handle{flex-shrink:0;cursor:grab;-webkit-user-select:none;user-select:none;padding:4px 6px;margin-top:2px;border-radius:6px;color:var(--muted);font-size:1rem;line-height:1;touch-action:none}.plays-dnd-handle:hover{background:#0000000a;color:var(--ink)}.plays-dnd-handle:active{cursor:grabbing}.plays-touch-moves{display:none;flex:1 1 100%;flex-wrap:wrap;gap:6px;align-items:stretch;margin-top:2px}.plays-touch-move-btn{flex:1 1 auto;min-width:0;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:8px 6px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#ffffffeb;font-size:.72rem;font-weight:600;color:var(--ink);cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent}.plays-touch-move-btn:active{background:#c6282814}.plays-touch-move-icon{font-size:.95rem;line-height:1}.plays-board-card-body{flex:1;min-width:0;overflow-x:hidden}.plays-board-card-body .play-chip,.plays-board-card-body .inline-detail .reaction-bar,.plays-board-card-body .inline-detail .dir-pills,.plays-board-card-body .inline-detail .chips{max-width:100%}.inline-detail .chips .chip{max-width:100%;min-width:0;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;text-overflow:ellipsis;line-height:1.35;text-align:left;white-space:normal;word-break:break-word}.plays-board-card-body .inline-detail button.ghost.sm{max-width:100%;justify-self:start}.plays-board-card-body .play-assets,.plays-board-card-body .play-assets-carousel{max-width:100%;box-sizing:border-box}.collapse-toggle{all:unset;cursor:pointer;display:flex;align-items:center;gap:6px;font-size:.84rem;font-weight:600;color:var(--muted);padding:6px 0}.collapse-toggle .arrow{transition:transform .2s ease}.collapse-toggle.open .arrow{transform:rotate(90deg)}.partner-code{display:flex;align-items:center;gap:10px;padding:12px 14px;border:1px solid var(--line);border-radius:var(--radius);background:#ffffffb8}.partner-code code{font-size:1.1rem;font-weight:700;flex:1;overflow-wrap:anywhere}.sub-tabs{display:flex;gap:0;border-bottom:2px solid var(--line);margin-bottom:12px}.sub-tab{padding:8px 16px;background:transparent;border:none;border-bottom:2px solid transparent;border-radius:0;color:var(--muted);font-weight:600;font-size:.84rem;margin-bottom:-2px;transition:color .1s,border-color .1s}.sub-tab:hover{transform:none;color:var(--ink)}.sub-tab.active{color:var(--accent);border-bottom-color:var(--accent)}.match-indicator{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:var(--radius-pill);font-size:.74rem;font-weight:600}.match-indicator.perfect{background:#e8f5e9;color:#2e7d32}.match-indicator.partial{background:#fff3e0;color:#e65100}.together-scenario-card{flex-direction:column;align-items:stretch;gap:10px}.together-scenario-copy{display:grid;gap:4px;min-width:0}.together-mother-label,.together-scenario-label{text-transform:uppercase;letter-spacing:.05em;font-weight:700;color:var(--muted);font-size:.68rem}.together-mother-name{font-size:.95rem;font-weight:700;line-height:1.35;overflow-wrap:anywhere;word-break:break-word}button.together-mother-link{all:unset;cursor:pointer;font-size:.95rem;font-weight:700;line-height:1.35;overflow-wrap:anywhere;word-break:break-word;color:var(--accent);text-decoration:underline;text-underline-offset:2px}button.together-mother-link:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.together-scenario-title{font-size:.88rem;font-weight:650;line-height:1.45;overflow-wrap:anywhere;word-break:break-word;white-space:normal}.together-scenario-meta{display:flex;flex-direction:column;gap:8px;min-width:0}.together-scenario-you{display:grid;gap:4px;padding-top:4px;border-top:1px solid var(--line)}.onboard{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px;text-align:center}.onboard .card{width:100%;max-width:440px;text-align:left}.onboard h1{font-size:2.4rem;letter-spacing:-.04em;line-height:1;margin:0 0 8px}.onboard .sub{color:var(--muted);margin:0 0 24px;font-size:.96rem}.starter-page{padding-top:24px}.starter-hero{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}.starter-hero .sub{margin:8px 0 0;max-width:520px}.creds{border:1px solid var(--line);border-radius:var(--radius);padding:14px;background:#ffffffb8}.creds code{display:block;margin-top:6px;padding:8px 10px;border-radius:var(--radius-sm);background:#f5eee4;overflow-wrap:anywhere;font-weight:600}.settings-code{display:inline-block;margin-top:6px;padding:8px 10px;border-radius:var(--radius-sm);background:#f5eee4;overflow-wrap:anywhere;font-weight:600}.settings-toggle{margin:0;max-width:none}.settings-share-row{padding:10px 12px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#ffffffb8}.settings-share-title{font-size:.9rem;font-weight:700;margin-bottom:4px}.search-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:80;background:#1d18156b;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);display:flex;flex-direction:column;align-items:center;padding:16px}.search-overlay .search-inner{width:100%;max-width:560px;display:grid;gap:12px}.search-overlay input{font-size:1.1rem;padding:14px 16px}.play-assets{width:100%}.play-assets-carousel{position:relative;border-radius:var(--radius-sm);border:1px solid var(--line);overflow:hidden;background:var(--surface)}.play-assets-carousel .play-assets-slide{min-height:200px;max-height:min(48vh,340px)}.play-assets-carousel .play-assets-slide img{width:100%;height:100%;min-height:200px;max-height:min(48vh,340px);object-fit:cover;display:block}.play-edge{position:absolute;top:0;bottom:0;width:28%;max-width:120px;z-index:2;cursor:pointer;border:none;padding:0;background:transparent;-webkit-tap-highlight-color:transparent}.play-edge--left{left:0;background:linear-gradient(90deg,rgba(0,0,0,.08),transparent)}.play-edge--right{right:0;background:linear-gradient(270deg,rgba(0,0,0,.08),transparent)}.play-assets-dots{position:absolute;bottom:10px;left:50%;transform:translate(-50%);display:flex;gap:6px;z-index:3;pointer-events:none}.play-assets-dots span{width:6px;height:6px;border-radius:50%;background:#ffffff73}.play-assets-dots span.on{background:#fff}.play-assets-badge{position:absolute;top:8px;right:8px;z-index:3;padding:4px 10px;border-radius:var(--radius-pill);background:#0000007a;color:#fff;font-size:.72rem;font-weight:700;pointer-events:none}.image-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.image-tile{position:relative;aspect-ratio:1;background:#c9b8a7;border-radius:var(--radius-sm);overflow:hidden}.image-tile img{width:100%;height:100%;object-fit:cover}.image-tile .reveal-btn{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:0;background:linear-gradient(180deg,#0000001f,#000000b3);display:flex;align-items:center;justify-content:center;font-size:.82rem}.safety-badge{padding:4px 8px;border-radius:8px;font-size:.78rem;font-weight:600;display:inline-block}.safety-badge.extreme{background:#ffebee;color:#c62828;border:1px solid #ef9a9a}.safety-badge.advanced{background:#fff3e0;color:#e65100;border:1px solid #ffcc80}.stack{display:grid;gap:12px}.stack-sm{display:grid;gap:8px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.muted{color:var(--muted)}.tiny{font-size:.82rem;color:var(--muted)}.empty{color:var(--muted);border:1px dashed rgba(219,205,189,.7);border-radius:14px;padding:10px 12px;background:#ffffff59;font-size:.88rem}.chips{display:flex;gap:6px;flex-wrap:wrap}.chip{border:1px solid var(--line);border-radius:var(--radius-pill);padding:5px 10px;font-size:.82rem;background:#fff;color:var(--ink);cursor:pointer;font-weight:500}.chip.active{border-color:var(--accent);background:var(--accent-soft);color:var(--accent);font-weight:600}.status{min-height:1.2em;color:var(--muted);font-size:.86rem}.together-scope-hint{margin:0 0 10px;color:var(--muted)}.together-section-label{margin:8px 0 2px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}.related-match-chip{align-items:flex-start}.related-arrow{color:var(--muted);font-weight:700}.related-match-meta{flex:1 1 240px;color:var(--muted)}.their-board-picker{margin-bottom:12px}.together-react-row{display:inline-flex;flex-wrap:wrap;gap:6px;align-items:center;justify-content:flex-end;max-width:100%}.together-react-pair{display:inline-flex;gap:3px;align-items:center}.together-react-id{font-family:ui-monospace,monospace;font-size:.75em;opacity:.85}@media (min-width: 768px){.page{max-width:720px;padding:24px}.action-btn{padding:14px 28px;min-width:80px;font-size:.82rem}.action-btn .action-icon{font-size:1.6rem}.card-lg{padding:36px 32px}.card-title{font-size:2.2rem}}.desktop-nav{display:none}@media (min-width: 1024px){.page{max-width:880px}.tab-bar{display:none}body{padding-bottom:0}.desktop-nav{display:flex;gap:0;margin-bottom:20px;border-bottom:2px solid var(--line)}.desktop-nav .tab-btn{flex-direction:row;gap:6px;padding:10px 20px;font-size:.88rem;max-width:none;border-bottom:2px solid transparent;margin-bottom:-2px}.desktop-nav .tab-btn.active{border-bottom-color:var(--accent);color:var(--accent)}} diff --git a/frontend/dist/assets/index-CselhGyO.js b/frontend/dist/assets/index-CselhGyO.js new file mode 100644 index 0000000000000000000000000000000000000000..25adda4bfce8c48c1cc0f49a327c86a9692290a4 --- /dev/null +++ b/frontend/dist/assets/index-CselhGyO.js @@ -0,0 +1,1442 @@ +var bu=e=>{throw TypeError(e)};var Wo=(e,t,n)=>t.has(e)||bu("Cannot "+n);var y=(e,t,n)=>(Wo(e,t,"read from private field"),n?n.call(e):t.get(e)),b=(e,t,n)=>t.has(e)?bu("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),O=(e,t,n,r)=>(Wo(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),K=(e,t,n)=>(Wo(e,t,"access private method"),n);var vs=(e,t,n,r)=>({set _(i){O(e,t,i,n)},get _(){return y(e,t,r)}});(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();function wh(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Id={exports:{}},V={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var as=Symbol.for("react.element"),_h=Symbol.for("react.portal"),Nh=Symbol.for("react.fragment"),Ch=Symbol.for("react.strict_mode"),Eh=Symbol.for("react.profiler"),Ih=Symbol.for("react.provider"),Ph=Symbol.for("react.context"),Th=Symbol.for("react.forward_ref"),xh=Symbol.for("react.suspense"),Rh=Symbol.for("react.memo"),Dh=Symbol.for("react.lazy"),ju=Symbol.iterator;function Oh(e){return e===null||typeof e!="object"?null:(e=ju&&e[ju]||e["@@iterator"],typeof e=="function"?e:null)}var Pd={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Td=Object.assign,xd={};function ii(e,t,n){this.props=e,this.context=t,this.refs=xd,this.updater=n||Pd}ii.prototype.isReactComponent={};ii.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};ii.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Rd(){}Rd.prototype=ii.prototype;function Ra(e,t,n){this.props=e,this.context=t,this.refs=xd,this.updater=n||Pd}var Da=Ra.prototype=new Rd;Da.constructor=Ra;Td(Da,ii.prototype);Da.isPureReactComponent=!0;var Qu=Array.isArray,Dd=Object.prototype.hasOwnProperty,Oa={current:null},Od={key:!0,ref:!0,__self:!0,__source:!0};function Ld(e,t,n){var r,i={},s=null,o=null;if(t!=null)for(r in t.ref!==void 0&&(o=t.ref),t.key!==void 0&&(s=""+t.key),t)Dd.call(t,r)&&!Od.hasOwnProperty(r)&&(i[r]=t[r]);var l=arguments.length-2;if(l===1)i.children=n;else if(1>>1,A=D[W];if(0>>1;Wi(q,Q))Sei(xe,q)?(D[W]=xe,D[Se]=Q,W=Se):(D[W]=q,D[re]=Q,W=re);else if(Sei(xe,Q))D[W]=xe,D[Se]=Q,W=Se;else break e}}return F}function i(D,F){var Q=D.sortIndex-F.sortIndex;return Q!==0?Q:D.id-F.id}if(typeof performance=="object"&&typeof performance.now=="function"){var s=performance;e.unstable_now=function(){return s.now()}}else{var o=Date,l=o.now();e.unstable_now=function(){return o.now()-l}}var a=[],u=[],h=1,f=null,d=3,m=!1,$=!1,k=!1,w=typeof setTimeout=="function"?setTimeout:null,p=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function v(D){for(var F=n(u);F!==null;){if(F.callback===null)r(u);else if(F.startTime<=D)r(u),F.sortIndex=F.expirationTime,t(a,F);else break;F=n(u)}}function g(D){if(k=!1,v(D),!$)if(n(a)!==null)$=!0,Be(_);else{var F=n(u);F!==null&&Me(g,F.startTime-D)}}function _(D,F){$=!1,k&&(k=!1,p(C),C=-1),m=!0;var Q=d;try{for(v(F),f=n(a);f!==null&&(!(f.expirationTime>F)||D&&!H());){var W=f.callback;if(typeof W=="function"){f.callback=null,d=f.priorityLevel;var A=W(f.expirationTime<=F);F=e.unstable_now(),typeof A=="function"?f.callback=A:f===n(a)&&r(a),v(F)}else r(a);f=n(a)}if(f!==null)var ie=!0;else{var re=n(u);re!==null&&Me(g,re.startTime-F),ie=!1}return ie}finally{f=null,d=Q,m=!1}}var I=!1,E=null,C=-1,P=5,T=-1;function H(){return!(e.unstable_now()-TD||125W?(D.sortIndex=Q,t(u,D),n(a)===null&&D===n(u)&&(k?(p(C),C=-1):k=!0,Me(g,Q-W))):(D.sortIndex=A,t(a,D),$||m||($=!0,Be(_))),D},e.unstable_shouldYield=H,e.unstable_wrapCallback=function(D){var F=d;return function(){var Q=d;d=F;try{return D.apply(this,arguments)}finally{d=Q}}}})(bd);zd.exports=bd;var jh=zd.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Qh=N,dt=jh;function x(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),_l=Object.prototype.hasOwnProperty,Ah=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Uu={},Bu={};function Uh(e){return _l.call(Bu,e)?!0:_l.call(Uu,e)?!1:Ah.test(e)?Bu[e]=!0:(Uu[e]=!0,!1)}function Bh(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Kh(e,t,n,r){if(t===null||typeof t>"u"||Bh(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function Ge(e,t,n,r,i,s,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=i,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=s,this.removeEmptyString=o}var Oe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){Oe[e]=new Ge(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];Oe[t]=new Ge(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){Oe[e]=new Ge(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Oe[e]=new Ge(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){Oe[e]=new Ge(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){Oe[e]=new Ge(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){Oe[e]=new Ge(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){Oe[e]=new Ge(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){Oe[e]=new Ge(e,5,!1,e.toLowerCase(),null,!1,!1)});var Ma=/[\-:]([a-z])/g;function Fa(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Ma,Fa);Oe[t]=new Ge(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Ma,Fa);Oe[t]=new Ge(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Ma,Fa);Oe[t]=new Ge(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){Oe[e]=new Ge(e,1,!1,e.toLowerCase(),null,!1,!1)});Oe.xlinkHref=new Ge("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){Oe[e]=new Ge(e,1,!1,e.toLowerCase(),null,!0,!0)});function za(e,t,n,r){var i=Oe.hasOwnProperty(t)?Oe[t]:null;(i!==null?i.type!==0:r||!(2l||i[o]!==s[l]){var a=` +`+i[o].replace(" at new "," at ");return e.displayName&&a.includes("")&&(a=a.replace("",e.displayName)),a}while(1<=o&&0<=l);break}}}finally{Jo=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?wi(e):""}function Hh(e){switch(e.tag){case 5:return wi(e.type);case 16:return wi("Lazy");case 13:return wi("Suspense");case 19:return wi("SuspenseList");case 0:case 2:case 15:return e=Xo(e.type,!1),e;case 11:return e=Xo(e.type.render,!1),e;case 1:return e=Xo(e.type,!0),e;default:return""}}function Il(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case $r:return"Fragment";case kr:return"Portal";case Nl:return"Profiler";case ba:return"StrictMode";case Cl:return"Suspense";case El:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Ad:return(e.displayName||"Context")+".Consumer";case Qd:return(e._context.displayName||"Context")+".Provider";case ja:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Qa:return t=e.displayName||null,t!==null?t:Il(e.type)||"Memo";case ln:t=e._payload,e=e._init;try{return Il(e(t))}catch{}}return null}function Vh(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Il(t);case 8:return t===ba?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Rn(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Bd(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function qh(e){var t=Bd(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var i=n.get,s=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(o){r=""+o,s.call(this,o)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(o){r=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function ks(e){e._valueTracker||(e._valueTracker=qh(e))}function Kd(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Bd(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Ys(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Pl(e,t){var n=t.checked;return he({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Hu(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Rn(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Hd(e,t){t=t.checked,t!=null&&za(e,"checked",t,!1)}function Tl(e,t){Hd(e,t);var n=Rn(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?xl(e,t.type,n):t.hasOwnProperty("defaultValue")&&xl(e,t.type,Rn(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Vu(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function xl(e,t,n){(t!=="number"||Ys(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var _i=Array.isArray;function Rr(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i"+t.valueOf().toString()+"",t=$s.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function bi(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Ii={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Wh=["Webkit","ms","Moz","O"];Object.keys(Ii).forEach(function(e){Wh.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Ii[t]=Ii[e]})});function Gd(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Ii.hasOwnProperty(e)&&Ii[e]?(""+t).trim():t+"px"}function Yd(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,i=Gd(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,i):e[n]=i}}var Gh=he({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Ol(e,t){if(t){if(Gh[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(x(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(x(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(x(61))}if(t.style!=null&&typeof t.style!="object")throw Error(x(62))}}function Ll(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Ml=null;function Aa(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Fl=null,Dr=null,Or=null;function Gu(e){if(e=ds(e)){if(typeof Fl!="function")throw Error(x(280));var t=e.stateNode;t&&(t=Po(t),Fl(e.stateNode,e.type,t))}}function Jd(e){Dr?Or?Or.push(e):Or=[e]:Dr=e}function Xd(){if(Dr){var e=Dr,t=Or;if(Or=Dr=null,Gu(e),t)for(e=0;e>>=0,e===0?32:31-(om(e)/lm|0)|0}var Ss=64,ws=4194304;function Ni(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function eo(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,i=e.suspendedLanes,s=e.pingedLanes,o=n&268435455;if(o!==0){var l=o&~i;l!==0?r=Ni(l):(s&=o,s!==0&&(r=Ni(s)))}else o=n&~i,o!==0?r=Ni(o):s!==0&&(r=Ni(s));if(r===0)return 0;if(t!==0&&t!==r&&!(t&i)&&(i=r&-r,s=t&-t,i>=s||i===16&&(s&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function us(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-It(t),e[t]=n}function dm(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Ti),ic=" ",sc=!1;function kf(e,t){switch(e){case"keyup":return jm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function $f(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Sr=!1;function Am(e,t){switch(e){case"compositionend":return $f(t);case"keypress":return t.which!==32?null:(sc=!0,ic);case"textInput":return e=t.data,e===ic&&sc?null:e;default:return null}}function Um(e,t){if(Sr)return e==="compositionend"||!Ga&&kf(e,t)?(e=yf(),js=Va=$n=null,Sr=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=uc(n)}}function Nf(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Nf(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Cf(){for(var e=window,t=Ys();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Ys(e.document)}return t}function Ya(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Jm(e){var t=Cf(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Nf(n.ownerDocument.documentElement,n)){if(r!==null&&Ya(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=n.textContent.length,s=Math.min(r.start,i);r=r.end===void 0?s:Math.min(r.end,i),!e.extend&&s>r&&(i=r,r=s,s=i),i=cc(n,s);var o=cc(n,r);i&&o&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),s>r?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,wr=null,Ul=null,Ri=null,Bl=!1;function dc(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Bl||wr==null||wr!==Ys(r)||(r=wr,"selectionStart"in r&&Ya(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Ri&&Ki(Ri,r)||(Ri=r,r=ro(Ul,"onSelect"),0Cr||(e.current=Gl[Cr],Gl[Cr]=null,Cr--)}function oe(e,t){Cr++,Gl[Cr]=e.current,e.current=t}var Dn={},Ue=Ln(Dn),tt=Ln(!1),rr=Dn;function Xr(e,t){var n=e.type.contextTypes;if(!n)return Dn;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},s;for(s in n)i[s]=t[s];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function nt(e){return e=e.childContextTypes,e!=null}function so(){ue(tt),ue(Ue)}function gc(e,t,n){if(Ue.current!==Dn)throw Error(x(168));oe(Ue,t),oe(tt,n)}function Lf(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var i in r)if(!(i in t))throw Error(x(108,Vh(e)||"Unknown",i));return he({},n,r)}function oo(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Dn,rr=Ue.current,oe(Ue,e),oe(tt,tt.current),!0}function kc(e,t,n){var r=e.stateNode;if(!r)throw Error(x(169));n?(e=Lf(e,t,rr),r.__reactInternalMemoizedMergedChildContext=e,ue(tt),ue(Ue),oe(Ue,e)):ue(tt),oe(tt,n)}var Ut=null,To=!1,fl=!1;function Mf(e){Ut===null?Ut=[e]:Ut.push(e)}function uv(e){To=!0,Mf(e)}function Mn(){if(!fl&&Ut!==null){fl=!0;var e=0,t=ee;try{var n=Ut;for(ee=1;e>=o,i-=o,qt=1<<32-It(t)+i|n<C?(P=E,E=null):P=E.sibling;var T=d(p,E,v[C],g);if(T===null){E===null&&(E=P);break}e&&E&&T.alternate===null&&t(p,E),c=s(T,c,C),I===null?_=T:I.sibling=T,I=T,E=P}if(C===v.length)return n(p,E),de&&Qn(p,C),_;if(E===null){for(;CC?(P=E,E=null):P=E.sibling;var H=d(p,E,T.value,g);if(H===null){E===null&&(E=P);break}e&&E&&H.alternate===null&&t(p,E),c=s(H,c,C),I===null?_=H:I.sibling=H,I=H,E=P}if(T.done)return n(p,E),de&&Qn(p,C),_;if(E===null){for(;!T.done;C++,T=v.next())T=f(p,T.value,g),T!==null&&(c=s(T,c,C),I===null?_=T:I.sibling=T,I=T);return de&&Qn(p,C),_}for(E=r(p,E);!T.done;C++,T=v.next())T=m(E,p,C,T.value,g),T!==null&&(e&&T.alternate!==null&&E.delete(T.key===null?C:T.key),c=s(T,c,C),I===null?_=T:I.sibling=T,I=T);return e&&E.forEach(function(J){return t(p,J)}),de&&Qn(p,C),_}function w(p,c,v,g){if(typeof v=="object"&&v!==null&&v.type===$r&&v.key===null&&(v=v.props.children),typeof v=="object"&&v!==null){switch(v.$$typeof){case gs:e:{for(var _=v.key,I=c;I!==null;){if(I.key===_){if(_=v.type,_===$r){if(I.tag===7){n(p,I.sibling),c=i(I,v.props.children),c.return=p,p=c;break e}}else if(I.elementType===_||typeof _=="object"&&_!==null&&_.$$typeof===ln&&wc(_)===I.type){n(p,I.sibling),c=i(I,v.props),c.ref=ki(p,I,v),c.return=p,p=c;break e}n(p,I);break}else t(p,I);I=I.sibling}v.type===$r?(c=nr(v.props.children,p.mode,g,v.key),c.return=p,p=c):(g=qs(v.type,v.key,v.props,null,p.mode,g),g.ref=ki(p,c,v),g.return=p,p=g)}return o(p);case kr:e:{for(I=v.key;c!==null;){if(c.key===I)if(c.tag===4&&c.stateNode.containerInfo===v.containerInfo&&c.stateNode.implementation===v.implementation){n(p,c.sibling),c=i(c,v.children||[]),c.return=p,p=c;break e}else{n(p,c);break}else t(p,c);c=c.sibling}c=$l(v,p.mode,g),c.return=p,p=c}return o(p);case ln:return I=v._init,w(p,c,I(v._payload),g)}if(_i(v))return $(p,c,v,g);if(hi(v))return k(p,c,v,g);Ts(p,v)}return typeof v=="string"&&v!==""||typeof v=="number"?(v=""+v,c!==null&&c.tag===6?(n(p,c.sibling),c=i(c,v),c.return=p,p=c):(n(p,c),c=kl(v,p.mode,g),c.return=p,p=c),o(p)):n(p,c)}return w}var ei=jf(!0),Qf=jf(!1),uo=Ln(null),co=null,Pr=null,eu=null;function tu(){eu=Pr=co=null}function nu(e){var t=uo.current;ue(uo),e._currentValue=t}function Xl(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function Mr(e,t){co=e,eu=Pr=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(et=!0),e.firstContext=null)}function $t(e){var t=e._currentValue;if(eu!==e)if(e={context:e,memoizedValue:t,next:null},Pr===null){if(co===null)throw Error(x(308));Pr=e,co.dependencies={lanes:0,firstContext:e}}else Pr=Pr.next=e;return t}var Bn=null;function ru(e){Bn===null?Bn=[e]:Bn.push(e)}function Af(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,ru(t)):(n.next=i.next,i.next=n),t.interleaved=n,Zt(e,r)}function Zt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var an=!1;function iu(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Uf(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Gt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function En(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Y&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,Zt(e,n)}return i=r.interleaved,i===null?(t.next=t,ru(r)):(t.next=i.next,i.next=t),r.interleaved=t,Zt(e,n)}function As(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ba(e,n)}}function _c(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,s=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};s===null?i=s=o:s=s.next=o,n=n.next}while(n!==null);s===null?i=s=t:s=s.next=t}else i=s=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:s,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function fo(e,t,n,r){var i=e.updateQueue;an=!1;var s=i.firstBaseUpdate,o=i.lastBaseUpdate,l=i.shared.pending;if(l!==null){i.shared.pending=null;var a=l,u=a.next;a.next=null,o===null?s=u:o.next=u,o=a;var h=e.alternate;h!==null&&(h=h.updateQueue,l=h.lastBaseUpdate,l!==o&&(l===null?h.firstBaseUpdate=u:l.next=u,h.lastBaseUpdate=a))}if(s!==null){var f=i.baseState;o=0,h=u=a=null,l=s;do{var d=l.lane,m=l.eventTime;if((r&d)===d){h!==null&&(h=h.next={eventTime:m,lane:0,tag:l.tag,payload:l.payload,callback:l.callback,next:null});e:{var $=e,k=l;switch(d=t,m=n,k.tag){case 1:if($=k.payload,typeof $=="function"){f=$.call(m,f,d);break e}f=$;break e;case 3:$.flags=$.flags&-65537|128;case 0:if($=k.payload,d=typeof $=="function"?$.call(m,f,d):$,d==null)break e;f=he({},f,d);break e;case 2:an=!0}}l.callback!==null&&l.lane!==0&&(e.flags|=64,d=i.effects,d===null?i.effects=[l]:d.push(l))}else m={eventTime:m,lane:d,tag:l.tag,payload:l.payload,callback:l.callback,next:null},h===null?(u=h=m,a=f):h=h.next=m,o|=d;if(l=l.next,l===null){if(l=i.shared.pending,l===null)break;d=l,l=d.next,d.next=null,i.lastBaseUpdate=d,i.shared.pending=null}}while(!0);if(h===null&&(a=f),i.baseState=a,i.firstBaseUpdate=u,i.lastBaseUpdate=h,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else s===null&&(i.shared.lanes=0);or|=o,e.lanes=o,e.memoizedState=f}}function Nc(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=hl.transition;hl.transition={};try{e(!1),t()}finally{ee=n,hl.transition=r}}function sp(){return St().memoizedState}function pv(e,t,n){var r=Pn(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},op(e))lp(t,n);else if(n=Af(e,t,n,r),n!==null){var i=qe();Pt(n,e,r,i),ap(n,t,r)}}function hv(e,t,n){var r=Pn(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(op(e))lp(t,i);else{var s=e.alternate;if(e.lanes===0&&(s===null||s.lanes===0)&&(s=t.lastRenderedReducer,s!==null))try{var o=t.lastRenderedState,l=s(o,n);if(i.hasEagerState=!0,i.eagerState=l,Tt(l,o)){var a=t.interleaved;a===null?(i.next=i,ru(t)):(i.next=a.next,a.next=i),t.interleaved=i;return}}catch{}finally{}n=Af(e,t,i,r),n!==null&&(i=qe(),Pt(n,e,r,i),ap(n,t,r))}}function op(e){var t=e.alternate;return e===pe||t!==null&&t===pe}function lp(e,t){Di=ho=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function ap(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ba(e,n)}}var mo={readContext:$t,useCallback:ze,useContext:ze,useEffect:ze,useImperativeHandle:ze,useInsertionEffect:ze,useLayoutEffect:ze,useMemo:ze,useReducer:ze,useRef:ze,useState:ze,useDebugValue:ze,useDeferredValue:ze,useTransition:ze,useMutableSource:ze,useSyncExternalStore:ze,useId:ze,unstable_isNewReconciler:!1},mv={readContext:$t,useCallback:function(e,t){return Rt().memoizedState=[e,t===void 0?null:t],e},useContext:$t,useEffect:Ec,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Bs(4194308,4,ep.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Bs(4194308,4,e,t)},useInsertionEffect:function(e,t){return Bs(4,2,e,t)},useMemo:function(e,t){var n=Rt();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Rt();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=pv.bind(null,pe,e),[r.memoizedState,e]},useRef:function(e){var t=Rt();return e={current:e},t.memoizedState=e},useState:Cc,useDebugValue:fu,useDeferredValue:function(e){return Rt().memoizedState=e},useTransition:function(){var e=Cc(!1),t=e[0];return e=fv.bind(null,e[1]),Rt().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=pe,i=Rt();if(de){if(n===void 0)throw Error(x(407));n=n()}else{if(n=t(),Te===null)throw Error(x(349));sr&30||Vf(r,t,n)}i.memoizedState=n;var s={value:n,getSnapshot:t};return i.queue=s,Ec(Wf.bind(null,r,s,e),[e]),r.flags|=2048,Xi(9,qf.bind(null,r,s,n,t),void 0,null),n},useId:function(){var e=Rt(),t=Te.identifierPrefix;if(de){var n=Wt,r=qt;n=(r&~(1<<32-It(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Yi++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=o.createElement(n,{is:r.is}):(e=o.createElement(n),n==="select"&&(o=e,r.multiple?o.multiple=!0:r.size&&(o.size=r.size))):e=o.createElementNS(e,n),e[Mt]=t,e[qi]=r,gp(e,t,!1,!1),t.stateNode=e;e:{switch(o=Ll(n,r),n){case"dialog":ae("cancel",e),ae("close",e),i=r;break;case"iframe":case"object":case"embed":ae("load",e),i=r;break;case"video":case"audio":for(i=0;iri&&(t.flags|=128,r=!0,$i(s,!1),t.lanes=4194304)}else{if(!r)if(e=po(o),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),$i(s,!0),s.tail===null&&s.tailMode==="hidden"&&!o.alternate&&!de)return be(t),null}else 2*$e()-s.renderingStartTime>ri&&n!==1073741824&&(t.flags|=128,r=!0,$i(s,!1),t.lanes=4194304);s.isBackwards?(o.sibling=t.child,t.child=o):(n=s.last,n!==null?n.sibling=o:t.child=o,s.last=o)}return s.tail!==null?(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=$e(),t.sibling=null,n=fe.current,oe(fe,r?n&1|2:n&1),t):(be(t),null);case 22:case 23:return gu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?st&1073741824&&(be(t),t.subtreeFlags&6&&(t.flags|=8192)):be(t),null;case 24:return null;case 25:return null}throw Error(x(156,t.tag))}function _v(e,t){switch(Xa(t),t.tag){case 1:return nt(t.type)&&so(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ti(),ue(tt),ue(Ue),lu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return ou(t),null;case 13:if(ue(fe),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(x(340));Zr()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return ue(fe),null;case 4:return ti(),null;case 10:return nu(t.type._context),null;case 22:case 23:return gu(),null;case 24:return null;default:return null}}var Rs=!1,Ae=!1,Nv=typeof WeakSet=="function"?WeakSet:Set,M=null;function Tr(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){ge(e,t,r)}else n.current=null}function la(e,t,n){try{n()}catch(r){ge(e,t,r)}}var zc=!1;function Cv(e,t){if(Kl=to,e=Cf(),Ya(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var i=r.anchorOffset,s=r.focusNode;r=r.focusOffset;try{n.nodeType,s.nodeType}catch{n=null;break e}var o=0,l=-1,a=-1,u=0,h=0,f=e,d=null;t:for(;;){for(var m;f!==n||i!==0&&f.nodeType!==3||(l=o+i),f!==s||r!==0&&f.nodeType!==3||(a=o+r),f.nodeType===3&&(o+=f.nodeValue.length),(m=f.firstChild)!==null;)d=f,f=m;for(;;){if(f===e)break t;if(d===n&&++u===i&&(l=o),d===s&&++h===r&&(a=o),(m=f.nextSibling)!==null)break;f=d,d=f.parentNode}f=m}n=l===-1||a===-1?null:{start:l,end:a}}else n=null}n=n||{start:0,end:0}}else n=null;for(Hl={focusedElem:e,selectionRange:n},to=!1,M=t;M!==null;)if(t=M,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,M=e;else for(;M!==null;){t=M;try{var $=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if($!==null){var k=$.memoizedProps,w=$.memoizedState,p=t.stateNode,c=p.getSnapshotBeforeUpdate(t.elementType===t.type?k:_t(t.type,k),w);p.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var v=t.stateNode.containerInfo;v.nodeType===1?v.textContent="":v.nodeType===9&&v.documentElement&&v.removeChild(v.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(x(163))}}catch(g){ge(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,M=e;break}M=t.return}return $=zc,zc=!1,$}function Oi(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var s=i.destroy;i.destroy=void 0,s!==void 0&&la(t,n,s)}i=i.next}while(i!==r)}}function Do(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function aa(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Sp(e){var t=e.alternate;t!==null&&(e.alternate=null,Sp(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Mt],delete t[qi],delete t[Wl],delete t[lv],delete t[av])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function wp(e){return e.tag===5||e.tag===3||e.tag===4}function bc(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||wp(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function ua(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=io));else if(r!==4&&(e=e.child,e!==null))for(ua(e,t,n),e=e.sibling;e!==null;)ua(e,t,n),e=e.sibling}function ca(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(ca(e,t,n),e=e.sibling;e!==null;)ca(e,t,n),e=e.sibling}var Re=null,Ct=!1;function sn(e,t,n){for(n=n.child;n!==null;)_p(e,t,n),n=n.sibling}function _p(e,t,n){if(Ft&&typeof Ft.onCommitFiberUnmount=="function")try{Ft.onCommitFiberUnmount(No,n)}catch{}switch(n.tag){case 5:Ae||Tr(n,t);case 6:var r=Re,i=Ct;Re=null,sn(e,t,n),Re=r,Ct=i,Re!==null&&(Ct?(e=Re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Re.removeChild(n.stateNode));break;case 18:Re!==null&&(Ct?(e=Re,n=n.stateNode,e.nodeType===8?dl(e.parentNode,n):e.nodeType===1&&dl(e,n),Ui(e)):dl(Re,n.stateNode));break;case 4:r=Re,i=Ct,Re=n.stateNode.containerInfo,Ct=!0,sn(e,t,n),Re=r,Ct=i;break;case 0:case 11:case 14:case 15:if(!Ae&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var s=i,o=s.destroy;s=s.tag,o!==void 0&&(s&2||s&4)&&la(n,t,o),i=i.next}while(i!==r)}sn(e,t,n);break;case 1:if(!Ae&&(Tr(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(l){ge(n,t,l)}sn(e,t,n);break;case 21:sn(e,t,n);break;case 22:n.mode&1?(Ae=(r=Ae)||n.memoizedState!==null,sn(e,t,n),Ae=r):sn(e,t,n);break;default:sn(e,t,n)}}function jc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Nv),t.forEach(function(r){var i=Lv.bind(null,e,r);n.has(r)||(n.add(r),r.then(i,i))})}}function wt(e,t){var n=t.deletions;if(n!==null)for(var r=0;ri&&(i=o),r&=~s}if(r=i,r=$e()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Iv(r/1960))-r,10e?16:e,Sn===null)var r=!1;else{if(e=Sn,Sn=null,go=0,Y&6)throw Error(x(331));var i=Y;for(Y|=4,M=e.current;M!==null;){var s=M,o=s.child;if(M.flags&16){var l=s.deletions;if(l!==null){for(var a=0;a$e()-vu?tr(e,0):mu|=n),rt(e,t)}function Rp(e,t){t===0&&(e.mode&1?(t=ws,ws<<=1,!(ws&130023424)&&(ws=4194304)):t=1);var n=qe();e=Zt(e,t),e!==null&&(us(e,t,n),rt(e,n))}function Ov(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Rp(e,n)}function Lv(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,i=e.memoizedState;i!==null&&(n=i.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(x(314))}r!==null&&r.delete(t),Rp(e,n)}var Dp;Dp=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||tt.current)et=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return et=!1,Sv(e,t,n);et=!!(e.flags&131072)}else et=!1,de&&t.flags&1048576&&Ff(t,ao,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Ks(e,t),e=t.pendingProps;var i=Xr(t,Ue.current);Mr(t,n),i=uu(null,t,r,e,i,n);var s=cu();return t.flags|=1,typeof i=="object"&&i!==null&&typeof i.render=="function"&&i.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,nt(r)?(s=!0,oo(t)):s=!1,t.memoizedState=i.state!==null&&i.state!==void 0?i.state:null,iu(t),i.updater=Ro,t.stateNode=i,i._reactInternals=t,ea(t,r,e,n),t=ra(null,t,r,!0,s,n)):(t.tag=0,de&&s&&Ja(t),He(null,t,i,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Ks(e,t),e=t.pendingProps,i=r._init,r=i(r._payload),t.type=r,i=t.tag=Fv(r),e=_t(r,e),i){case 0:t=na(null,t,r,e,n);break e;case 1:t=Lc(null,t,r,e,n);break e;case 11:t=Dc(null,t,r,e,n);break e;case 14:t=Oc(null,t,r,_t(r.type,e),n);break e}throw Error(x(306,r,""))}return t;case 0:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:_t(r,i),na(e,t,r,i,n);case 1:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:_t(r,i),Lc(e,t,r,i,n);case 3:e:{if(mp(t),e===null)throw Error(x(387));r=t.pendingProps,s=t.memoizedState,i=s.element,Uf(e,t),fo(t,r,null,n);var o=t.memoizedState;if(r=o.element,s.isDehydrated)if(s={element:r,isDehydrated:!1,cache:o.cache,pendingSuspenseBoundaries:o.pendingSuspenseBoundaries,transitions:o.transitions},t.updateQueue.baseState=s,t.memoizedState=s,t.flags&256){i=ni(Error(x(423)),t),t=Mc(e,t,r,n,i);break e}else if(r!==i){i=ni(Error(x(424)),t),t=Mc(e,t,r,n,i);break e}else for(at=Cn(t.stateNode.containerInfo.firstChild),ut=t,de=!0,Et=null,n=Qf(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Zr(),r===i){t=en(e,t,n);break e}He(e,t,r,n)}t=t.child}return t;case 5:return Bf(t),e===null&&Jl(t),r=t.type,i=t.pendingProps,s=e!==null?e.memoizedProps:null,o=i.children,Vl(r,i)?o=null:s!==null&&Vl(r,s)&&(t.flags|=32),hp(e,t),He(e,t,o,n),t.child;case 6:return e===null&&Jl(t),null;case 13:return vp(e,t,n);case 4:return su(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=ei(t,null,r,n):He(e,t,r,n),t.child;case 11:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:_t(r,i),Dc(e,t,r,i,n);case 7:return He(e,t,t.pendingProps,n),t.child;case 8:return He(e,t,t.pendingProps.children,n),t.child;case 12:return He(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,i=t.pendingProps,s=t.memoizedProps,o=i.value,oe(uo,r._currentValue),r._currentValue=o,s!==null)if(Tt(s.value,o)){if(s.children===i.children&&!tt.current){t=en(e,t,n);break e}}else for(s=t.child,s!==null&&(s.return=t);s!==null;){var l=s.dependencies;if(l!==null){o=s.child;for(var a=l.firstContext;a!==null;){if(a.context===r){if(s.tag===1){a=Gt(-1,n&-n),a.tag=2;var u=s.updateQueue;if(u!==null){u=u.shared;var h=u.pending;h===null?a.next=a:(a.next=h.next,h.next=a),u.pending=a}}s.lanes|=n,a=s.alternate,a!==null&&(a.lanes|=n),Xl(s.return,n,t),l.lanes|=n;break}a=a.next}}else if(s.tag===10)o=s.type===t.type?null:s.child;else if(s.tag===18){if(o=s.return,o===null)throw Error(x(341));o.lanes|=n,l=o.alternate,l!==null&&(l.lanes|=n),Xl(o,n,t),o=s.sibling}else o=s.child;if(o!==null)o.return=s;else for(o=s;o!==null;){if(o===t){o=null;break}if(s=o.sibling,s!==null){s.return=o.return,o=s;break}o=o.return}s=o}He(e,t,i.children,n),t=t.child}return t;case 9:return i=t.type,r=t.pendingProps.children,Mr(t,n),i=$t(i),r=r(i),t.flags|=1,He(e,t,r,n),t.child;case 14:return r=t.type,i=_t(r,t.pendingProps),i=_t(r.type,i),Oc(e,t,r,i,n);case 15:return fp(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:_t(r,i),Ks(e,t),t.tag=1,nt(r)?(e=!0,oo(t)):e=!1,Mr(t,n),up(t,r,i),ea(t,r,i,n),ra(null,t,r,!0,e,n);case 19:return yp(e,t,n);case 22:return pp(e,t,n)}throw Error(x(156,t.tag))};function Op(e,t){return of(e,t)}function Mv(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function gt(e,t,n,r){return new Mv(e,t,n,r)}function $u(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Fv(e){if(typeof e=="function")return $u(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ja)return 11;if(e===Qa)return 14}return 2}function Tn(e,t){var n=e.alternate;return n===null?(n=gt(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function qs(e,t,n,r,i,s){var o=2;if(r=e,typeof e=="function")$u(e)&&(o=1);else if(typeof e=="string")o=5;else e:switch(e){case $r:return nr(n.children,i,s,t);case ba:o=8,i|=8;break;case Nl:return e=gt(12,n,t,i|2),e.elementType=Nl,e.lanes=s,e;case Cl:return e=gt(13,n,t,i),e.elementType=Cl,e.lanes=s,e;case El:return e=gt(19,n,t,i),e.elementType=El,e.lanes=s,e;case Ud:return Lo(n,i,s,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Qd:o=10;break e;case Ad:o=9;break e;case ja:o=11;break e;case Qa:o=14;break e;case ln:o=16,r=null;break e}throw Error(x(130,e==null?e:typeof e,""))}return t=gt(o,n,t,i),t.elementType=e,t.type=r,t.lanes=s,t}function nr(e,t,n,r){return e=gt(7,e,r,t),e.lanes=n,e}function Lo(e,t,n,r){return e=gt(22,e,r,t),e.elementType=Ud,e.lanes=n,e.stateNode={isHidden:!1},e}function kl(e,t,n){return e=gt(6,e,null,t),e.lanes=n,e}function $l(e,t,n){return t=gt(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function zv(e,t,n,r,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=el(0),this.expirationTimes=el(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=el(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function Su(e,t,n,r,i,s,o,l,a){return e=new zv(e,t,n,l,a),t===1?(t=1,s===!0&&(t|=8)):t=0,s=gt(3,null,null,t),e.current=s,s.stateNode=e,s.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},iu(s),e}function bv(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(zp)}catch(e){console.error(e)}}zp(),Fd.exports=ft;var Bv=Fd.exports,bp,qc=Bv;bp=qc.createRoot,qc.hydrateRoot;var li=class{constructor(){this.listeners=new Set,this.subscribe=this.subscribe.bind(this)}subscribe(e){return this.listeners.add(e),this.onSubscribe(),()=>{this.listeners.delete(e),this.onUnsubscribe()}}hasListeners(){return this.listeners.size>0}onSubscribe(){}onUnsubscribe(){}},Vn,dn,br,vd,Kv=(vd=class extends li{constructor(){super();b(this,Vn);b(this,dn);b(this,br);O(this,br,t=>{if(typeof window<"u"&&window.addEventListener){const n=()=>t();return window.addEventListener("visibilitychange",n,!1),()=>{window.removeEventListener("visibilitychange",n)}}})}onSubscribe(){y(this,dn)||this.setEventListener(y(this,br))}onUnsubscribe(){var t;this.hasListeners()||((t=y(this,dn))==null||t.call(this),O(this,dn,void 0))}setEventListener(t){var n;O(this,br,t),(n=y(this,dn))==null||n.call(this),O(this,dn,t(r=>{typeof r=="boolean"?this.setFocused(r):this.onFocus()}))}setFocused(t){y(this,Vn)!==t&&(O(this,Vn,t),this.onFocus())}onFocus(){const t=this.isFocused();this.listeners.forEach(n=>{n(t)})}isFocused(){var t;return typeof y(this,Vn)=="boolean"?y(this,Vn):((t=globalThis.document)==null?void 0:t.visibilityState)!=="hidden"}},Vn=new WeakMap,dn=new WeakMap,br=new WeakMap,vd),Cu=new Kv,Hv={setTimeout:(e,t)=>setTimeout(e,t),clearTimeout:e=>clearTimeout(e),setInterval:(e,t)=>setInterval(e,t),clearInterval:e=>clearInterval(e)},fn,xa,yd,Vv=(yd=class{constructor(){b(this,fn,Hv);b(this,xa,!1)}setTimeoutProvider(e){O(this,fn,e)}setTimeout(e,t){return y(this,fn).setTimeout(e,t)}clearTimeout(e){y(this,fn).clearTimeout(e)}setInterval(e,t){return y(this,fn).setInterval(e,t)}clearInterval(e){y(this,fn).clearInterval(e)}},fn=new WeakMap,xa=new WeakMap,yd),Hn=new Vv;function qv(e){setTimeout(e,0)}var Wv=typeof window>"u"||"Deno"in globalThis;function Ve(){}function Gv(e,t){return typeof e=="function"?e(t):e}function ma(e){return typeof e=="number"&&e>=0&&e!==1/0}function jp(e,t){return Math.max(e+(t||0)-Date.now(),0)}function xn(e,t){return typeof e=="function"?e(t):e}function lt(e,t){return typeof e=="function"?e(t):e}function Wc(e,t){const{type:n="all",exact:r,fetchStatus:i,predicate:s,queryKey:o,stale:l}=e;if(o){if(r){if(t.queryHash!==Eu(o,t.options))return!1}else if(!es(t.queryKey,o))return!1}if(n!=="all"){const a=t.isActive();if(n==="active"&&!a||n==="inactive"&&a)return!1}return!(typeof l=="boolean"&&t.isStale()!==l||i&&i!==t.state.fetchStatus||s&&!s(t))}function Gc(e,t){const{exact:n,status:r,predicate:i,mutationKey:s}=e;if(s){if(!t.options.mutationKey)return!1;if(n){if(ar(t.options.mutationKey)!==ar(s))return!1}else if(!es(t.options.mutationKey,s))return!1}return!(r&&t.state.status!==r||i&&!i(t))}function Eu(e,t){return((t==null?void 0:t.queryKeyHashFn)||ar)(e)}function ar(e){return JSON.stringify(e,(t,n)=>va(n)?Object.keys(n).sort().reduce((r,i)=>(r[i]=n[i],r),{}):n)}function es(e,t){return e===t?!0:typeof e!=typeof t?!1:e&&t&&typeof e=="object"&&typeof t=="object"?Object.keys(t).every(n=>es(e[n],t[n])):!1}var Yv=Object.prototype.hasOwnProperty;function Qp(e,t,n=0){if(e===t)return e;if(n>500)return t;const r=Yc(e)&&Yc(t);if(!r&&!(va(e)&&va(t)))return t;const s=(r?e:Object.keys(e)).length,o=r?t:Object.keys(t),l=o.length,a=r?new Array(l):{};let u=0;for(let h=0;h{Hn.setTimeout(t,e)})}function ya(e,t,n){return typeof n.structuralSharing=="function"?n.structuralSharing(e,t):n.structuralSharing!==!1?Qp(e,t):t}function Xv(e,t,n=0){const r=[...e,t];return n&&r.length>n?r.slice(1):r}function Zv(e,t,n=0){const r=[t,...e];return n&&r.length>n?r.slice(0,-1):r}var Iu=Symbol();function Ap(e,t){return!e.queryFn&&(t!=null&&t.initialPromise)?()=>t.initialPromise:!e.queryFn||e.queryFn===Iu?()=>Promise.reject(new Error(`Missing queryFn: '${e.queryHash}'`)):e.queryFn}function Pu(e,t){return typeof e=="function"?e(...t):!!e}function ey(e,t,n){let r=!1,i;return Object.defineProperty(e,"signal",{enumerable:!0,get:()=>(i??(i=t()),r||(r=!0,i.aborted?n():i.addEventListener("abort",n,{once:!0})),i)}),e}var ts=(()=>{let e=()=>Wv;return{isServer(){return e()},setIsServer(t){e=t}}})();function ga(){let e,t;const n=new Promise((i,s)=>{e=i,t=s});n.status="pending",n.catch(()=>{});function r(i){Object.assign(n,i),delete n.resolve,delete n.reject}return n.resolve=i=>{r({status:"fulfilled",value:i}),e(i)},n.reject=i=>{r({status:"rejected",reason:i}),t(i)},n}var ty=qv;function ny(){let e=[],t=0,n=l=>{l()},r=l=>{l()},i=ty;const s=l=>{t?e.push(l):i(()=>{n(l)})},o=()=>{const l=e;e=[],l.length&&i(()=>{r(()=>{l.forEach(a=>{n(a)})})})};return{batch:l=>{let a;t++;try{a=l()}finally{t--,t||o()}return a},batchCalls:l=>(...a)=>{s(()=>{l(...a)})},schedule:s,setNotifyFunction:l=>{n=l},setBatchNotifyFunction:l=>{r=l},setScheduler:l=>{i=l}}}var Ce=ny(),jr,pn,Qr,gd,ry=(gd=class extends li{constructor(){super();b(this,jr,!0);b(this,pn);b(this,Qr);O(this,Qr,t=>{if(typeof window<"u"&&window.addEventListener){const n=()=>t(!0),r=()=>t(!1);return window.addEventListener("online",n,!1),window.addEventListener("offline",r,!1),()=>{window.removeEventListener("online",n),window.removeEventListener("offline",r)}}})}onSubscribe(){y(this,pn)||this.setEventListener(y(this,Qr))}onUnsubscribe(){var t;this.hasListeners()||((t=y(this,pn))==null||t.call(this),O(this,pn,void 0))}setEventListener(t){var n;O(this,Qr,t),(n=y(this,pn))==null||n.call(this),O(this,pn,t(this.setOnline.bind(this)))}setOnline(t){y(this,jr)!==t&&(O(this,jr,t),this.listeners.forEach(r=>{r(t)}))}isOnline(){return y(this,jr)}},jr=new WeakMap,pn=new WeakMap,Qr=new WeakMap,gd),wo=new ry;function iy(e){return Math.min(1e3*2**e,3e4)}function Up(e){return(e??"online")==="online"?wo.isOnline():!0}var ka=class extends Error{constructor(e){super("CancelledError"),this.revert=e==null?void 0:e.revert,this.silent=e==null?void 0:e.silent}};function Bp(e){let t=!1,n=0,r;const i=ga(),s=()=>i.status!=="pending",o=k=>{var w;if(!s()){const p=new ka(k);d(p),(w=e.onCancel)==null||w.call(e,p)}},l=()=>{t=!0},a=()=>{t=!1},u=()=>Cu.isFocused()&&(e.networkMode==="always"||wo.isOnline())&&e.canRun(),h=()=>Up(e.networkMode)&&e.canRun(),f=k=>{s()||(r==null||r(),i.resolve(k))},d=k=>{s()||(r==null||r(),i.reject(k))},m=()=>new Promise(k=>{var w;r=p=>{(s()||u())&&k(p)},(w=e.onPause)==null||w.call(e)}).then(()=>{var k;r=void 0,s()||(k=e.onContinue)==null||k.call(e)}),$=()=>{if(s())return;let k;const w=n===0?e.initialPromise:void 0;try{k=w??e.fn()}catch(p){k=Promise.reject(p)}Promise.resolve(k).then(f).catch(p=>{var I;if(s())return;const c=e.retry??(ts.isServer()?0:3),v=e.retryDelay??iy,g=typeof v=="function"?v(n,p):v,_=c===!0||typeof c=="number"&&nu()?void 0:m()).then(()=>{t?d(p):$()})})};return{promise:i,status:()=>i.status,cancel:o,continue:()=>(r==null||r(),i),cancelRetry:l,continueRetry:a,canStart:h,start:()=>(h()?$():m().then($),i)}}var qn,kd,Kp=(kd=class{constructor(){b(this,qn)}destroy(){this.clearGcTimeout()}scheduleGc(){this.clearGcTimeout(),ma(this.gcTime)&&O(this,qn,Hn.setTimeout(()=>{this.optionalRemove()},this.gcTime))}updateGcTime(e){this.gcTime=Math.max(this.gcTime||0,e??(ts.isServer()?1/0:5*60*1e3))}clearGcTimeout(){y(this,qn)!==void 0&&(Hn.clearTimeout(y(this,qn)),O(this,qn,void 0))}},qn=new WeakMap,kd);function sy(e){return{onFetch:(t,n)=>{var h,f,d,m,$;const r=t.options,i=(d=(f=(h=t.fetchOptions)==null?void 0:h.meta)==null?void 0:f.fetchMore)==null?void 0:d.direction,s=((m=t.state.data)==null?void 0:m.pages)||[],o=(($=t.state.data)==null?void 0:$.pageParams)||[];let l={pages:[],pageParams:[]},a=0;const u=async()=>{let k=!1;const w=v=>{ey(v,()=>t.signal,()=>k=!0)},p=Ap(t.options,t.fetchOptions),c=async(v,g,_)=>{if(k)return Promise.reject(t.signal.reason);if(g==null&&v.pages.length)return Promise.resolve(v);const E=(()=>{const H={client:t.client,queryKey:t.queryKey,pageParam:g,direction:_?"backward":"forward",meta:t.options.meta};return w(H),H})(),C=await p(E),{maxPages:P}=t.options,T=_?Zv:Xv;return{pages:T(v.pages,C,P),pageParams:T(v.pageParams,g,P)}};if(i&&s.length){const v=i==="backward",g=v?oy:Xc,_={pages:s,pageParams:o},I=g(r,_);l=await c(_,I,v)}else{const v=e??s.length;do{const g=a===0?o[0]??r.initialPageParam:Xc(r,l);if(a>0&&g==null)break;l=await c(l,g),a++}while(a{var k,w;return(w=(k=t.options).persister)==null?void 0:w.call(k,u,{client:t.client,queryKey:t.queryKey,meta:t.options.meta,signal:t.signal},n)}:t.fetchFn=u}}}function Xc(e,{pages:t,pageParams:n}){const r=t.length-1;return t.length>0?e.getNextPageParam(t[r],t,n[r],n):void 0}function oy(e,{pages:t,pageParams:n}){var r;return t.length>0?(r=e.getPreviousPageParam)==null?void 0:r.call(e,t[0],t,n[0],n):void 0}var Ar,Wn,Ur,mt,Gn,Ie,rs,Yn,ot,Hp,Qt,$d,ly=($d=class extends Kp{constructor(t){super();b(this,ot);b(this,Ar);b(this,Wn);b(this,Ur);b(this,mt);b(this,Gn);b(this,Ie);b(this,rs);b(this,Yn);O(this,Yn,!1),O(this,rs,t.defaultOptions),this.setOptions(t.options),this.observers=[],O(this,Gn,t.client),O(this,mt,y(this,Gn).getQueryCache()),this.queryKey=t.queryKey,this.queryHash=t.queryHash,O(this,Wn,ed(this.options)),this.state=t.state??y(this,Wn),this.scheduleGc()}get meta(){return this.options.meta}get queryType(){return y(this,Ar)}get promise(){var t;return(t=y(this,Ie))==null?void 0:t.promise}setOptions(t){if(this.options={...y(this,rs),...t},t!=null&&t._type&&O(this,Ar,t._type),this.updateGcTime(this.options.gcTime),this.state&&this.state.data===void 0){const n=ed(this.options);n.data!==void 0&&(this.setState(Zc(n.data,n.dataUpdatedAt)),O(this,Wn,n))}}optionalRemove(){!this.observers.length&&this.state.fetchStatus==="idle"&&y(this,mt).remove(this)}setData(t,n){const r=ya(this.state.data,t,this.options);return K(this,ot,Qt).call(this,{data:r,type:"success",dataUpdatedAt:n==null?void 0:n.updatedAt,manual:n==null?void 0:n.manual}),r}setState(t){K(this,ot,Qt).call(this,{type:"setState",state:t})}cancel(t){var r,i;const n=(r=y(this,Ie))==null?void 0:r.promise;return(i=y(this,Ie))==null||i.cancel(t),n?n.then(Ve).catch(Ve):Promise.resolve()}destroy(){super.destroy(),this.cancel({silent:!0})}get resetState(){return y(this,Wn)}reset(){this.destroy(),this.setState(this.resetState)}isActive(){return this.observers.some(t=>lt(t.options.enabled,this)!==!1)}isDisabled(){return this.getObserversCount()>0?!this.isActive():this.options.queryFn===Iu||!this.isFetched()}isFetched(){return this.state.dataUpdateCount+this.state.errorUpdateCount>0}isStatic(){return this.getObserversCount()>0?this.observers.some(t=>xn(t.options.staleTime,this)==="static"):!1}isStale(){return this.getObserversCount()>0?this.observers.some(t=>t.getCurrentResult().isStale):this.state.data===void 0||this.state.isInvalidated}isStaleByTime(t=0){return this.state.data===void 0?!0:t==="static"?!1:this.state.isInvalidated?!0:!jp(this.state.dataUpdatedAt,t)}onFocus(){var n;const t=this.observers.find(r=>r.shouldFetchOnWindowFocus());t==null||t.refetch({cancelRefetch:!1}),(n=y(this,Ie))==null||n.continue()}onOnline(){var n;const t=this.observers.find(r=>r.shouldFetchOnReconnect());t==null||t.refetch({cancelRefetch:!1}),(n=y(this,Ie))==null||n.continue()}addObserver(t){this.observers.includes(t)||(this.observers.push(t),this.clearGcTimeout(),y(this,mt).notify({type:"observerAdded",query:this,observer:t}))}removeObserver(t){this.observers.includes(t)&&(this.observers=this.observers.filter(n=>n!==t),this.observers.length||(y(this,Ie)&&(y(this,Yn)||K(this,ot,Hp).call(this)?y(this,Ie).cancel({revert:!0}):y(this,Ie).cancelRetry()),this.scheduleGc()),y(this,mt).notify({type:"observerRemoved",query:this,observer:t}))}getObserversCount(){return this.observers.length}invalidate(){this.state.isInvalidated||K(this,ot,Qt).call(this,{type:"invalidate"})}async fetch(t,n){var u,h,f,d,m,$,k,w,p,c,v;if(this.state.fetchStatus!=="idle"&&((u=y(this,Ie))==null?void 0:u.status())!=="rejected"){if(this.state.data!==void 0&&(n!=null&&n.cancelRefetch))this.cancel({silent:!0});else if(y(this,Ie))return y(this,Ie).continueRetry(),y(this,Ie).promise}if(t&&this.setOptions(t),!this.options.queryFn){const g=this.observers.find(_=>_.options.queryFn);g&&this.setOptions(g.options)}const r=new AbortController,i=g=>{Object.defineProperty(g,"signal",{enumerable:!0,get:()=>(O(this,Yn,!0),r.signal)})},s=()=>{const g=Ap(this.options,n),I=(()=>{const E={client:y(this,Gn),queryKey:this.queryKey,meta:this.meta};return i(E),E})();return O(this,Yn,!1),this.options.persister?this.options.persister(g,I,this):g(I)},l=(()=>{const g={fetchOptions:n,options:this.options,queryKey:this.queryKey,client:y(this,Gn),state:this.state,fetchFn:s};return i(g),g})(),a=y(this,Ar)==="infinite"?sy(this.options.pages):this.options.behavior;a==null||a.onFetch(l,this),O(this,Ur,this.state),(this.state.fetchStatus==="idle"||this.state.fetchMeta!==((h=l.fetchOptions)==null?void 0:h.meta))&&K(this,ot,Qt).call(this,{type:"fetch",meta:(f=l.fetchOptions)==null?void 0:f.meta}),O(this,Ie,Bp({initialPromise:n==null?void 0:n.initialPromise,fn:l.fetchFn,onCancel:g=>{g instanceof ka&&g.revert&&this.setState({...y(this,Ur),fetchStatus:"idle"}),r.abort()},onFail:(g,_)=>{K(this,ot,Qt).call(this,{type:"failed",failureCount:g,error:_})},onPause:()=>{K(this,ot,Qt).call(this,{type:"pause"})},onContinue:()=>{K(this,ot,Qt).call(this,{type:"continue"})},retry:l.options.retry,retryDelay:l.options.retryDelay,networkMode:l.options.networkMode,canRun:()=>!0}));try{const g=await y(this,Ie).start();if(g===void 0)throw new Error(`${this.queryHash} data is undefined`);return this.setData(g),(m=(d=y(this,mt).config).onSuccess)==null||m.call(d,g,this),(k=($=y(this,mt).config).onSettled)==null||k.call($,g,this.state.error,this),g}catch(g){if(g instanceof ka){if(g.silent)return y(this,Ie).promise;if(g.revert){if(this.state.data===void 0)throw g;return this.state.data}}throw K(this,ot,Qt).call(this,{type:"error",error:g}),(p=(w=y(this,mt).config).onError)==null||p.call(w,g,this),(v=(c=y(this,mt).config).onSettled)==null||v.call(c,this.state.data,g,this),g}finally{this.scheduleGc()}}},Ar=new WeakMap,Wn=new WeakMap,Ur=new WeakMap,mt=new WeakMap,Gn=new WeakMap,Ie=new WeakMap,rs=new WeakMap,Yn=new WeakMap,ot=new WeakSet,Hp=function(){return this.state.fetchStatus==="paused"&&this.state.status==="pending"},Qt=function(t){const n=r=>{switch(t.type){case"failed":return{...r,fetchFailureCount:t.failureCount,fetchFailureReason:t.error};case"pause":return{...r,fetchStatus:"paused"};case"continue":return{...r,fetchStatus:"fetching"};case"fetch":return{...r,...Vp(r.data,this.options),fetchMeta:t.meta??null};case"success":const i={...r,...Zc(t.data,t.dataUpdatedAt),dataUpdateCount:r.dataUpdateCount+1,...!t.manual&&{fetchStatus:"idle",fetchFailureCount:0,fetchFailureReason:null}};return O(this,Ur,t.manual?i:void 0),i;case"error":const s=t.error;return{...r,error:s,errorUpdateCount:r.errorUpdateCount+1,errorUpdatedAt:Date.now(),fetchFailureCount:r.fetchFailureCount+1,fetchFailureReason:s,fetchStatus:"idle",status:"error",isInvalidated:!0};case"invalidate":return{...r,isInvalidated:!0};case"setState":return{...r,...t.state}}};this.state=n(this.state),Ce.batch(()=>{this.observers.forEach(r=>{r.onQueryUpdate()}),y(this,mt).notify({query:this,type:"updated",action:t})})},$d);function Vp(e,t){return{fetchFailureCount:0,fetchFailureReason:null,fetchStatus:Up(t.networkMode)?"fetching":"paused",...e===void 0&&{error:null,status:"pending"}}}function Zc(e,t){return{data:e,dataUpdatedAt:t??Date.now(),error:null,isInvalidated:!1,status:"success"}}function ed(e){const t=typeof e.initialData=="function"?e.initialData():e.initialData,n=t!==void 0,r=n?typeof e.initialDataUpdatedAt=="function"?e.initialDataUpdatedAt():e.initialDataUpdatedAt:0;return{data:t,dataUpdateCount:0,dataUpdatedAt:n?r??Date.now():0,error:null,errorUpdateCount:0,errorUpdatedAt:0,fetchFailureCount:0,fetchFailureReason:null,fetchMeta:null,isInvalidated:!1,status:n?"success":"pending",fetchStatus:"idle"}}var Je,G,is,Ke,Jn,Br,Bt,hn,ss,Kr,Hr,Xn,Zn,mn,Vr,X,Ei,$a,Sa,wa,_a,Na,Ca,Ea,qp,Sd,ay=(Sd=class extends li{constructor(t,n){super();b(this,X);b(this,Je);b(this,G);b(this,is);b(this,Ke);b(this,Jn);b(this,Br);b(this,Bt);b(this,hn);b(this,ss);b(this,Kr);b(this,Hr);b(this,Xn);b(this,Zn);b(this,mn);b(this,Vr,new Set);this.options=n,O(this,Je,t),O(this,hn,null),O(this,Bt,ga()),this.bindMethods(),this.setOptions(n)}bindMethods(){this.refetch=this.refetch.bind(this)}onSubscribe(){this.listeners.size===1&&(y(this,G).addObserver(this),td(y(this,G),this.options)?K(this,X,Ei).call(this):this.updateResult(),K(this,X,_a).call(this))}onUnsubscribe(){this.hasListeners()||this.destroy()}shouldFetchOnReconnect(){return Ia(y(this,G),this.options,this.options.refetchOnReconnect)}shouldFetchOnWindowFocus(){return Ia(y(this,G),this.options,this.options.refetchOnWindowFocus)}destroy(){this.listeners=new Set,K(this,X,Na).call(this),K(this,X,Ca).call(this),y(this,G).removeObserver(this)}setOptions(t){const n=this.options,r=y(this,G);if(this.options=y(this,Je).defaultQueryOptions(t),this.options.enabled!==void 0&&typeof this.options.enabled!="boolean"&&typeof this.options.enabled!="function"&&typeof lt(this.options.enabled,y(this,G))!="boolean")throw new Error("Expected enabled to be a boolean or a callback that returns a boolean");K(this,X,Ea).call(this),y(this,G).setOptions(this.options),n._defaulted&&!So(this.options,n)&&y(this,Je).getQueryCache().notify({type:"observerOptionsUpdated",query:y(this,G),observer:this});const i=this.hasListeners();i&&nd(y(this,G),r,this.options,n)&&K(this,X,Ei).call(this),this.updateResult(),i&&(y(this,G)!==r||lt(this.options.enabled,y(this,G))!==lt(n.enabled,y(this,G))||xn(this.options.staleTime,y(this,G))!==xn(n.staleTime,y(this,G)))&&K(this,X,$a).call(this);const s=K(this,X,Sa).call(this);i&&(y(this,G)!==r||lt(this.options.enabled,y(this,G))!==lt(n.enabled,y(this,G))||s!==y(this,mn))&&K(this,X,wa).call(this,s)}getOptimisticResult(t){const n=y(this,Je).getQueryCache().build(y(this,Je),t),r=this.createResult(n,t);return cy(this,r)&&(O(this,Ke,r),O(this,Br,this.options),O(this,Jn,y(this,G).state)),r}getCurrentResult(){return y(this,Ke)}trackResult(t,n){return new Proxy(t,{get:(r,i)=>(this.trackProp(i),n==null||n(i),i==="promise"&&(this.trackProp("data"),!this.options.experimental_prefetchInRender&&y(this,Bt).status==="pending"&&y(this,Bt).reject(new Error("experimental_prefetchInRender feature flag is not enabled"))),Reflect.get(r,i))})}trackProp(t){y(this,Vr).add(t)}getCurrentQuery(){return y(this,G)}refetch({...t}={}){return this.fetch({...t})}fetchOptimistic(t){const n=y(this,Je).defaultQueryOptions(t),r=y(this,Je).getQueryCache().build(y(this,Je),n);return r.fetch().then(()=>this.createResult(r,n))}fetch(t){return K(this,X,Ei).call(this,{...t,cancelRefetch:t.cancelRefetch??!0}).then(()=>(this.updateResult(),y(this,Ke)))}createResult(t,n){var P;const r=y(this,G),i=this.options,s=y(this,Ke),o=y(this,Jn),l=y(this,Br),u=t!==r?t.state:y(this,is),{state:h}=t;let f={...h},d=!1,m;if(n._optimisticResults){const T=this.hasListeners(),H=!T&&td(t,n),J=T&&nd(t,r,n,i);(H||J)&&(f={...f,...Vp(h.data,t.options)}),n._optimisticResults==="isRestoring"&&(f.fetchStatus="idle")}let{error:$,errorUpdatedAt:k,status:w}=f;m=f.data;let p=!1;if(n.placeholderData!==void 0&&m===void 0&&w==="pending"){let T;s!=null&&s.isPlaceholderData&&n.placeholderData===(l==null?void 0:l.placeholderData)?(T=s.data,p=!0):T=typeof n.placeholderData=="function"?n.placeholderData((P=y(this,Hr))==null?void 0:P.state.data,y(this,Hr)):n.placeholderData,T!==void 0&&(w="success",m=ya(s==null?void 0:s.data,T,n),d=!0)}if(n.select&&m!==void 0&&!p)if(s&&m===(o==null?void 0:o.data)&&n.select===y(this,ss))m=y(this,Kr);else try{O(this,ss,n.select),m=n.select(m),m=ya(s==null?void 0:s.data,m,n),O(this,Kr,m),O(this,hn,null)}catch(T){O(this,hn,T)}y(this,hn)&&($=y(this,hn),m=y(this,Kr),k=Date.now(),w="error");const c=f.fetchStatus==="fetching",v=w==="pending",g=w==="error",_=v&&c,I=m!==void 0,C={status:w,fetchStatus:f.fetchStatus,isPending:v,isSuccess:w==="success",isError:g,isInitialLoading:_,isLoading:_,data:m,dataUpdatedAt:f.dataUpdatedAt,error:$,errorUpdatedAt:k,failureCount:f.fetchFailureCount,failureReason:f.fetchFailureReason,errorUpdateCount:f.errorUpdateCount,isFetched:t.isFetched(),isFetchedAfterMount:f.dataUpdateCount>u.dataUpdateCount||f.errorUpdateCount>u.errorUpdateCount,isFetching:c,isRefetching:c&&!v,isLoadingError:g&&!I,isPaused:f.fetchStatus==="paused",isPlaceholderData:d,isRefetchError:g&&I,isStale:Tu(t,n),refetch:this.refetch,promise:y(this,Bt),isEnabled:lt(n.enabled,t)!==!1};if(this.options.experimental_prefetchInRender){const T=C.data!==void 0,H=C.status==="error"&&!T,J=Le=>{H?Le.reject(C.error):T&&Le.resolve(C.data)},le=()=>{const Le=O(this,Bt,C.promise=ga());J(Le)},ce=y(this,Bt);switch(ce.status){case"pending":t.queryHash===r.queryHash&&J(ce);break;case"fulfilled":(H||C.data!==ce.value)&&le();break;case"rejected":(!H||C.error!==ce.reason)&&le();break}}return C}updateResult(){const t=y(this,Ke),n=this.createResult(y(this,G),this.options);if(O(this,Jn,y(this,G).state),O(this,Br,this.options),y(this,Jn).data!==void 0&&O(this,Hr,y(this,G)),So(n,t))return;O(this,Ke,n);const r=()=>{if(!t)return!0;const{notifyOnChangeProps:i}=this.options,s=typeof i=="function"?i():i;if(s==="all"||!s&&!y(this,Vr).size)return!0;const o=new Set(s??y(this,Vr));return this.options.throwOnError&&o.add("error"),Object.keys(y(this,Ke)).some(l=>{const a=l;return y(this,Ke)[a]!==t[a]&&o.has(a)})};K(this,X,qp).call(this,{listeners:r()})}onQueryUpdate(){this.updateResult(),this.hasListeners()&&K(this,X,_a).call(this)}},Je=new WeakMap,G=new WeakMap,is=new WeakMap,Ke=new WeakMap,Jn=new WeakMap,Br=new WeakMap,Bt=new WeakMap,hn=new WeakMap,ss=new WeakMap,Kr=new WeakMap,Hr=new WeakMap,Xn=new WeakMap,Zn=new WeakMap,mn=new WeakMap,Vr=new WeakMap,X=new WeakSet,Ei=function(t){K(this,X,Ea).call(this);let n=y(this,G).fetch(this.options,t);return t!=null&&t.throwOnError||(n=n.catch(Ve)),n},$a=function(){K(this,X,Na).call(this);const t=xn(this.options.staleTime,y(this,G));if(ts.isServer()||y(this,Ke).isStale||!ma(t))return;const r=jp(y(this,Ke).dataUpdatedAt,t)+1;O(this,Xn,Hn.setTimeout(()=>{y(this,Ke).isStale||this.updateResult()},r))},Sa=function(){return(typeof this.options.refetchInterval=="function"?this.options.refetchInterval(y(this,G)):this.options.refetchInterval)??!1},wa=function(t){K(this,X,Ca).call(this),O(this,mn,t),!(ts.isServer()||lt(this.options.enabled,y(this,G))===!1||!ma(y(this,mn))||y(this,mn)===0)&&O(this,Zn,Hn.setInterval(()=>{(this.options.refetchIntervalInBackground||Cu.isFocused())&&K(this,X,Ei).call(this)},y(this,mn)))},_a=function(){K(this,X,$a).call(this),K(this,X,wa).call(this,K(this,X,Sa).call(this))},Na=function(){y(this,Xn)!==void 0&&(Hn.clearTimeout(y(this,Xn)),O(this,Xn,void 0))},Ca=function(){y(this,Zn)!==void 0&&(Hn.clearInterval(y(this,Zn)),O(this,Zn,void 0))},Ea=function(){const t=y(this,Je).getQueryCache().build(y(this,Je),this.options);if(t===y(this,G))return;const n=y(this,G);O(this,G,t),O(this,is,t.state),this.hasListeners()&&(n==null||n.removeObserver(this),t.addObserver(this))},qp=function(t){Ce.batch(()=>{t.listeners&&this.listeners.forEach(n=>{n(y(this,Ke))}),y(this,Je).getQueryCache().notify({query:y(this,G),type:"observerResultsUpdated"})})},Sd);function uy(e,t){return lt(t.enabled,e)!==!1&&e.state.data===void 0&&!(e.state.status==="error"&<(t.retryOnMount,e)===!1)}function td(e,t){return uy(e,t)||e.state.data!==void 0&&Ia(e,t,t.refetchOnMount)}function Ia(e,t,n){if(lt(t.enabled,e)!==!1&&xn(t.staleTime,e)!=="static"){const r=typeof n=="function"?n(e):n;return r==="always"||r!==!1&&Tu(e,t)}return!1}function nd(e,t,n,r){return(e!==t||lt(r.enabled,e)===!1)&&(!n.suspense||e.state.status!=="error")&&Tu(e,n)}function Tu(e,t){return lt(t.enabled,e)!==!1&&e.isStaleByTime(xn(t.staleTime,e))}function cy(e,t){return!So(e.getCurrentResult(),t)}var os,Dt,Qe,er,Ot,on,wd,dy=(wd=class extends Kp{constructor(t){super();b(this,Ot);b(this,os);b(this,Dt);b(this,Qe);b(this,er);O(this,os,t.client),this.mutationId=t.mutationId,O(this,Qe,t.mutationCache),O(this,Dt,[]),this.state=t.state||Wp(),this.setOptions(t.options),this.scheduleGc()}setOptions(t){this.options=t,this.updateGcTime(this.options.gcTime)}get meta(){return this.options.meta}addObserver(t){y(this,Dt).includes(t)||(y(this,Dt).push(t),this.clearGcTimeout(),y(this,Qe).notify({type:"observerAdded",mutation:this,observer:t}))}removeObserver(t){O(this,Dt,y(this,Dt).filter(n=>n!==t)),this.scheduleGc(),y(this,Qe).notify({type:"observerRemoved",mutation:this,observer:t})}optionalRemove(){y(this,Dt).length||(this.state.status==="pending"?this.scheduleGc():y(this,Qe).remove(this))}continue(){var t;return((t=y(this,er))==null?void 0:t.continue())??this.execute(this.state.variables)}async execute(t){var o,l,a,u,h,f,d,m,$,k,w,p,c,v,g,_,I,E;const n=()=>{K(this,Ot,on).call(this,{type:"continue"})},r={client:y(this,os),meta:this.options.meta,mutationKey:this.options.mutationKey};O(this,er,Bp({fn:()=>this.options.mutationFn?this.options.mutationFn(t,r):Promise.reject(new Error("No mutationFn found")),onFail:(C,P)=>{K(this,Ot,on).call(this,{type:"failed",failureCount:C,error:P})},onPause:()=>{K(this,Ot,on).call(this,{type:"pause"})},onContinue:n,retry:this.options.retry??0,retryDelay:this.options.retryDelay,networkMode:this.options.networkMode,canRun:()=>y(this,Qe).canRun(this)}));const i=this.state.status==="pending",s=!y(this,er).canStart();try{if(i)n();else{K(this,Ot,on).call(this,{type:"pending",variables:t,isPaused:s}),y(this,Qe).config.onMutate&&await y(this,Qe).config.onMutate(t,this,r);const P=await((l=(o=this.options).onMutate)==null?void 0:l.call(o,t,r));P!==this.state.context&&K(this,Ot,on).call(this,{type:"pending",context:P,variables:t,isPaused:s})}const C=await y(this,er).start();return await((u=(a=y(this,Qe).config).onSuccess)==null?void 0:u.call(a,C,t,this.state.context,this,r)),await((f=(h=this.options).onSuccess)==null?void 0:f.call(h,C,t,this.state.context,r)),await((m=(d=y(this,Qe).config).onSettled)==null?void 0:m.call(d,C,null,this.state.variables,this.state.context,this,r)),await((k=($=this.options).onSettled)==null?void 0:k.call($,C,null,t,this.state.context,r)),K(this,Ot,on).call(this,{type:"success",data:C}),C}catch(C){try{await((p=(w=y(this,Qe).config).onError)==null?void 0:p.call(w,C,t,this.state.context,this,r))}catch(P){Promise.reject(P)}try{await((v=(c=this.options).onError)==null?void 0:v.call(c,C,t,this.state.context,r))}catch(P){Promise.reject(P)}try{await((_=(g=y(this,Qe).config).onSettled)==null?void 0:_.call(g,void 0,C,this.state.variables,this.state.context,this,r))}catch(P){Promise.reject(P)}try{await((E=(I=this.options).onSettled)==null?void 0:E.call(I,void 0,C,t,this.state.context,r))}catch(P){Promise.reject(P)}throw K(this,Ot,on).call(this,{type:"error",error:C}),C}finally{y(this,Qe).runNext(this)}}},os=new WeakMap,Dt=new WeakMap,Qe=new WeakMap,er=new WeakMap,Ot=new WeakSet,on=function(t){const n=r=>{switch(t.type){case"failed":return{...r,failureCount:t.failureCount,failureReason:t.error};case"pause":return{...r,isPaused:!0};case"continue":return{...r,isPaused:!1};case"pending":return{...r,context:t.context,data:void 0,failureCount:0,failureReason:null,error:null,isPaused:t.isPaused,status:"pending",variables:t.variables,submittedAt:Date.now()};case"success":return{...r,data:t.data,failureCount:0,failureReason:null,error:null,status:"success",isPaused:!1};case"error":return{...r,data:void 0,error:t.error,failureCount:r.failureCount+1,failureReason:t.error,isPaused:!1,status:"error"}}};this.state=n(this.state),Ce.batch(()=>{y(this,Dt).forEach(r=>{r.onMutationUpdate(t)}),y(this,Qe).notify({mutation:this,type:"updated",action:t})})},wd);function Wp(){return{context:void 0,data:void 0,error:null,failureCount:0,failureReason:null,isPaused:!1,status:"idle",variables:void 0,submittedAt:0}}var Kt,Nt,ls,_d,fy=(_d=class extends li{constructor(t={}){super();b(this,Kt);b(this,Nt);b(this,ls);this.config=t,O(this,Kt,new Set),O(this,Nt,new Map),O(this,ls,0)}build(t,n,r){const i=new dy({client:t,mutationCache:this,mutationId:++vs(this,ls)._,options:t.defaultMutationOptions(n),state:r});return this.add(i),i}add(t){y(this,Kt).add(t);const n=Ls(t);if(typeof n=="string"){const r=y(this,Nt).get(n);r?r.push(t):y(this,Nt).set(n,[t])}this.notify({type:"added",mutation:t})}remove(t){if(y(this,Kt).delete(t)){const n=Ls(t);if(typeof n=="string"){const r=y(this,Nt).get(n);if(r)if(r.length>1){const i=r.indexOf(t);i!==-1&&r.splice(i,1)}else r[0]===t&&y(this,Nt).delete(n)}}this.notify({type:"removed",mutation:t})}canRun(t){const n=Ls(t);if(typeof n=="string"){const r=y(this,Nt).get(n),i=r==null?void 0:r.find(s=>s.state.status==="pending");return!i||i===t}else return!0}runNext(t){var r;const n=Ls(t);if(typeof n=="string"){const i=(r=y(this,Nt).get(n))==null?void 0:r.find(s=>s!==t&&s.state.isPaused);return(i==null?void 0:i.continue())??Promise.resolve()}else return Promise.resolve()}clear(){Ce.batch(()=>{y(this,Kt).forEach(t=>{this.notify({type:"removed",mutation:t})}),y(this,Kt).clear(),y(this,Nt).clear()})}getAll(){return Array.from(y(this,Kt))}find(t){const n={exact:!0,...t};return this.getAll().find(r=>Gc(n,r))}findAll(t={}){return this.getAll().filter(n=>Gc(t,n))}notify(t){Ce.batch(()=>{this.listeners.forEach(n=>{n(t)})})}resumePausedMutations(){const t=this.getAll().filter(n=>n.state.isPaused);return Ce.batch(()=>Promise.all(t.map(n=>n.continue().catch(Ve))))}},Kt=new WeakMap,Nt=new WeakMap,ls=new WeakMap,_d);function Ls(e){var t;return(t=e.options.scope)==null?void 0:t.id}var Ht,vn,Xe,Vt,Yt,Ws,Pa,Nd,py=(Nd=class extends li{constructor(n,r){super();b(this,Yt);b(this,Ht);b(this,vn);b(this,Xe);b(this,Vt);O(this,Ht,n),this.setOptions(r),this.bindMethods(),K(this,Yt,Ws).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(n){var i;const r=this.options;this.options=y(this,Ht).defaultMutationOptions(n),So(this.options,r)||y(this,Ht).getMutationCache().notify({type:"observerOptionsUpdated",mutation:y(this,Xe),observer:this}),r!=null&&r.mutationKey&&this.options.mutationKey&&ar(r.mutationKey)!==ar(this.options.mutationKey)?this.reset():((i=y(this,Xe))==null?void 0:i.state.status)==="pending"&&y(this,Xe).setOptions(this.options)}onUnsubscribe(){var n;this.hasListeners()||(n=y(this,Xe))==null||n.removeObserver(this)}onMutationUpdate(n){K(this,Yt,Ws).call(this),K(this,Yt,Pa).call(this,n)}getCurrentResult(){return y(this,vn)}reset(){var n;(n=y(this,Xe))==null||n.removeObserver(this),O(this,Xe,void 0),K(this,Yt,Ws).call(this),K(this,Yt,Pa).call(this)}mutate(n,r){var i;return O(this,Vt,r),(i=y(this,Xe))==null||i.removeObserver(this),O(this,Xe,y(this,Ht).getMutationCache().build(y(this,Ht),this.options)),y(this,Xe).addObserver(this),y(this,Xe).execute(n)}},Ht=new WeakMap,vn=new WeakMap,Xe=new WeakMap,Vt=new WeakMap,Yt=new WeakSet,Ws=function(){var r;const n=((r=y(this,Xe))==null?void 0:r.state)??Wp();O(this,vn,{...n,isPending:n.status==="pending",isSuccess:n.status==="success",isError:n.status==="error",isIdle:n.status==="idle",mutate:this.mutate,reset:this.reset})},Pa=function(n){Ce.batch(()=>{var r,i,s,o,l,a,u,h;if(y(this,Vt)&&this.hasListeners()){const f=y(this,vn).variables,d=y(this,vn).context,m={client:y(this,Ht),meta:this.options.meta,mutationKey:this.options.mutationKey};if((n==null?void 0:n.type)==="success"){try{(i=(r=y(this,Vt)).onSuccess)==null||i.call(r,n.data,f,d,m)}catch($){Promise.reject($)}try{(o=(s=y(this,Vt)).onSettled)==null||o.call(s,n.data,null,f,d,m)}catch($){Promise.reject($)}}else if((n==null?void 0:n.type)==="error"){try{(a=(l=y(this,Vt)).onError)==null||a.call(l,n.error,f,d,m)}catch($){Promise.reject($)}try{(h=(u=y(this,Vt)).onSettled)==null||h.call(u,void 0,n.error,f,d,m)}catch($){Promise.reject($)}}}this.listeners.forEach(f=>{f(y(this,vn))})})},Nd),Lt,Cd,hy=(Cd=class extends li{constructor(t={}){super();b(this,Lt);this.config=t,O(this,Lt,new Map)}build(t,n,r){const i=n.queryKey,s=n.queryHash??Eu(i,n);let o=this.get(s);return o||(o=new ly({client:t,queryKey:i,queryHash:s,options:t.defaultQueryOptions(n),state:r,defaultOptions:t.getQueryDefaults(i)}),this.add(o)),o}add(t){y(this,Lt).has(t.queryHash)||(y(this,Lt).set(t.queryHash,t),this.notify({type:"added",query:t}))}remove(t){const n=y(this,Lt).get(t.queryHash);n&&(t.destroy(),n===t&&y(this,Lt).delete(t.queryHash),this.notify({type:"removed",query:t}))}clear(){Ce.batch(()=>{this.getAll().forEach(t=>{this.remove(t)})})}get(t){return y(this,Lt).get(t)}getAll(){return[...y(this,Lt).values()]}find(t){const n={exact:!0,...t};return this.getAll().find(r=>Wc(n,r))}findAll(t={}){const n=this.getAll();return Object.keys(t).length>0?n.filter(r=>Wc(t,r)):n}notify(t){Ce.batch(()=>{this.listeners.forEach(n=>{n(t)})})}onFocus(){Ce.batch(()=>{this.getAll().forEach(t=>{t.onFocus()})})}onOnline(){Ce.batch(()=>{this.getAll().forEach(t=>{t.onOnline()})})}},Lt=new WeakMap,Cd),ye,yn,gn,qr,Wr,kn,Gr,Yr,Ed,my=(Ed=class{constructor(e={}){b(this,ye);b(this,yn);b(this,gn);b(this,qr);b(this,Wr);b(this,kn);b(this,Gr);b(this,Yr);O(this,ye,e.queryCache||new hy),O(this,yn,e.mutationCache||new fy),O(this,gn,e.defaultOptions||{}),O(this,qr,new Map),O(this,Wr,new Map),O(this,kn,0)}mount(){vs(this,kn)._++,y(this,kn)===1&&(O(this,Gr,Cu.subscribe(async e=>{e&&(await this.resumePausedMutations(),y(this,ye).onFocus())})),O(this,Yr,wo.subscribe(async e=>{e&&(await this.resumePausedMutations(),y(this,ye).onOnline())})))}unmount(){var e,t;vs(this,kn)._--,y(this,kn)===0&&((e=y(this,Gr))==null||e.call(this),O(this,Gr,void 0),(t=y(this,Yr))==null||t.call(this),O(this,Yr,void 0))}isFetching(e){return y(this,ye).findAll({...e,fetchStatus:"fetching"}).length}isMutating(e){return y(this,yn).findAll({...e,status:"pending"}).length}getQueryData(e){var n;const t=this.defaultQueryOptions({queryKey:e});return(n=y(this,ye).get(t.queryHash))==null?void 0:n.state.data}ensureQueryData(e){const t=this.defaultQueryOptions(e),n=y(this,ye).build(this,t),r=n.state.data;return r===void 0?this.fetchQuery(e):(e.revalidateIfStale&&n.isStaleByTime(xn(t.staleTime,n))&&this.prefetchQuery(t),Promise.resolve(r))}getQueriesData(e){return y(this,ye).findAll(e).map(({queryKey:t,state:n})=>{const r=n.data;return[t,r]})}setQueryData(e,t,n){const r=this.defaultQueryOptions({queryKey:e}),i=y(this,ye).get(r.queryHash),s=i==null?void 0:i.state.data,o=Gv(t,s);if(o!==void 0)return y(this,ye).build(this,r).setData(o,{...n,manual:!0})}setQueriesData(e,t,n){return Ce.batch(()=>y(this,ye).findAll(e).map(({queryKey:r})=>[r,this.setQueryData(r,t,n)]))}getQueryState(e){var n;const t=this.defaultQueryOptions({queryKey:e});return(n=y(this,ye).get(t.queryHash))==null?void 0:n.state}removeQueries(e){const t=y(this,ye);Ce.batch(()=>{t.findAll(e).forEach(n=>{t.remove(n)})})}resetQueries(e,t){const n=y(this,ye);return Ce.batch(()=>(n.findAll(e).forEach(r=>{r.reset()}),this.refetchQueries({type:"active",...e},t)))}cancelQueries(e,t={}){const n={revert:!0,...t},r=Ce.batch(()=>y(this,ye).findAll(e).map(i=>i.cancel(n)));return Promise.all(r).then(Ve).catch(Ve)}invalidateQueries(e,t={}){return Ce.batch(()=>(y(this,ye).findAll(e).forEach(n=>{n.invalidate()}),(e==null?void 0:e.refetchType)==="none"?Promise.resolve():this.refetchQueries({...e,type:(e==null?void 0:e.refetchType)??(e==null?void 0:e.type)??"active"},t)))}refetchQueries(e,t={}){const n={...t,cancelRefetch:t.cancelRefetch??!0},r=Ce.batch(()=>y(this,ye).findAll(e).filter(i=>!i.isDisabled()&&!i.isStatic()).map(i=>{let s=i.fetch(void 0,n);return n.throwOnError||(s=s.catch(Ve)),i.state.fetchStatus==="paused"?Promise.resolve():s}));return Promise.all(r).then(Ve)}fetchQuery(e){const t=this.defaultQueryOptions(e);t.retry===void 0&&(t.retry=!1);const n=y(this,ye).build(this,t);return n.isStaleByTime(xn(t.staleTime,n))?n.fetch(t):Promise.resolve(n.state.data)}prefetchQuery(e){return this.fetchQuery(e).then(Ve).catch(Ve)}fetchInfiniteQuery(e){return e._type="infinite",this.fetchQuery(e)}prefetchInfiniteQuery(e){return this.fetchInfiniteQuery(e).then(Ve).catch(Ve)}ensureInfiniteQueryData(e){return e._type="infinite",this.ensureQueryData(e)}resumePausedMutations(){return wo.isOnline()?y(this,yn).resumePausedMutations():Promise.resolve()}getQueryCache(){return y(this,ye)}getMutationCache(){return y(this,yn)}getDefaultOptions(){return y(this,gn)}setDefaultOptions(e){O(this,gn,e)}setQueryDefaults(e,t){y(this,qr).set(ar(e),{queryKey:e,defaultOptions:t})}getQueryDefaults(e){const t=[...y(this,qr).values()],n={};return t.forEach(r=>{es(e,r.queryKey)&&Object.assign(n,r.defaultOptions)}),n}setMutationDefaults(e,t){y(this,Wr).set(ar(e),{mutationKey:e,defaultOptions:t})}getMutationDefaults(e){const t=[...y(this,Wr).values()],n={};return t.forEach(r=>{es(e,r.mutationKey)&&Object.assign(n,r.defaultOptions)}),n}defaultQueryOptions(e){if(e._defaulted)return e;const t={...y(this,gn).queries,...this.getQueryDefaults(e.queryKey),...e,_defaulted:!0};return t.queryHash||(t.queryHash=Eu(t.queryKey,t)),t.refetchOnReconnect===void 0&&(t.refetchOnReconnect=t.networkMode!=="always"),t.throwOnError===void 0&&(t.throwOnError=!!t.suspense),!t.networkMode&&t.persister&&(t.networkMode="offlineFirst"),t.queryFn===Iu&&(t.enabled=!1),t}defaultMutationOptions(e){return e!=null&&e._defaulted?e:{...y(this,gn).mutations,...(e==null?void 0:e.mutationKey)&&this.getMutationDefaults(e.mutationKey),...e,_defaulted:!0}}clear(){y(this,ye).clear(),y(this,yn).clear()}},ye=new WeakMap,yn=new WeakMap,gn=new WeakMap,qr=new WeakMap,Wr=new WeakMap,kn=new WeakMap,Gr=new WeakMap,Yr=new WeakMap,Ed),Gp={exports:{}},jo={};/** + * @license React + * react-jsx-runtime.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var vy=N,yy=Symbol.for("react.element"),gy=Symbol.for("react.fragment"),ky=Object.prototype.hasOwnProperty,$y=vy.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,Sy={key:!0,ref:!0,__self:!0,__source:!0};function Yp(e,t,n){var r,i={},s=null,o=null;n!==void 0&&(s=""+n),t.key!==void 0&&(s=""+t.key),t.ref!==void 0&&(o=t.ref);for(r in t)ky.call(t,r)&&!Sy.hasOwnProperty(r)&&(i[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps,t)i[r]===void 0&&(i[r]=t[r]);return{$$typeof:yy,type:e,key:s,ref:o,props:i,_owner:$y.current}}jo.Fragment=gy;jo.jsx=Yp;jo.jsxs=Yp;Gp.exports=jo;var wy=Gp.exports,Jp=N.createContext(void 0),it=e=>{const t=N.useContext(Jp);if(!t)throw new Error("No QueryClient set, use QueryClientProvider to set one");return t},rd=({client:e,children:t})=>(N.useEffect(()=>(e.mount(),()=>{e.unmount()}),[e]),wy.jsx(Jp.Provider,{value:e,children:t})),Xp=N.createContext(!1),_y=()=>N.useContext(Xp);Xp.Provider;function Ny(){let e=!1;return{clearReset:()=>{e=!1},reset:()=>{e=!0},isReset:()=>e}}var Cy=N.createContext(Ny()),Ey=()=>N.useContext(Cy),Iy=(e,t,n)=>{const r=n!=null&&n.state.error&&typeof e.throwOnError=="function"?Pu(e.throwOnError,[n.state.error,n]):e.throwOnError;(e.suspense||e.experimental_prefetchInRender||r)&&(t.isReset()||(e.retryOnMount=!1))},Py=e=>{N.useEffect(()=>{e.clearReset()},[e])},Ty=({result:e,errorResetBoundary:t,throwOnError:n,query:r,suspense:i})=>e.isError&&!t.isReset()&&!e.isFetching&&r&&(i&&e.data===void 0||Pu(n,[e.error,r])),xy=e=>{if(e.suspense){const n=i=>i==="static"?i:Math.max(i??1e3,1e3),r=e.staleTime;e.staleTime=typeof r=="function"?(...i)=>n(r(...i)):n(r),typeof e.gcTime=="number"&&(e.gcTime=Math.max(e.gcTime,1e3))}},Ry=(e,t)=>e.isLoading&&e.isFetching&&!t,Dy=(e,t)=>(e==null?void 0:e.suspense)&&t.isPending,id=(e,t,n)=>t.fetchOptimistic(e).catch(()=>{n.clearReset()});function Oy(e,t,n){var d,m,$,k;const r=_y(),i=Ey(),s=it(),o=s.defaultQueryOptions(e);(m=(d=s.getDefaultOptions().queries)==null?void 0:d._experimental_beforeQuery)==null||m.call(d,o);const l=s.getQueryCache().get(o.queryHash);o._optimisticResults=r?"isRestoring":"optimistic",xy(o),Iy(o,i,l),Py(i);const a=!s.getQueryCache().get(o.queryHash),[u]=N.useState(()=>new t(s,o)),h=u.getOptimisticResult(o),f=!r&&e.subscribed!==!1;if(N.useSyncExternalStore(N.useCallback(w=>{const p=f?u.subscribe(Ce.batchCalls(w)):Ve;return u.updateResult(),p},[u,f]),()=>u.getCurrentResult(),()=>u.getCurrentResult()),N.useEffect(()=>{u.setOptions(o)},[o,u]),Dy(o,h))throw id(o,u,i);if(Ty({result:h,errorResetBoundary:i,throwOnError:o.throwOnError,query:l,suspense:o.suspense}))throw h.error;if((k=($=s.getDefaultOptions().queries)==null?void 0:$._experimental_afterQuery)==null||k.call($,o,h),o.experimental_prefetchInRender&&!ts.isServer()&&Ry(h,r)){const w=a?id(o,u,i):l==null?void 0:l.promise;w==null||w.catch(Ve).finally(()=>{u.updateResult()})}return o.notifyOnChangeProps?h:u.trackResult(h)}function ht(e,t){return Oy(e,ay)}function ct(e,t){const n=it(),[r]=N.useState(()=>new py(n,e));N.useEffect(()=>{r.setOptions(e)},[r,e]);const i=N.useSyncExternalStore(N.useCallback(o=>r.subscribe(Ce.batchCalls(o)),[r]),()=>r.getCurrentResult(),()=>r.getCurrentResult()),s=N.useCallback((o,l)=>{r.mutate(o,l).catch(Ve)},[r]);if(i.error&&Pu(r.options.throwOnError,[i.error]))throw i.error;return{...i,mutate:s,mutateAsync:i.mutate}}var Zp=function(e,t,n,r){var i;t[0]=0;for(var s=1;s=5&&((o||!d&&s===5)&&(a.push(s,0,o,i),s=6),d&&(a.push(s,d,0,i),s=6)),o=""},h=0;h"?(s=1,o=""):o=r+o[0]:l?r===l?l="":o+=r:r==='"'||r==="'"?l=r:r===">"?(u(),s=1):s&&(r==="="?(s=5,i=o,o=""):r==="/"&&(s<5||n[h][f+1]===">")?(u(),s===3&&(a=a[0]),s=a,(a=a[0]).push(2,0,s),s=0):r===" "||r===" "||r===` +`||r==="\r"?(u(),s=2):o+=r),s===3&&o==="!--"&&(s=4,a=a[0])}return u(),a}(e)),t),arguments,[])).length>1?t:t[0]}const S=Ly.bind(bh.createElement),bt=[{ui:"love",api:"love",icon:"❤️",label:"Love"},{ui:"like",api:"like",icon:"👍",label:"Like"},{ui:"maybe",api:"curious",icon:"🤔",label:"Maybe"},{ui:"not_interested",api:"not_interested",icon:"🙈",label:"Hide"},{ui:"no_go",api:"hard_no",icon:"🚫",label:"No-go"}],Qo=[{key:"to_me",label:"To Me",icon:"⬅️"},{key:"by_me",label:"By Me",icon:"➡️"},{key:"together",label:"Together",icon:"🤝"}],je={DISCOVER:"discover",SCENARIOS:"scenarios",PLAYS:"plays",TOGETHER:"together",SETTINGS:"settings"};function xu(e,t){const n=((e==null?void 0:e.name)||"").trim()||"Shared list";if(!t)return n;const r=((e==null?void 0:e.participant_ids)||[]).filter(s=>s&&s!==t);if(!r.length)return n;const i=s=>s.length>12?`${s.slice(0,10)}…`:s;return`${n} — ${r.map(i).join(" · ")}`}function eh(e,t){if(!t)return"";const n=((e==null?void 0:e.participant_ids)||[]).filter(r=>r&&r!==t);return n.length?`Partners: ${n.join(", ")}`:""}const th=["to_me","by_me","together"],nh=new Set(th),My=th;function rh(e,t){const n=[...new Set((e||[]).filter(r=>nh.has(r)))].sort();return t==="together"?["together"]:!n.length||n.length===1&&n[0]==="together"?[t]:n.includes(t)?n:[...new Set([...n.filter(r=>r!=="together"),t])].sort()}function Fy(e,t){var r;const n=(((r=t[e])==null?void 0:r.directions)||[]).filter(i=>nh.has(i));return n.length?n:["together"]}const od=280;function zy(e){const t=String(e||"").trimStart();return t?t.startsWith("=5&&/^<\s*html[\s>]/i.test(t.slice(0,32))?!0:t.startsWith("od?`${t.slice(0,od)}…`:t:""}function ld(e,t,n){const r=String(t||"").trim();if(zy(r))return`Server error (${e}) — the response was a web page, not JSON. Try again in a moment.`;if(n&&typeof n.detail=="string"&&n.detail.trim())return Sl(n.detail);if(n&&n.detail!=null)try{return Sl(JSON.stringify(n.detail))}catch{return`Request failed (${e})`}return Sl(r)||`Request failed (${e})`}async function Z(e,t={}){const n=await fetch(e,t),r=await n.text();let i;try{i=r?JSON.parse(r):{}}catch{if(!n.ok){const s=ld(n.status,r,null),o=new Error(s);throw o.status=n.status,o.payload=null,o}return{}}if(!n.ok){const s=ld(n.status,r,i),o=new Error(s);throw o.status=n.status,o.payload=i,o}return i}function ke(e){return e?{"x-private-token":e}:{}}function tn(e){return e==="love"?"love":e==="like"?"like":e==="curious"?"maybe":e==="not_interested"?"not_interested":e==="hard_no"||e==="no"?"no_go":""}function by(e,t){var n;return((n=e==null?void 0:e[t])==null?void 0:n.directions)||[]}const ad={play:"Play",role:"Role",dynamic:"Dynamic",meta:null};function jy(e){if(!e||typeof e!="object")return null;const t=e.content_kind;return t!=null&&Object.prototype.hasOwnProperty.call(ad,t)?ad[t]:null}function Qy({assets:e,imageRevealed:t,kinkName:n,onReveal:r,resetKey:i}){const[s,o]=N.useState(0),l=(Array.isArray(e)?e:[]).filter(d=>d&&typeof d.asset_url=="string"&&d.asset_url),a=l.length;N.useEffect(()=>{o(0)},[i]),N.useEffect(()=>{o(d=>a?Math.min(Math.max(0,d),a-1):0)},[a]);const u=N.useCallback((d,m)=>{var $,k;($=m==null?void 0:m.preventDefault)==null||$.call(m),(k=m==null?void 0:m.stopPropagation)==null||k.call(m),!(a<2)&&o(w=>(w+d+a)%a)},[a]);if(!a)return null;const h=Math.min(Math.max(0,s),a-1),f=l[h];return f!=null&&f.asset_url?S` +
+
+ ${a>1?S` + +
+ `} +
+
+ + `:null}function zr({text:e}){return S`
${e||""}
`}function ns({current:e,onRate:t,size:n="normal",disabled:r=!1}){const i=n==="compact"?bt.filter(s=>s.ui!=="not_interested"):bt;return S` +
+ ${i.map(s=>S` + + `)} +
+ `}function Ay({onRate:e}){return S` +
+ ${[{api:"love",icon:"❤️",label:"Love",cls:"love"},{api:"like",icon:"👍",label:"Like",cls:"like"},{api:"curious",icon:"🤔",label:"Maybe",cls:"maybe"},{api:"not_interested",icon:"➡️",label:"Skip",cls:"skip"},{api:"hard_no",icon:"🚫",label:"No-go",cls:"nogo"}].map(n=>S` + + `)} +
+ `}function Uy(e){return(e.detail_summary||e.definition||e.summary||"").trim()}function By(e){return(e.detail_summary||e.definition||e.summary||"").trim()}function Ky(e){const t=(e.definition||"").trim();return t||(e.detail_summary||e.summary||"").trim()}function Hy({kink:e,onRevealImage:t,imageRevealed:n,photosEnabled:r=!0,layout:i="default",infoExpanded:s=!1,onToggleInfo:o}){const l=(e.assets||[]).filter(m=>m&&typeof m.asset_url=="string"&&m.asset_url),a=r&&l.length>0,u=jy(e),h=Uy(e),f=S` + ${e.is_extreme?S`
Significant risk
`:e.risk_level==="advanced"?S`
Advanced
`:null} + `,d=S` + ${u?S`
${u}
`:null} +

${e.name}

+ ${h?S`

${h}

`:null} + ${f} + `;if(i==="tinder"){const m=!!s,$=(e.notes||"").trim(),k=By(e),w=Ky(e),c=typeof o=="function"&&(!!$||!!w&&(!k||w!==k)),v=S` + ${u?S`
${u}
`:null} +

${e.name}

+ ${m?S` + ${w?S`

${w}

`:null} + ${$?S`
${$}
`:null} + `:k?S`

${k}

`:null} + ${f} + ${c?S``:null} + `,g=a?S` +
+ <${Qy} + resetKey=${e.id} + assets=${l} + imageRevealed=${n} + kinkName=${e.name} + onReveal=${t} + /> +
+ `:S` +
+
${(e.name||"?").slice(0,2).toUpperCase()}
+
+
+ `,_=`tinder-card__body${c?" tinder-card__body--tappable":""}${m?" tinder-card__body--expanded":""}`,I=c?m?S` +
o()} + onKeyDown=${E=>{(E.key==="Enter"||E.key===" ")&&(E.preventDefault(),o())}} + > + ${v} +
+ `:S` + + `:S`
${v}
`;return S` +
+ ${g} + ${I} +
+ `}return S` +
+ ${d} + ${a?n?S` +
+ ${l.map((m,$)=>S` +
+ +
+ `)} +
+ `:S` +
+ + Show +
+ `:null} +
+ `}function ih({name:e,emoji:t,expanded:n,onClick:r,children:i,nearDupHint:s}){return S` +
+
+ ${t} + ${e} + ${s?S``:null} +
+ ${n&&i?S`
o.stopPropagation()}>${i}
`:null} +
+ `}function Vy({directions:e,locked:t,onToggle:n}){return S` +
+ ${Qo.map(r=>S` + + `)} +
+ `}function sh({query:e,onQueryChange:t,results:n,onSelect:r,onClose:i,hint:s}){return S` +
+
o.stopPropagation()}> + t(o.target.value)} + /> + ${s?S`

${s}

`:null} +
+ ${n.map(o=>{var l;return S` +
r(o.kink)}> + ${o.kink.name} + ${(l=o.kink.summary)!=null&&l.trim()?S`${o.kink.summary.trim()}`:null} +
+ `})} + ${e&&!n.length?S`
No results
`:null} +
+
+
+ `}function _o({kink:e,selected:t,onToggle:n}){return S` +
+ ${e.name} + +
+ `}const Fn={retry:!1,refetchOnWindowFocus:!1},ud=12e3,qy=1500;function Wy(e){var n,r;return e&&(((n=e.incoming_partner_requests)==null?void 0:n.length)||0)+(((r=e.outgoing_partner_requests)==null?void 0:r.length)||0)>0?qy:ud}function Gy(e,t={}){const n=!!t.pauseUserRefetch;return ht({queryKey:["user",e.userId],queryFn:()=>Z(`/users/${e.userId}`,{headers:ke(e.token)}),enabled:!!(e.userId&&e.token),...Fn,refetchOnWindowFocus:!0,refetchInterval:r=>n?!1:Wy(r.state.data),refetchIntervalInBackground:!1})}function Yy(e){return ht({queryKey:["board",e.userId],queryFn:()=>Z(`/users/${e.userId}/play-board`,{headers:ke(e.token)}),enabled:!!(e.userId&&e.token),...Fn})}function Jy(e){return ht({queryKey:["scenario-parents",e.userId],queryFn:()=>Z(`/users/${e.userId}/scenario-parents`,{headers:ke(e.token)}),enabled:!!(e.userId&&e.token),...Fn})}function oh(e){return ht({queryKey:["partner-groups",e.userId],queryFn:()=>Z(`/users/${e.userId}/partner-groups`,{headers:ke(e.token)}),enabled:!!(e.userId&&e.token),...Fn})}function Xy(e,t,n){return ht({queryKey:["recs",e.userId,t],queryFn:()=>Z(`/users/${e.userId}/recommendations?limit=24${t?`&group_id=${encodeURIComponent(t)}`:""}`,{headers:ke(e.token)}),enabled:n,staleTime:45e3,...Fn})}function lh(e){return ht({queryKey:["search",e],queryFn:()=>Z(`/search?q=${encodeURIComponent(e)}&limit=20`),enabled:!!e.trim()})}function Zy(e){return ht({queryKey:["role-search",e],queryFn:()=>Z(`/roles/search?q=${encodeURIComponent(e)}&limit=12`),enabled:!!e.trim()})}function eg(e){return ht({queryKey:["roles",e.userId],queryFn:()=>Z(`/users/${e.userId}/roles`,{headers:ke(e.token)}),enabled:!!(e.userId&&e.token),...Fn})}function ah(e){return ht({queryKey:["kink",e],queryFn:()=>Z(`/kinks/${e}`),enabled:!!e})}function tg(e){return ht({queryKey:["similar",e],queryFn:()=>Z(`/kinks/${e}/similar?limit=10`),enabled:!!e})}function ng(e){return ht({queryKey:["scenarios",e],queryFn:()=>Z(`/kinks/${e}/scenarios?limit=48`),enabled:!!e})}function rg(e,t){return ht({queryKey:["group-overlap",e.userId,t],queryFn:()=>Z(`/users/${e.userId}/partner-groups/${encodeURIComponent(t)}/overlap`,{headers:ke(e.token)}),enabled:!!(e.userId&&e.token&&t),...Fn})}function ig(e,t,n){return ht({queryKey:["partner-board",e.userId,t,n],queryFn:()=>Z(`/users/${e.userId}/partner-groups/${encodeURIComponent(t)}/partner-board/${encodeURIComponent(n)}`,{headers:ke(e.token)}),enabled:!!(e.userId&&e.token&&t&&n),...Fn})}const uh={to_me:[],by_me:[],together:[],hidden:[],no_go:[]};function sg(e){return[...e].sort((t,n)=>(n.popularity||0)-(t.popularity||0)||t.name.localeCompare(n.name))}function ch(e,t,n,r,i,s){const o=String(t),l=Object.fromEntries(Object.entries(e||uh).map(([f,d])=>[f,(d||[]).filter(m=>String(m.id)!==o)])),h={...og(e,o)||(i?lg(i,s,o):null)||{id:o,name:o,summary:"",popularity:0},interest_state:n,directions:r};if(n==="hard_no")l.no_go.push(h);else if(n==="not_interested")l.hidden.push(h);else{const f=r.length?r:["together"];for(const d of f)l[d]&&l[d].push({...h})}for(const f of Object.keys(l))l[f]=sg(l[f]);return l}function og(e,t){const n=String(t);for(const r of Object.values(e||uh)){const i=(r||[]).find(s=>String(s.id)===n);if(i)return i}return null}function Gs(e){return!(e!=null&&e.id)||!(e!=null&&e.name)?null:{id:e.id,name:e.name,summary:e.summary||e.definition||e.detail_summary||"",popularity:e.popularity||0}}function cd(e,t){var i;const n=String(t),r=(i=e==null?void 0:e.items)==null?void 0:i.find(s=>{var o;return String((o=s==null?void 0:s.kink)==null?void 0:o.id)===n});return Gs((r==null?void 0:r.kink)||null)}function lg(e,t,n){var u,h,f;const r=e.getQueryData(["kink",n]),i=Gs(r);if(i)return i;const s=[["recs",t==null?void 0:t.userId,""],["prompts",t==null?void 0:t.userId,""]];for(const d of s){const m=cd(e.getQueryData(d),n);if(m)return m}const o=e.getQueryCache(),l=o.findAll({predicate:d=>{const m=d.queryKey||[];return m[0]==="recs"||m[0]==="prompts"||m[0]==="search"}});for(const d of l){const m=cd((u=d.state)==null?void 0:u.data,n);if(m)return m}const a=o.findAll({predicate:d=>(d.queryKey||[])[0]==="group-overlap"});for(const d of a){const m=((h=d.state)==null?void 0:h.data)||{},$=(m.direct_matches||[]).find(p=>(p==null?void 0:p.id)===n);if($)return Gs($);const k=(f=(m.similar_matches||[]).find(p=>{var c;return((c=p==null?void 0:p.kink)==null?void 0:c.id)===n}))==null?void 0:f.kink,w=Gs(k);if(w)return w}return null}function dh(e,t){if(!(e!=null&&e.items))return e;const n=String(t);return{...e,items:e.items.filter(r=>{var i;return String((i=r==null?void 0:r.kink)==null?void 0:i.id)!==n})}}function dd(e,t,n=[]){return`${e}::${t}::${[...n||[]].sort().join("|")}`}const ag={to_me:[],by_me:[],together:[],hidden:[],no_go:[]};function ug({setAuth:e,setCreateResult:t,setAuthOpen:n,setStatus:r}){const i=it();return ct({mutationFn:({user_id:s,private_token:o})=>Z("/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user_id:s,private_token:o})}),onSuccess:(s,o)=>{e({userId:o.user_id,token:o.private_token}),t(null),i.setQueryData(["user",o.user_id],s.user),r("Profile open.")},onError:s=>r(s.message)})}function cg({setAuth:e,setCreateResult:t,setLoginForm:n,setStatus:r}){return ct({mutationFn:()=>Z("/users",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"}),onMutate:()=>{r("Creating profile… If this sits here a while, the Space is still loading the catalog (first boot can take several minutes).")},onSuccess:i=>{e({userId:i.id,token:i.private_token}),t(i),n({user_id:i.id,private_token:i.private_token}),r("Profile created. Choose roles, then open your Play List.")},onError:i=>r(i.message)})}function dg({auth:e,activeGroupId:t,selectedKinkId:n,setSelectedKinkId:r,setStatus:i,pendingPlayCommitRef:s,playCommitKey:o,syncPostSaveCaches:l,onSaveStart:a=()=>{},onSaveSettled:u=()=>{}}){const h=it();return ct({mutationKey:["savePlay"],mutationFn:({kinkId:f,interestState:d,directions:m})=>Z(`/users/${e.userId}/plays`,{method:"POST",headers:{"Content-Type":"application/json",...ke(e.token)},body:JSON.stringify({kink_id:f,interest_state:d,directions:m})}),onMutate:async f=>{a();const d=f.directions||[],m=["recs",e.userId,t],$=h.getQueryData(["user",e.userId]),k=h.getQueryData(["board",e.userId]),w=h.getQueryData(m),p=String(f.kinkId);return h.setQueryData(["user",e.userId],c=>{const v=c&&typeof c=="object"?c:{id:e.userId,plays:{}};return{...v,plays:{...v.plays||{},[p]:{interest_state:f.interestState,directions:d}}}}),w!=null&&w.items&&h.setQueryData(m,c=>dh(c,p)),h.setQueryData(["board",e.userId],c=>ch(c||k||ag,p,f.interestState,d,h,e)),i("Saving..."),{previousUser:$,previousBoard:k,previousRecs:w,recsKey:m,commitKey:o(p,f.interestState,d)}},onSuccess:(f,d,m)=>{l(f,String(d.kinkId),d.interestState,d.directions||[]),h.invalidateQueries({queryKey:["board",e.userId]}),i("Saved.")},onError:(f,d,m)=>{m!=null&&m.previousUser&&h.setQueryData(["user",e.userId],m.previousUser),m!=null&&m.previousBoard&&h.setQueryData(["board",e.userId],m.previousBoard),m!=null&&m.recsKey&&h.setQueryData(m.recsKey,m.previousRecs),i(f.message)},onSettled:(f,d,m,$)=>{u(),$!=null&&$.commitKey&&s.current===$.commitKey&&(s.current="")}})}function fg({auth:e,setStatus:t}){const n=it();return ct({mutationFn:({kinkId:r,selected:i})=>Z(`/users/${e.userId}/roles`,{method:"POST",headers:{"Content-Type":"application/json",...ke(e.token)},body:JSON.stringify({kink_id:r,selected:i})}),onSuccess:()=>{n.invalidateQueries({queryKey:["user",e.userId]}),n.invalidateQueries({queryKey:["roles",e.userId]}),t("Role saved.")},onError:r=>t(r.message)})}function pg({auth:e,setStatus:t}){const n=it();return ct({mutationFn:r=>Z(`/users/${e.userId}/preferences`,{method:"POST",headers:{"Content-Type":"application/json",...ke(e.token)},body:JSON.stringify({show_images:r})}),onSuccess:r=>n.setQueryData(["user",e.userId],r),onError:r=>t(r.message)})}function hg({auth:e,activeGroupId:t,setStatus:n}){const r=it();return ct({mutationFn:({parentKinkId:i,scenarioKinkId:s,interestState:o,directions:l=[],remove:a=!1})=>a?Z(`/users/${e.userId}/scenario-preferences/${encodeURIComponent(s)}`,{method:"DELETE",headers:ke(e.token)}):Z(`/users/${e.userId}/scenario-preferences`,{method:"POST",headers:{"Content-Type":"application/json",...ke(e.token)},body:JSON.stringify({parent_kink_id:i,scenario_kink_id:s,interest_state:o,directions:l})}),onSuccess:i=>{r.setQueryData(["user",e.userId],i),r.invalidateQueries({queryKey:["scenario-parents",e.userId]}),t&&r.invalidateQueries({queryKey:["group-overlap",e.userId,t]}),n("Scenario saved.")},onError:i=>n(i.message)})}function fh(e,t,n){(n==null?void 0:n.partner_groups)!=null&&e.setQueryData(["partner-groups",t],{items:n.partner_groups})}function mg({auth:e,setGroupDraftMembers:t,setGroupNameInput:n,setStatus:r}){const i=it();return ct({mutationFn:s=>Z(`/users/${e.userId}/partners`,{method:"POST",headers:{"Content-Type":"application/json",...ke(e.token)},body:JSON.stringify({partner_id:s})}),onSuccess:(s,o)=>{var l;i.setQueryData(["user",e.userId],s),fh(i,e.userId,s),i.invalidateQueries({queryKey:["partner-groups",e.userId]}),(l=s.partners)!=null&&l.includes(o)?r(`You're linked. A shared list "Together" was created for both of you.`):r('Link request sent. When they accept, you will both get a shared "Together" list.')},onError:s=>r(s.message)})}function vg({auth:e,setStatus:t}){const n=it(),r=l=>{n.setQueryData(["user",e.userId],l),fh(n,e.userId,l),n.invalidateQueries({queryKey:["partner-groups",e.userId]})},i=ct({mutationFn:l=>Z(`/users/${e.userId}/partner-requests/accept`,{method:"POST",headers:{"Content-Type":"application/json",...ke(e.token)},body:JSON.stringify({from_user_id:l})}),onSuccess:l=>{r(l),t('Linked. A shared "Together" list is ready — you can add more lists anytime.')},onError:l=>t(l.message)}),s=ct({mutationFn:l=>Z(`/users/${e.userId}/partner-requests/decline`,{method:"POST",headers:{"Content-Type":"application/json",...ke(e.token)},body:JSON.stringify({from_user_id:l})}),onSuccess:l=>{r(l),t("Request declined.")},onError:l=>t(l.message)}),o=ct({mutationFn:l=>Z(`/users/${e.userId}/partner-requests/cancel`,{method:"POST",headers:{"Content-Type":"application/json",...ke(e.token)},body:JSON.stringify({to_user_id:l})}),onSuccess:l=>{r(l),t("Request withdrawn.")},onError:l=>t(l.message)});return{accept:i,decline:s,cancel:o}}function yg({auth:e,setActiveGroupId:t,setGroupNameInput:n,setGroupDraftMembers:r,setStatus:i}){const s=it();return ct({mutationFn:({name:o,memberIds:l})=>Z(`/users/${e.userId}/partner-groups`,{method:"POST",headers:{"Content-Type":"application/json",...ke(e.token)},body:JSON.stringify({name:o,member_ids:l})}),onSuccess:o=>{s.invalidateQueries({queryKey:["user",e.userId]}),s.invalidateQueries({queryKey:["partner-groups",e.userId]}),t(o.id),i("Group created.")},onError:o=>i(o.message)})}function gg({auth:e,removeMemberFromGroupCache:t,setStatus:n}){const r=it();return ct({mutationFn:({groupId:i,memberId:s})=>Z(`/users/${e.userId}/partner-groups/${encodeURIComponent(i)}/members/${encodeURIComponent(s)}`,{method:"DELETE",headers:ke(e.token)}),onSuccess:(i,s)=>{t(s.groupId,s.memberId),r.invalidateQueries({queryKey:["group-overlap",e.userId,s.groupId]}),r.invalidateQueries({queryKey:["recs",e.userId,s.groupId]}),n("Group updated.")},onError:i=>n(i.message)})}function kg({auth:e,replacePartnerGroup:t,setStatus:n}){const r=it();return ct({mutationFn:({groupId:i,share:s})=>Z(`/users/${e.userId}/partner-groups/${encodeURIComponent(i)}/share-toggle`,{method:"POST",headers:{"Content-Type":"application/json",...ke(e.token)},body:JSON.stringify({share:s})}),onSuccess:(i,s)=>{t({id:s.groupId,my_share_full_list:s.share}),n("Sharing preference updated."),r.invalidateQueries({queryKey:["group-overlap",e.userId,s.groupId]})},onError:i=>n(i.message)})}const ph=new Set(["to_me","by_me","together"]);function Fi(e=[]){const t=[...new Set((e||[]).filter(n=>ph.has(n)))];return t.length===0?["together"]:t.length===1?t:["together"]}function $g(e,t){return ph.has(t)?Fi(e)[0]===t?["together"]:[t]:["together"]}function Sg(e,t){var s;const n=t||{},r=new Set,i=[];for(const o of e||[]){const l=(s=o==null?void 0:o.kink)==null?void 0:s.id;if(l==null||l==="")continue;const a=String(l);n[a]||r.has(a)||(r.add(a),i.push(o))}return i}function wg({groupSelectionReady:e,isSuccess:t,isFetching:n,visibleCount:r,attempts:i,maxAttempts:s}){return!!(e&&t&&!n&&r===0&&i0){document.getElementsByTagName("link");const o=document.querySelector("meta[property=csp-nonce]"),l=(o==null?void 0:o.nonce)||(o==null?void 0:o.getAttribute("nonce"));i=Promise.allSettled(n.map(a=>{if(a=Ng(a),a in fd)return;fd[a]=!0;const u=a.endsWith(".css"),h=u?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${a}"]${h}`))return;const f=document.createElement("link");if(f.rel=u?"stylesheet":_g,u||(f.as="script"),f.crossOrigin="",f.href=a,l&&f.setAttribute("nonce",l),document.head.appendChild(f),u)return new Promise((d,m)=>{f.addEventListener("load",d),f.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${a}`)))})}))}function s(o){const l=new Event("vite:preloadError",{cancelable:!0});if(l.payload=o,window.dispatchEvent(l),!l.defaultPrevented)throw o}return i.then(o=>{for(const l of o||[])l.status==="rejected"&&s(l.reason);return t().catch(s)})},Eg="/telemetry";function gr(e){try{navigator.sendBeacon(Eg,JSON.stringify(e))}catch{}}function Ig(e){gr(e)}function Pg(){Cg(async()=>{const{onCLS:n,onLCP:r,onINP:i}=await import("./web-vitals.attribution-C_HyTrBb.js");return{onCLS:n,onLCP:r,onINP:i}},[]).then(({onCLS:n,onLCP:r,onINP:i})=>{n(s=>{var o,l;s.value>.05&&gr({type:"cls",value:s.value,target:((o=s.attribution)==null?void 0:o.largestShiftTarget)||null,sources:((l=s.attribution)==null?void 0:l.largestShiftSource)||null})}),r(s=>{var o,l;gr({type:"lcp",value:s.value,element:((o=s.attribution)==null?void 0:o.element)||null,url:((l=s.attribution)==null?void 0:l.url)||null})}),i(s=>{var o;s.value>200&&gr({type:"inp",value:s.value,target:((o=s.attribution)==null?void 0:o.interactionTarget)||null})})}).catch(()=>{});const e=["tab-bar","action-bar"],t=new ResizeObserver(n=>{var r;for(const i of n){const s=((r=i.target.dataset)==null?void 0:r.watchId)||i.target.className,o=i.target.getBoundingClientRect();gr({type:"resize",id:s,width:Math.round(o.width),height:Math.round(o.height)})}});requestAnimationFrame(()=>{for(const n of e){const r=document.querySelector(`[data-watch-id="${n}"]`)||document.querySelector(`.${n}`);r&&t.observe(r)}});try{new PerformanceObserver(r=>{for(const i of r.getEntries())i.duration>200&&gr({type:"slow_interaction",name:i.name,duration:Math.round(i.duration)})}).observe({type:"event",buffered:!0,durationThreshold:100})}catch{}}function pd({auth:e,activeGroupId:t,groupSelectionReady:n,partnerGroups:r,onActiveGroupChange:i,photosOnCards:s,onPhotoPreference:o,onRate:l,onNavigate:a,onOpenScenarios:u,savedCount:h,currentPlays:f,discoverSheetDirections:d=["together"],onDiscoverSheetDirectionsChange:m=()=>{},starterMode:$=!1,starterFinalizing:k=!1,starterTarget:w=12,showSearch:p=!0,headerTitle:c="Discover",headerSubtitle:v=""}){var ps,ui,ci,pr,di;const[g,_]=N.useState(0),[I,E]=N.useState(!1),[C,P]=N.useState(!1),[T,H]=N.useState(""),[J,le]=N.useState(""),[ce,Le]=N.useState(!1),[Be,Me]=N.useState(!1),D=N.useRef(!1),[F,Q]=N.useState(null),W=N.useRef(null),A=it(),ie=$?"":t,re=$?!!(e.userId&&e.token):n,q=Xy(e,ie,re),Se=((ps=q.data)==null?void 0:ps.items)||[],xe=N.useRef(0),L=N.useMemo(()=>Sg(Se,f),[Se,f]),te=N.useMemo(()=>L.map(j=>{var _e;return(_e=j==null?void 0:j.kink)==null?void 0:_e.id}).filter(Boolean).join("\0"),[L]);N.useEffect(()=>{if(!q.isSuccess||!te)return;const j=[...new Set(te.split("\0").filter(Boolean))].slice(0,32);if(!j.length)return;let _e=!1;const jt=performance.now();return(async()=>{var hs;try{const{items:hr}=await Z("/kinks/batch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({ids:j})});if(_e)return;const fi=Math.round(performance.now()-jt);for(const Ye of hr||[])Ye!=null&&Ye.id&&A.setQueryData(["kink",Ye.id],Ye);Ig({type:"discover_kinks_batch_ms",ms:fi,requested:j.length,returned:(hr||[]).length});try{((hs=globalThis.localStorage)==null?void 0:hs.getItem("kink_debug_timing"))==="1"&&console.info(`[kink] discover kinks/batch ${fi}ms (${(hr||[]).length}/${j.length} rows)`)}catch{}}catch{}})(),()=>{_e=!0}},[q.isSuccess,te,A]),N.useEffect(()=>{if(L.length>0){xe.current=0;return}wg({groupSelectionReady:re,isSuccess:q.isSuccess,isFetching:q.isFetching,visibleCount:L.length,attempts:xe.current,maxAttempts:8})&&(xe.current+=1,q.refetch())},[re,q.isSuccess,q.isFetching,L.length,q.refetch]);const me=L.length?Math.min(g,L.length-1):0,se=L[me]||null,zn=(ui=se==null?void 0:se.kink)==null?void 0:ui.id,Fe=zn==null||zn===""?"":String(zn),dr=ah(Fe),ve=N.useMemo(()=>{const j=se==null?void 0:se.kink,_e=dr.data,jt=_e&&String(_e.id)===Fe?_e:null;return!j&&!jt?null:jt&&j&&j.id===jt.id?{...j,...jt}:j||jt},[Fe,se,dr.data]),bn=N.useMemo(()=>Fi(d),[d])[0],Uo=N.useCallback(j=>{m(_e=>$g(_e,j))},[m]),Bo=lh(J),fr=Math.min(h,w),jn=$?k?"Finishing your starter picks…":`${fr} / ${w} starter picks`:h{E(!1),Le(!1),D.current=!1,Me(!1)},[Fe]),N.useEffect(()=>{_(0)},[ie,$]),N.useEffect(()=>{me!==g&&_(me)},[me,g]);const Ko=N.useCallback(async j=>{var _e;if(Fe&&!D.current){D.current=!0,Me(!0);try{await Promise.resolve(l(Fe,j)),m(["together"]),E(!1),Le(!1),["love","like","curious"].includes(j)&&Number((ve==null?void 0:ve.scenario_child_count)||0)>0?Q({parentKinkId:Fe,parentName:(ve==null?void 0:ve.name)||((_e=se==null?void 0:se.kink)==null?void 0:_e.name)||"this kink",scenarioCount:Number((ve==null?void 0:ve.scenario_child_count)||0)}):(F==null?void 0:F.parentKinkId)===Fe&&Q(null)}finally{D.current=!1,Me(!1)}}},[Fe,l,m,ve==null?void 0:ve.scenario_child_count,ve==null?void 0:ve.name,(pr=se==null?void 0:se.kink)==null?void 0:pr.name,F==null?void 0:F.parentKinkId]),Ho=N.useCallback(j=>{H(j),W.current&&clearTimeout(W.current),W.current=setTimeout(()=>le(j.trim()),300)},[]),Vo=N.useCallback(j=>{P(!1),H(""),le(""),a("plays",j.id)},[a]),qo=ve?S`<${Hy} + kink=${ve} + photosEnabled=${s} + imageRevealed=${I} + onRevealImage=${()=>E(!0)} + layout="tinder" + infoExpanded=${ce} + onToggleInfo=${()=>Le(j=>!j)} + />`:null;return S` +
+
+
+
${c}
+
${v||jn}
+
+
+
${h} saved
+ ${p?S``:null} +
+
+ + ${r!=null&&r.length&&!$?S` +
+
Partner context for discovery
+
+ ${r.map(j=>S` + + `)} +
+
+ `:null} + + ${F&&!$?S` +
+
+ ${F.parentName} +
+ ${F.scenarioCount} optional scenario${F.scenarioCount===1?"":"s"} available. +
+
+ +
+ `:null} + + ${k?S`

Finishing your starter picks…

`:null} + ${!k&&!ve&&re&&q.isSuccess&&L.length===0&&!q.isFetching?S`

Nothing new to show right now.

`:null} + ${!k&&!ve&&!(re&&q.isSuccess&&L.length===0&&!q.isFetching)?S`

${q.isError?(()=>{var _e;const j=String(((_e=q.error)==null?void 0:_e.message)||"").trim();return j?j.startsWith("<")||j.length>400?"Could not load recommendations (server error).":j:"Could not load recommendations."})():re&&q.isSuccess&&L.length===0&&q.isFetching?"Loading recommendations…":!re&&e.userId&&e.token?"Loading your profile…":"Loading…"}

`:null} + + ${!k&&ve?S` + +
${qo}
+ ${ai?S` +
+ + ${ai} + +
+ `:null} +

+ Tap the bottom of the card for the full description. Multiple photos: tap the + left or right edge of the image to browse. +

+
+
Love / Like / Maybe → My Plays column
+
+ ${Qo.map(j=>S` + + `)} +
+
+ <${Ay} onRate=${Ko} /> + `:null} + + ${C&&p?S` + <${sh} + query=${T} + onQueryChange=${Ho} + results=${((di=Bo.data)==null?void 0:di.items)||[]} + hint="Tap a result to open it on My Plays." + onSelect=${Vo} + onClose=${()=>{P(!1),H(""),le("")}} + /> + `:null} +
+ `}const hd={love:0,like:1,curious:2,"":3,not_interested:4,hard_no:5};function Tg(e=[],t={},n=""){const r=[...e||[]];return r.sort((i,s)=>{const o=i.scenario||i.kink||i,l=s.scenario||s.kink||s,a=t==null?void 0:t[o.id],u=t==null?void 0:t[l.id],h=(a==null?void 0:a.parent_kink_id)===n&&a.interest_state||"",f=(u==null?void 0:u.parent_kink_id)===n&&u.interest_state||"",d=hd[h]??9,m=hd[f]??9;return d!==m?d-m:String(o.name||"").localeCompare(String(l.name||""))}),r}function xg({auth:e,currentPlays:t,scenarioPreferences:n={},onScenarioRate:r,onNavigateToPlays:i,focusParentKinkId:s="",focusScenarioKinkId:o="",onScenarioFocusConsumed:l}){var g,_,I,E;const[a,u]=N.useState(""),[h,f]=N.useState(""),d=Jy(e),m=((g=d.data)==null?void 0:g.items)||[],$=N.useMemo(()=>{const C=a.trim().toLowerCase();return C?m.filter(P=>{var T;return String(((T=P.parent_kink)==null?void 0:T.name)||"").toLowerCase().includes(C)}):m},[m,a]);N.useEffect(()=>{s&&(f(s),l==null||l())},[s,l]),N.useEffect(()=>{if(!$.length){h&&f("");return}$.some(C=>{var P;return((P=C.parent_kink)==null?void 0:P.id)===h})||f($[0].parent_kink.id)},[$,h]);const k=$.find(C=>{var P;return((P=C.parent_kink)==null?void 0:P.id)===h})||m.find(C=>{var P;return((P=C.parent_kink)==null?void 0:P.id)===h})||$[0]||null,w=(k==null?void 0:k.parent_kink)||null,p=ng((w==null?void 0:w.id)||""),c=((_=t[(w==null?void 0:w.id)||""])==null?void 0:_.directions)||(k==null?void 0:k.directions)||["together"],v=N.useMemo(()=>{var C;return Tg(((C=p.data)==null?void 0:C.items)||[],n,(w==null?void 0:w.id)||"")},[p.data,n,w==null?void 0:w.id]);return d.isError?S` +
+
+
+
Scenarios
+
Optional scene variations under the base kinks you already like.
+
+
+

Could not load scenarios (${((I=d.error)==null?void 0:I.message)||"error"}).

+
+ `:S` +
+
+
+
Scenarios
+
Optional scene variations under the base kinks you already like.
+
+
+ + ${!m.length&&!d.isFetching?S` +
+ No scenario bases yet +
+ Love or Like a base kink first. Then its optional scenarios will show up here. +
+
+ `:null} + + ${m.length?S` +
+
+
+ Base kinks +
Only kinks you Love or Like appear here.
+
+ u(C.target.value)} + /> +
+
+ ${$.length?$.map(C=>{const P=C.parent_kink,T=P.id===(w==null?void 0:w.id);return S` + + `}):S`
No matching base kinks.
`} +
+
+ `:null} + + ${w?S` +
+
+
+
Base kink
+

${w.name}

+
+ Saved under ${((E=t[w.id])==null?void 0:E.interest_state)==="love"?"Love":"Like"}. +
+
+ +
+ ${p.isFetching?S`
Loading scenarios…
`:null} + ${!p.isFetching&&!v.length?S`
No scenarios available under this base kink.
`:null} +
+ ${v.map(C=>{const P=C.scenario||C.kink||C,T=n==null?void 0:n[P.id],H=(T==null?void 0:T.parent_kink_id)===w.id,J=tn(H?T==null?void 0:T.interest_state:"");return S` +
+
+
${P.name}
+ ${P.summary?S`
${P.summary}
`:null} +
+
+ <${ns} + current=${J} + onRate=${le=>{var ce;return r(w.id,P.id,le,H&&((ce=T==null?void 0:T.directions)!=null&&ce.length)?T.directions:c)}} + /> +
+
+ `})} +
+
+ `:null} +
+ `}const Rg=new Set(["to_me","by_me","together"]);function Dg(e=[]){const t=[];for(const n of e||[])!Rg.has(n)||t.includes(n)||t.push(n);return t.length?t:["together"]}function Og(e,t,n=[]){return{shouldTentativeAdd:!(e!=null&&e[t]),directions:Dg(n)}}function Lg(e){let t=String(e||"").trim().toLowerCase();if(!t)return"";try{t=t.normalize("NFKD").replace(/[\u0300-\u036f]/g,"")}catch{}return t=t.replace(/[\u2018\u2019\u201c\u201d`´]/g,"'"),t=t.replace(/\s+/g," "),t=t.replace(/\bgood\s+morning\s+/g,"goodmorning "),t=t.replace(/\bdeep\s*throating\b/g,"deepthroat"),t=t.replace(/\bdeepthroating\b/g,"deepthroat"),t=t.replace(/\bdeep\s*throat\b/g,"deepthroat"),t=t.replace(/\bblow\s+jobs\b/gi,"blowjob"),t=t.replace(/\bblow\s+job\b/gi,"blowjob"),t=t.replace(/\bblowjobs\b/g,"blowjob"),t=t.replace(/\bstrap\s+ons\b/gi,"strapon"),t=t.replace(/\bstrap\s+on\b/gi,"strapon"),t=t.replace(/\bstrapons\b/g,"strapon"),t=t.replace(/[^a-z0-9\s]/g," "),t=t.replace(/\s+/g," ").trim(),t=t.replace(/\s+/g,""),t}function Mg(e){const t=new Map;for(const r of e){const i=Lg(r.name);i.length<4||(t.has(i)||t.set(i,[]),t.get(i).push(r))}const n=new Map;for(const r of t.values()){if(r.length<2)continue;const i=[...new Set(r.map(s=>s.name))];for(const s of r){const o=i.filter(l=>l!==s.name);o.length&&n.set(s.id,`Near-duplicate title in your list: ${o.join(" · ")}`)}}return n}const Ru=[...My];function hh({kinkId:e,onDirectionColumnChange:t}){return S` +
+ ${Qo.map(n=>S` + + `)} +
+ `}function mh(e){const t=new Set,n=[];for(const r of Ru)for(const i of e[r]||[])t.has(i.id)||(t.add(i.id),n.push(i));return n}function Fg(e){const t=new Map;for(const n of[...Ru,"hidden","no_go"])for(const r of(e==null?void 0:e[n])||[])!(r!=null&&r.id)||t.has(r.id)||t.set(r.id,r);return t}function zg(e){return((e==null?void 0:e.items)||[]).filter(t=>(t==null?void 0:t.kink)&&!t.kink.is_scenario).slice(0,6)}function bg({similarKinkId:e,parentDirections:t,currentPlays:n,onRate:r,onInspect:i}){const s=Og(n,e,t);s.shouldTentativeAdd&&r(e,"curious",s.directions),i(e)}function jg({board:e,currentPlays:t,inspectedId:n,onInspect:r,onDirectionColumnChange:i,nearDupHints:s,dragOver:o,setDragOver:l,onDragStart:a,onDragEnd:u}){const h=N.useMemo(()=>mh(e),[e]),f=N.useMemo(()=>{const w={to_me:[],by_me:[],together:[]};for(const p of h)for(const c of Fy(p.id,t))w[c]&&w[c].push(p);for(const p of Ru)w[p].sort((c,v)=>(v.popularity||0)-(c.popularity||0)||c.name.localeCompare(v.name));return w},[h,t]),d=N.useCallback((w,p)=>{w.preventDefault(),w.dataTransfer.dropEffect="move",l(p)},[l]),m=N.useCallback(w=>{w.currentTarget.contains(w.relatedTarget)||l(null)},[]),$=N.useCallback((w,p)=>{var E,C,P;w.preventDefault();const c=w.dataTransfer.getData("text/plain");if(l(null),!c)return;const v=[...new Set((((E=t[c])==null?void 0:E.directions)||[]).filter(Boolean))].sort(),g=[...rh((C=t[c])==null?void 0:C.directions,p)].sort(),_=JSON.stringify(v)!==JSON.stringify(g),I=((P=t[c])==null?void 0:P.interest_state)==="not_interested";(_||I)&&i(c,p)},[t,i,l]),k=(w,p)=>{const c=tn(w.interest_state),v=bt.find(_=>_.ui===c),g=n===w.id;return S` +
+ a(_,w.id)} + onDragEnd=${u} + > + ⠿ + + <${hh} kinkId=${w.id} onDirectionColumnChange=${i} /> +
+ <${ih} + name=${w.name} + emoji=${(v==null?void 0:v.icon)||""} + expanded=${g} + nearDupHint=${s.get(w.id)||""} + onClick=${()=>r(g?"":w.id)} + /> +
+
+ `};return S` +
+
+

By direction

+ ${h.length} +
+

+ Drag the ⠿ handle into a column for To me / By me / Together (works best with a mouse). On a phone, use the + buttons under each play instead. +

+
+ ${Qo.map(w=>S` +
d(p,w.key)} + onDragLeave=${m} + onDrop=${p=>$(p,w.key)} + > +
+ ${w.icon} + ${w.label} +
+
+ ${f[w.key].length?f[w.key].map(p=>k(p,w.key)):S`
Drop here
`} +
+
+ `)} +
+
+ `}function Qg({title:e,emoji:t,items:n,inspectedId:r,onInspect:i,nearDupHints:s,columnDragHandles:o=!1,onDragStart:l,onDragEnd:a,onDirectionColumnChange:u}){return n.length?S` +
+
+

${t} ${e}

+ ${n.length} +
+ ${o?S`

+ Use the direction buttons under each play on your phone, or drag ⠿ into a column above, to move it back onto your board. +

`:null} +
+ ${n.map(h=>{const f=tn(h.interest_state),d=bt.find(k=>k.ui===f),m=r===h.id,$=S` + <${ih} + name=${h.name} + emoji=${(d==null?void 0:d.icon)||""} + expanded=${m} + nearDupHint=${s.get(h.id)||""} + onClick=${()=>i(m?"":h.id)} + /> + `;return o?S` +
+ l(k,h.id)} + onDragEnd=${a} + > + ⠿ + + <${hh} kinkId=${h.id} onDirectionColumnChange=${u} /> +
${$}
+
+ `:S`
${$}
`})} +
+
+ `:null}function md(e){const{title:t,emoji:n,items:r}=e,[i,s]=N.useState(!1);return r.length?S` +
+ + ${i?S`<${Qg} ...${e} />`:null} +
+ `:null}function Ag({assets:e}){var a;const[t,n]=N.useState(!1),[r,i]=N.useState(0),s=e.length,o=e[r];N.useEffect(()=>{i(0)},[e==null?void 0:e.length,(a=e==null?void 0:e[0])==null?void 0:a.asset_url]);const l=N.useCallback((u,h)=>{var f;(f=h==null?void 0:h.stopPropagation)==null||f.call(h),!(s<2)&&i(d=>(d+u+s)%s)},[s]);return S` +
+ ${t?S` +
+ ${s>1?S` +
+ `:S` + + `} +
+ `}function Ug({item:e,play:t,detailData:n,detailLoading:r,similarData:i,onRate:s,onDirectionToggle:o,onOpenSimilar:l,onOpenScenarios:a,onClose:u,showImages:h,inspectorRef:f}){var v;if(!(e!=null&&e.id))return null;const d=tn(t==null?void 0:t.interest_state),m=bt.find(g=>g.ui===d),$=(t==null?void 0:t.directions)||[],k=(n==null?void 0:n.id)===e.id?n:null,w=((k==null?void 0:k.definition)||(k==null?void 0:k.detail_summary)||e.summary||"").trim(),p=zg(i),c=Number((k==null?void 0:k.scenario_child_count)||e.scenario_child_count||0);return S` +
+
+
+
Selected play
+
+ ${(m==null?void 0:m.icon)||""} +

${e.name}

+
+
+ +
+ ${w?S`
${w}
`:r?S`
Loading details…
`:null} +
+ <${ns} current=${d} onRate=${g=>s(e.id,g)} /> + <${Vy} + directions=${$} + locked=${!d} + onToggle=${g=>o(e.id,g)} + /> +
+
+ + +
+ ${p.length?S` +
+
Similar
+
+ ${p.map(g=>S` + + `)} +
+
+ `:null} + ${k!=null&&k.is_extreme?S`
Significant risk
`:null} + ${h&&((v=k==null?void 0:k.assets)!=null&&v.length)?S`<${Ag} assets=${k.assets} />`:null} +
+ `}function Bg({auth:e,currentPlays:t,showImages:n,onRate:r,onDirectionToggle:i,onDirectionColumnChange:s,onNavigate:o,onOpenScenarios:l,focusKinkId:a,onFocusKinkConsumed:u}){var Q,W;const[h,f]=N.useState(""),[d,m]=N.useState(null),[$,k]=N.useState(!1),[w,p]=N.useState(""),[c,v]=N.useState(""),g=N.useRef(null),_=N.useRef(null),I=Yy(e),E=I.data||{},P=N.useMemo(()=>Fg(E),[E]).get(h)||null,T=ah(h),H=tg(h),J=lh(c),le=Object.keys(t||{}).length,ce=N.useMemo(()=>{const ie=[...mh(E),...E.hidden||[],...E.no_go||[]];return Mg(ie)},[E]);N.useEffect(()=>{var A,ie;h&&((ie=(A=_.current)==null?void 0:A.scrollIntoView)==null||ie.call(A,{behavior:"smooth",block:"start"}))},[h]),N.useEffect(()=>{a&&(t[a]||r(a,"curious",["together"]),f(a),u==null||u())},[a,t,r,u]);const Le=N.useCallback((A,ie)=>{A.dataTransfer.setData("text/plain",ie),A.dataTransfer.effectAllowed="move"},[]),Be=N.useCallback(()=>m(null),[]),Me=N.useCallback(A=>{p(A),g.current&&clearTimeout(g.current),g.current=setTimeout(()=>v(A.trim()),300)},[]),D=N.useCallback((A,ie)=>{bg({similarKinkId:A,parentDirections:ie,currentPlays:t,onRate:r,onInspect:f})},[t,r]),F=le===0;return S` +
+
+
My Plays
+
+ ${le} plays + +
+
+ +
+ ${P?S` + <${Ug} + item=${P} + play=${t[P.id]||{}} + detailData=${T.data} + detailLoading=${T.isFetching} + similarData=${H.data} + onRate=${r} + onDirectionToggle=${i} + onOpenSimilar=${D} + onOpenScenarios=${l} + onClose=${()=>f("")} + showImages=${n} + inspectorRef=${_} + /> + `:null} + + ${I.isError?S`

Could not load play layout (${((Q=I.error)==null?void 0:Q.message)||"error"}). Try refreshing.

`:null} + + ${F?S`
No plays yet. Go to Discover to find things you like.
`:S` + <${jg} + board=${E} + currentPlays=${t} + inspectedId=${h} + onInspect=${f} + onDirectionColumnChange=${s} + nearDupHints=${ce} + dragOver=${d} + setDragOver=${m} + onDragStart=${Le} + onDragEnd=${Be} + /> + `} + + <${md} + title="Hidden" + emoji="\uD83D\uDE48" + items=${E.hidden||[]} + inspectedId=${h} + onInspect=${f} + nearDupHints=${ce} + columnDragHandles=${!0} + onDragStart=${Le} + onDragEnd=${Be} + onDirectionColumnChange=${s} + /> + <${md} + title="No-go" + emoji="\uD83D\uDEAB" + items=${E.no_go||[]} + inspectedId=${h} + onInspect=${f} + nearDupHints=${ce} + /> +
+ + ${$?S` + <${sh} + query=${w} + onQueryChange=${Me} + results=${((W=J.data)==null?void 0:W.items)||[]} + hint="Tap a result to add it to your list (saved as Maybe) and open it." + onSelect=${A=>{k(!1),p(""),v(""),o==null||o("plays",A.id)}} + onClose=${()=>{k(!1),p(""),v("")}} + /> + `:null} +
+ `}function vh(e,t){const n=Array.isArray(e==null?void 0:e.partner_groups)?e.partner_groups:null,r=Array.isArray(t==null?void 0:t.items)?t.items:null;return n!=null&&n.length?n:r!=null&&r.length?r:n||r||[]}function yh(e,t=""){var i;const n=Array.isArray(e)?e:[];return n.length?t&&n.some(s=>(s==null?void 0:s.id)===t)?t:((i=n.find(s=>((s==null?void 0:s.member_ids)||[]).filter(Boolean).length>0)||n[0])==null?void 0:i.id)||"":""}function Kg({auth:e,user:t,currentPlays:n,onRate:r,onNavigate:i,onOpenScenarios:s,setStatus:o,graphDebugUi:l=!1,linkMutation:a,partnerRequestMutations:u,createGroupMutation:h,shareToggleMutation:f}){var xe;const[d,m]=N.useState(""),[$,k]=N.useState(""),[w,p]=N.useState([]),[c,v]=N.useState(""),[g,_]=N.useState("shared"),[I,E]=N.useState(null),C=oh(e),P=vh(t,C.data),T=P.find(L=>L.id===c)||null,H=(T==null?void 0:T.sharing_member_ids)||[],J=N.useMemo(()=>[...H].sort(),[T==null?void 0:T.id,H.join("|")]),le=N.useMemo(()=>{if(!(t!=null&&t.id)||typeof window>"u")return"";const L=new URL(window.location.href);return L.search="",L.hash="",L.searchParams.set("link",t.id),L.toString()},[t==null?void 0:t.id]);N.useEffect(()=>{if(!J.length){E(null);return}E(L=>L&&J.includes(L)?L:J[0])},[c,J.join("|")]),N.useEffect(()=>{if(!P.length){c&&v("");return}const L=yh(P,c);L!==c&&v(L)},[c,P]),N.useEffect(()=>{g==="theirs"&&!J.length&&_("shared")},[J.length,g]);const ce=I||J[0]||null,Be=rg(e,c).data||null,Me=ig(e,c,ce),D=(t==null?void 0:t.partners)||[],F=(t==null?void 0:t.incoming_partner_requests)||[],Q=(t==null?void 0:t.outgoing_partner_requests)||[],W=D.length>0,A=((T==null?void 0:T.participant_ids)||[]).length||1+(((xe=T==null?void 0:T.member_ids)==null?void 0:xe.length)||0),{accept:ie,decline:re,cancel:q}=u||{},Se=[{key:"shared",label:"Shared"},{key:"explore",label:"Worth Exploring"},...J.length?[{key:"theirs",label:"Their List"}]:[]];return S` +
+
+
+
Together
+
Connect easily. See what overlaps. Explore nearby things.
+
+
+ +
+ ${t?S` +
+
+
+
Share link
+ ${le} +
+ +
+
+
+
Manual invite code
+ ${t.id} +
+ +
+
+ `:null} + + ${F.length?S` +
+
Wants to connect
+ ${F.map(L=>S` +
+ ${L} +
+ + +
+
+ `)} +
+ `:null} + + ${Q.length?S` +
+
Pending connection requests
+ ${Q.map(L=>S` +
+ ${L} + +
+ `)} +
+ `:null} + +
+
+ Connect with a partner +
Share link first. Manual code entry is here as a fallback.
+
+ m(L.target.value)} + /> + +
+ + ${W&&!P.length?S` +
+
Make a shared list for linked partners
+ k(L.target.value)} /> +
+ ${D.map(L=>S` + + `)} +
+ +
+ `:null} + + ${P.length?S` +
+
Connection
+
+ ${P.map(L=>S` + + `)} +
+
+ `:null} + + ${T?S` +
+
Privacy
+
${T.my_share_full_list?"They can view my full list in this connection.":"My full list stays private in this connection."}
+
${J.length?`I can view: ${J.join(", ")}`:"No one in this connection is sharing their full list yet."}
+ +
+ +
+ ${Se.map(L=>S` + + `)} +
+ +

+ ${A>2?`This connection includes you and ${A-1} others.`:"This view compares you with the connected partner(s) in this list."} +

+ + ${g==="shared"?S`<${Hg} + overlap=${Be} + currentPlays=${n} + auth=${e} + participantCount=${A} + onNavigate=${i} + onOpenScenarios=${s} + />`:null} + ${g==="explore"?S`<${Vg} + overlap=${Be} + currentPlays=${n} + auth=${e} + onRate=${r} + onOpenScenarios=${s} + graphDebugUi=${l} + />`:null} + ${g==="theirs"?S` + <${qg} + partnerBoardQuery=${Me} + boardPartnerId=${ce} + sharingPartnerIds=${J} + selectedPartnerId=${I} + onSelectPartner=${E} + currentPlays=${n} + onRate=${r} + /> + `:null} + `:W?null:S`
Link a partner to get started.
`} +
+
+ `}function Hg({overlap:e,currentPlays:t,auth:n,participantCount:r=2,onNavigate:i,onOpenScenarios:s}){const o=(e==null?void 0:e.direct_matches)||[],l=(e==null?void 0:e.scenario_matches)||[],a=r>2?"No play is shared across everyone in this connection yet.":"No shared plays yet. Both of you need a few overlapping likes first.";return!o.length&&!l.length?S`
${a}
`:S` +
+ ${o.length?S` +
Same plays
+ ${o.map(u=>{var k;const h=u.partner_reactions||{},f=tn((k=t[u.id])==null?void 0:k.interest_state),d=Object.entries(h).filter(([w])=>w!==n.userId),m=d.some(([,w])=>{const p=tn(w);return(f==="love"||f==="like")&&(p==="love"||p==="like")}),$=()=>i==null?void 0:i(je.PLAYS,u.id);return S` +
{w.key!=="Enter"&&w.key!==" "||(w.preventDefault(),$())}} + > + ${u.name} + ${m?S`Match`:null} + + ${d.map(([w,p])=>{var c;return S` + + ${w.slice(0,8)}… + ${((c=bt.find(v=>v.api===p))==null?void 0:c.icon)||""} + + `})} + +
+ `})} + `:null} + + ${l.length?S` +
+ Same scenarios +
+ ${l.map(u=>{const h=u.scenario_kink,f=u.parent_kink,d=Object.entries(u.partner_reactions||{}).filter(([$])=>$!==n.userId),m=()=>{f!=null&&f.id?s==null||s(f.id,h.id):i==null||i(je.PLAYS,h.id)};return S` +
{$.key!=="Enter"&&$.key!==" "||($.preventDefault(),m())}} + > +
+
Base kink
+
${(f==null?void 0:f.name)||"Open in My Plays"}
+
Scenario
+
${h.name}
+
+
+ + ${d.map(([$,k])=>{var w;return S` + + ${$.slice(0,8)}… + ${((w=bt.find(p=>p.api===k.interest_state))==null?void 0:w.icon)||""} + + `})} + +
Open this in Scenarios to review the details.
+
+
+ `})} + `:null} +
+ `}function Vg({overlap:e,currentPlays:t,auth:n,onRate:r,onOpenScenarios:i,graphDebugUi:s=!1}){const o=(e==null?void 0:e.related_matches)||[],l=(e==null?void 0:e.scenario_related_matches)||[],a=(e==null?void 0:e.similar_matches)||[];return!o.length&&!l.length&&!a.length?S`
Add more overlap to unlock nearby ideas worth exploring together.
`:S` +
+ ${o.length?S` +
Connected likes
+ ${o.map(u=>{var k,w,p,c,v;const h=u.left_user_id===n.userId?u.left_kink:u.right_kink,f=u.left_user_id===n.userId?u.right_kink:u.left_kink,d=u.left_user_id===n.userId?u.right_user_id:u.left_user_id,m=((w=(k=u.partner_reactions)==null?void 0:k[d])==null?void 0:w.interest_state)||"",$=((p=u.reasons)==null?void 0:p[0])||`${h.name} is related to ${f.name}`;return S` +
+ + ${h.name} ${f.name} + + ${s?S` + ${u.hops===1?"1 hop":`${u.hops} hops`} + ${u.bridge_kink?` via ${u.bridge_kink.name}`:""} + · ${$} + `:null} + + + ${d.slice(0,8)}… + ${((c=bt.find(g=>g.api===m))==null?void 0:c.icon)||""} + + + <${ns} + current=${tn((v=t[f.id])==null?void 0:v.interest_state)} + onRate=${g=>r(f.id,g,["together"])} + size="compact" + /> +
+ `})} + `:null} + + ${l.length?S` +
+ Connected scenarios +
+ ${l.map(u=>{var p,c,v,g,_,I;const h=u.parent_kink,f=u.left_user_id===n.userId?u.left_scenario:u.right_scenario,d=u.left_user_id===n.userId?u.right_scenario:u.left_scenario,m=u.left_user_id===n.userId?u.right_user_id:u.left_user_id,$=((c=(p=u.partner_reactions)==null?void 0:p[m])==null?void 0:c.interest_state)||"",k=((v=u.reasons)==null?void 0:v[0])||`Both are scenarios under ${(h==null?void 0:h.name)||"the same base kink"}`,w=()=>{h!=null&&h.id&&(d!=null&&d.id)&&(i==null||i(h.id,d.id))};return S` +
{E.key!=="Enter"&&E.key!==" "||(E.preventDefault(),w())}} + > +
+
Base kink
+
${(h==null?void 0:h.name)||"Shared base kink"}
+
You
+
${(f==null?void 0:f.name)||"Scenario"}
+
Them
+
${(d==null?void 0:d.name)||"Scenario"}
+
+
+ + + ${m.slice(0,8)}… + ${((I=bt.find(E=>E.api===$))==null?void 0:I.icon)||""} + + + ${s?S`
${k}
`:S`
Open their scenario under the same base kink.
`} +
+
+ `})} + `:null} + + ${a.length?S` +
Suggested next
+ ${a.map(u=>{var h,f;return S` +
+ ${u.kink.name} + ${s&&((h=u.reasons)!=null&&h[0])?S`${u.reasons[0]}`:null} + <${ns} + current=${tn((f=t[u.kink.id])==null?void 0:f.interest_state)} + onRate=${d=>r(u.kink.id,d,["together"])} + size="compact" + /> +
+ `})} + `:null} +
+ `}function qg({partnerBoardQuery:e,boardPartnerId:t,sharingPartnerIds:n,selectedPartnerId:r,onSelectPartner:i,currentPlays:s,onRate:o}){const{data:l,isPending:a,isError:u,error:h}=e,f=n.length>1?S` +
+
Whose shared list?
+
+ ${n.map(k=>S` + + `)} +
+
+ `:null;if(!t)return S`
${f}
No partner has turned on full-list sharing for this connection.
`;if(a)return S`
${f}
Loading…
`;if(u){const k=(h==null?void 0:h.status)===403;return S`
${f}
${k?"This partner has not shared their list here.":(h==null?void 0:h.message)||"Could not load this list."}
`}if(!l)return S`
${f}
Could not load this list.
`;const d=[...l.to_me||[],...l.by_me||[],...l.together||[]],m=new Set,$=d.filter(k=>m.has(k.id)?!1:(m.add(k.id),!0));return $.length?S` +
+ ${f} +

+ Showing shared list from ${t.slice(0,12)}… +

+
+ ${$.map(k=>{var p,c;const w=((p=bt.find(v=>v.api===k.interest_state))==null?void 0:p.icon)||"";return S` +
+ ${k.name} + Them: ${w} + <${ns} + current=${tn((c=s[k.id])==null?void 0:c.interest_state)} + onRate=${v=>o(k.id,v,["together"])} + size="compact" + /> +
+ `})} +
+
+ `:S`
${f}
This shared list is empty.
`}function Wg({auth:e,user:t,status:n,selectedRoles:r,roleSearchInput:i,onRoleSearch:s,roleSearchItems:o,saveRoleMutation:l,onPhotoPreference:a,logout:u,onDeleted:h,setStatus:f}){var v;const[d,m]=N.useState(""),[$,k]=N.useState(!1),w=N.useMemo(()=>((t==null?void 0:t.partner_groups)||[]).map(_=>{const I=(_.participant_ids||[]).filter(P=>P&&P!==e.userId),E=_.my_share_full_list?I:[],C=(_.sharing_member_ids||[]).filter(P=>P&&P!==e.userId);return{group:_,visibleTo:E,visibleFrom:C}}),[t==null?void 0:t.partner_groups,e.userId]);async function p(){try{m("export");const g=await Z(`/users/${e.userId}/export`,{headers:ke(e.token)}),_=new Blob([JSON.stringify(g,null,2)],{type:"application/json"}),I=URL.createObjectURL(_),E=document.createElement("a");E.href=I,E.download=`${e.userId}-export.json`,document.body.appendChild(E),E.click(),E.remove(),URL.revokeObjectURL(I),f("Export downloaded.")}catch(g){f(g.message)}finally{m("")}}async function c(){try{m("delete"),await Z(`/users/${e.userId}`,{method:"DELETE",headers:ke(e.token)}),f("Account deleted."),h==null||h()}catch(g){f(g.message)}finally{m(""),k(!1)}}return S` +
+
+
+
Settings
+
Privacy, roles, and account controls
+
+
+ +
+
+
+
Profile ID
+ ${e.userId} +
+ + +
+ +
+
+ Roles +
Optional. These help with framing, not matching.
+
+ s(g.target.value)} + /> + ${r.length?S` +
+ ${r.map(g=>S` + <${_o} + key=${g.id} + kink=${g} + selected=${!0} + onToggle=${(_,I)=>l.mutate({kinkId:_,selected:I})} + /> + `)} +
+ `:S`
No roles selected.
`} + ${o.length?S` +
+ ${o.map(g=>S` + <${_o} + key=${g.kink.id} + kink=${g.kink} + selected=${!1} + onToggle=${(_,I)=>l.mutate({kinkId:_,selected:I})} + /> + `)} +
+ `:null} +
+ +
+
+ Sharing +
Private by default. Full-list sharing is group-specific.
+
+ ${w.length?S` +
+ ${w.map(({group:g,visibleTo:_,visibleFrom:I})=>S` +
+
+
${xu(g,e.userId)}
+
+ ${_.length?`They can view my list: ${_.join(", ")}`:"They cannot view my full list"} +
+
+ ${I.length?`I can view: ${I.join(", ")}`:"I cannot view anyone else's full list"} +
+
+
+ `)} +
+ `:S`
No partner groups yet.
`} +
+ +
+
+ Data +
Export your data or remove the account entirely.
+
+
+ + ${$?S` + + + `:S``} +
+
+ + <${zr} text=${n} /> +
+
+ `}function Gg({starterCompleted:e,savedCount:t,starterTarget:n,pendingSaveCount:r}){return e?!0:t>=n&&r===0}function Yg({hasAuth:e,hasCreateResult:t,userLoaded:n,savedCount:r,starterTarget:i,pendingSaveCount:s,starterCompleted:o}){return!e||t||!n?"inactive":o?"main":r0?"finalizing":"main"}const Jg=new my,Xg={to_me:[],by_me:[],together:[],hidden:[],no_go:[]},Ms=12,Ta="kink_pending_partner";function Zg(){try{const e=localStorage.getItem("kink_auth");if(e){const t=JSON.parse(e);if(t.userId&&t.token)return t}}catch{}return{userId:"",token:""}}function e0(){try{const t=(new URL(window.location.href).searchParams.get("link")||"").trim();return t||(localStorage.getItem(Ta)||"").trim()}catch{return""}}function t0(e){try{e?localStorage.setItem(Ta,e):localStorage.removeItem(Ta)}catch{}try{const t=new URL(window.location.href);e?t.searchParams.set("link",e):t.searchParams.delete("link");const n=`${t.pathname}${t.search}${t.hash}`;window.history.replaceState({},"",n||"/")}catch{}}const gh="kink_graph_debug_ui";function n0(){try{return localStorage.getItem(gh)==="1"}catch{return!1}}function r0(e){try{localStorage.setItem(gh,e?"1":"0")}catch{}}function wl({enabled:e,onToggle:t}){return S` +
+ +
+ `}function i0(){var Lu,Mu,Fu;const e=it(),[t,n]=N.useState(Zg),[r,i]=N.useState(t.userId?null:"welcome"),[s,o]=N.useState(e0),[l,a]=N.useState(""),[u,h]=N.useState(je.DISCOVER),[f,d]=N.useState({user_id:"",private_token:""}),[m,$]=N.useState(null),[k,w]=N.useState(""),[p,c]=N.useState(""),[v,g]=N.useState(null),[_,I]=N.useState(""),[E,C]=N.useState(""),[P,T]=N.useState({parentKinkId:"",scenarioKinkId:""}),[H,J]=N.useState(["together"]),[le,ce]=N.useState(0),[Le,Be]=N.useState(!1),[Me,D]=N.useState(n0),F=N.useRef(""),Q=N.useRef(""),W=N.useCallback(R=>{D(R),r0(R)},[]),A=N.useCallback(R=>{n(R),R.userId&&R.token?localStorage.setItem("kink_auth",JSON.stringify(R)):localStorage.removeItem("kink_auth")},[]),ie=N.useCallback(R=>{const z=String(R||"").trim();o(z),t0(z)},[]),re=N.useCallback(()=>{A({userId:"",token:""}),e.clear(),h(je.DISCOVER),i("welcome"),I(""),C(""),T({parentKinkId:"",scenarioKinkId:""}),J(["together"]),$(null),ce(0),Be(!1),a("")},[e,A]),q=Gy(t,{pauseUserRefetch:le>0}),Se=oh(t),xe=eg(t),L=Zy(p),te=q.data||null,me=(te==null?void 0:te.plays)||{},se=(te==null?void 0:te.scenario_preferences)||{},zn=((Lu=te==null?void 0:te.preferences)==null?void 0:Lu.show_images)??!0,Fe=Object.keys(me).length,dr=((Mu=xe.data)==null?void 0:Mu.items)||[],ve=new Set(dr.map(R=>R.id)),Ao=(((Fu=L.data)==null?void 0:Fu.items)||[]).filter(R=>!ve.has(R.kink.id)),bn=vh(te,Se.data),Uo=!t.userId||!t.token||Se.isSuccess||Se.isError,Bo=!!(t.userId&&t.token)&&Uo&&(!bn.length||!!_),fr=Yg({hasAuth:!!(t.userId&&t.token),hasCreateResult:!!m,userLoaded:q.isSuccess,savedCount:Fe,starterTarget:Ms,pendingSaveCount:le,starterCompleted:Le}),jn=fr==="starter"||fr==="finalizing",ai=jn?"":_;N.useEffect(()=>{var z;if(m||!t.userId||!t.token||!q.isError||q.isFetching)return;const R=(z=q.error)==null?void 0:z.status;(R===401||R===404)&&(a("That profile is gone. Create a new one or log in again."),re())},[m,t.userId,t.token,q.isError,q.isFetching,q.error,re]),N.useEffect(()=>{if(m||!t.userId||!t.token)return;const R=z=>{var B,U;return z.isError&&!z.isFetching&&(((B=z.error)==null?void 0:B.status)===401||((U=z.error)==null?void 0:U.status)===404)};(R(Se)||R(xe))&&(a("That profile is gone. Create a new one or log in again."),re())},[m,t.userId,t.token,Se.isError,Se.isFetching,Se.error,xe.isError,xe.isFetching,xe.error,re]),N.useEffect(()=>{if(!t.userId||!t.token){ce(0),Be(!1);return}m||!q.isSuccess||Be(R=>Gg({starterCompleted:R,savedCount:Fe,starterTarget:Ms,pendingSaveCount:le}))},[t.userId,t.token,m,q.isSuccess,Fe,le]),N.useEffect(()=>{if(!bn.length){_&&I("");return}const R=yh(bn,_);R!==_&&I(R)},[_,bn]);function Ko(R){e.setQueryData(["partner-groups",t.userId],z=>z!=null&&z.items?{...z,items:z.items.map(B=>B.id===R.id?{...B,...R}:B)}:z),e.setQueryData(["user",t.userId],z=>z!=null&&z.partner_groups?{...z,partner_groups:z.partner_groups.map(B=>B.id===R.id?{...B,...R}:B)}:z)}function Ho(R,z){const B=U=>U.id!==R?U:{...U,member_ids:(U.member_ids||[]).filter(ne=>ne!==z),participant_ids:(U.participant_ids||[]).filter(ne=>ne!==z),sharing_member_ids:(U.sharing_member_ids||[]).filter(ne=>ne!==z)};e.setQueryData(["partner-groups",t.userId],U=>U!=null&&U.items?{...U,items:U.items.map(B)}:U),e.setQueryData(["user",t.userId],U=>U!=null&&U.partner_groups?{...U,partner_groups:U.partner_groups.map(B)}:U)}function Vo(R,z,B,U){e.setQueryData(["user",t.userId],R),e.setQueryData(["board",t.userId],ne=>ch(ne||Xg,z,B,U,e,t)),e.setQueryData(["recs",t.userId,ai],ne=>dh(ne,z)),e.invalidateQueries({queryKey:["scenario-parents",t.userId]}),_&&e.invalidateQueries({queryKey:["group-overlap",t.userId,_]})}const qo=ug({setAuth:A,setCreateResult:$,setAuthOpen:()=>{},setStatus:a}),ps=cg({setAuth:A,setCreateResult:$,setLoginForm:d,setStatus:a}),ui=dg({auth:t,activeGroupId:ai,selectedKinkId:"",setSelectedKinkId:()=>{},setStatus:a,pendingPlayCommitRef:F,playCommitKey:dd,syncPostSaveCaches:Vo,onSaveStart:()=>ce(R=>R+1),onSaveSettled:()=>ce(R=>Math.max(0,R-1))}),ci=fg({auth:t,setStatus:a}),pr=pg({auth:t,setStatus:a}),di=hg({auth:t,activeGroupId:_,setStatus:a}),j=mg({auth:t,setGroupDraftMembers:()=>{},setGroupNameInput:()=>{},setStatus:a}),_e=vg({auth:t,setStatus:a}),jt=yg({auth:t,setActiveGroupId:I,setGroupNameInput:()=>{},setGroupDraftMembers:()=>{},setStatus:a});gg({auth:t,removeMemberFromGroupCache:Ho,setStatus:a});const hs=kg({auth:t,replacePartnerGroup:Ko,setStatus:a});N.useEffect(()=>{if(!(!s||!t.userId||!t.token||!te)){if(s===t.userId){ie(""),a("You cannot connect a profile to itself.");return}if((te.partners||[]).includes(s)){ie(""),a("That partner is already connected.");return}if((te.outgoing_partner_requests||[]).includes(s)){ie(""),a("Connection request already sent.");return}if((te.incoming_partner_requests||[]).includes(s)){ie(""),a("They already requested a connection. Open Together to accept.");return}Q.current===s||j.isPending||(Q.current=s,j.mutate(s,{onSettled:()=>{Q.current="",ie("")}}))}},[s,t.userId,t.token,te,ie,j]);function hr(R,z=[]){const B=[...new Set((z||[]).filter(Boolean))].sort();return B.length?B:["love","like","curious"].includes(R)?[...new Set(Fi(H).filter(Boolean))].sort():[]}const fi=N.useCallback(R=>{J(z=>{const B=Fi(z),U=typeof R=="function"?R(B):R;return Fi(U)})},[]),Ye=N.useCallback((R,z,B=null)=>{const U=String(R),ne=me[U]||null,rn=[...hr(z,B??by(me,U))].sort(),vr=[...(ne==null?void 0:ne.directions)||[]].sort();if(!jn&&(ne==null?void 0:ne.interest_state)===z&&JSON.stringify(vr)===JSON.stringify(rn))return Promise.resolve(null);const zu=dd(U,z,rn);return!jn&&F.current===zu?Promise.resolve(null):(F.current=zu,ui.mutateAsync({kinkId:U,interestState:z,directions:rn}))},[me,H,jn,ui]),kh=N.useCallback((R,z)=>{const B=me[R];if(!(B!=null&&B.interest_state)){a("Rate this play first.");return}const U=B.directions||[];let ne;if(U.includes(z)?ne=U.filter(rn=>rn!==z):U.length===1&&U[0]==="together"&&(z==="to_me"||z==="by_me")?ne=[z]:ne=[...U,z],!ne.length){a("Choose at least one direction.");return}const mr=B.interest_state==="not_interested"?"curious":B.interest_state;Ye(R,mr,ne)},[me,Ye]),$h=N.useCallback((R,z)=>{const B=me[R];if(!(B!=null&&B.interest_state)){a("Rate this play first.");return}if(!["to_me","by_me","together"].includes(z))return;const U=rh(B.directions,z),ne=[...new Set((B.directions||[]).filter(Boolean))].sort(),mr=[...U].sort();if(JSON.stringify(ne)===JSON.stringify(mr)&&B.interest_state!=="not_interested")return;const vr=B.interest_state==="not_interested"?"curious":B.interest_state;Ye(R,vr,U)},[me,Ye]),Sh=N.useCallback((R,z,B,U=null)=>{var vr;const ne=se[z]||null,mr=((vr=me[R])==null?void 0:vr.directions)||[],rn=hr(B,U??(ne==null?void 0:ne.directions)??mr);return di.mutateAsync({parentKinkId:R,scenarioKinkId:z,interestState:B,directions:rn})},[se,me,di]),ms=N.useCallback((R,z="")=>{T({parentKinkId:R||"",scenarioKinkId:z||""}),h(je.SCENARIOS)},[]),pi=N.useCallback((R,z)=>{h(R),z&&C(z)},[]),Du=N.useCallback(R=>{w(R),v&&clearTimeout(v),g(setTimeout(()=>c(R.trim()),300))},[v]);if(!t.userId||!t.token||m)return S` + <${s0} + step=${r||"welcome"} + setStep=${i} + loginForm=${f} + setLoginForm=${d} + createResult=${m} + status=${l} + loginMutation=${qo} + createMutation=${ps} + saveRoleMutation=${ci} + roleSearchInput=${k} + onRoleSearch=${Du} + roleSearchItems=${Ao} + selectedRoles=${dr} + pendingPartnerId=${s} + onFinish=${()=>{const R=t.userId;$(null),i(null),R&&(e.invalidateQueries({queryKey:["user",R]}),e.invalidateQueries({queryKey:["partner-groups",R]}),e.invalidateQueries({queryKey:["roles",R]}))}} + /> + `;if(t.userId&&t.token&&q.isLoading)return S` +
+ <${wl} enabled=${Me} onToggle=${W} /> +
+
Loading your profile…
+ <${zr} text=${l} /> +
+
+ `;if(jn)return S` +
+ <${wl} enabled=${Me} onToggle=${W} /> +
+
+
Start broad
+

${fr==="finalizing"?"Finishing your starter picks. Full discovery unlocks as soon as those saves land.":`Rate ${Ms} common starter cards first. After that, discovery opens up and partner influence can join in.`}

+ ${s?S`

Connection invite saved for ${s}. It will be sent automatically once your profile is ready.

`:null} +
+ +
+ <${pd} + auth=${t} + activeGroupId="" + groupSelectionReady=${!0} + partnerGroups=${[]} + onActiveGroupChange=${()=>{}} + photosOnCards=${zn} + onPhotoPreference=${R=>pr.mutate(R)} + onRate=${Ye} + onNavigate=${pi} + onOpenScenarios=${ms} + savedCount=${Fe} + currentPlays=${me} + discoverSheetDirections=${H} + onDiscoverSheetDirectionsChange=${fi} + starterMode=${!0} + starterFinalizing=${fr==="finalizing"} + starterTarget=${Ms} + showSearch=${!1} + headerTitle="Starter deck" + headerSubtitle="Rate a dozen broad starters before full discovery unlocks." + /> + <${zr} text=${l} /> +
+ `;const Ou=[{key:je.DISCOVER,label:"Discover",icon:"🔥"},{key:je.SCENARIOS,label:"Scenarios",icon:"🎬"},{key:je.PLAYS,label:"My Plays",icon:"🎵",badge:Fe||null},{key:je.TOGETHER,label:"Together",icon:"🤝"},{key:je.SETTINGS,label:"Settings",icon:"⚙️"}];return S` +
+ <${wl} enabled=${Me} onToggle=${W} /> + + + ${u===je.DISCOVER?S` + <${pd} + auth=${t} + activeGroupId=${_} + groupSelectionReady=${Bo} + partnerGroups=${bn} + onActiveGroupChange=${I} + photosOnCards=${zn} + onPhotoPreference=${R=>pr.mutate(R)} + onRate=${Ye} + onNavigate=${pi} + onOpenScenarios=${ms} + savedCount=${Fe} + currentPlays=${me} + discoverSheetDirections=${H} + onDiscoverSheetDirectionsChange=${fi} + /> + `:null} + + ${u===je.SCENARIOS?S` + <${xg} + auth=${t} + currentPlays=${me} + scenarioPreferences=${se} + onScenarioRate=${Sh} + onNavigateToPlays=${R=>pi(je.PLAYS,R)} + focusParentKinkId=${P.parentKinkId} + focusScenarioKinkId=${P.scenarioKinkId} + onScenarioFocusConsumed=${()=>T({parentKinkId:"",scenarioKinkId:""})} + /> + `:null} + + ${u===je.PLAYS?S` + <${Bg} + auth=${t} + currentPlays=${me} + showImages=${zn} + onRate=${Ye} + onDirectionToggle=${kh} + onDirectionColumnChange=${$h} + onNavigate=${pi} + onOpenScenarios=${ms} + focusKinkId=${E} + onFocusKinkConsumed=${()=>C("")} + /> + `:null} + + ${u===je.TOGETHER?S` + <${Kg} + auth=${t} + user=${te} + currentPlays=${me} + onRate=${Ye} + onNavigate=${pi} + onOpenScenarios=${ms} + setStatus=${a} + graphDebugUi=${Me} + linkMutation=${j} + partnerRequestMutations=${_e} + createGroupMutation=${jt} + shareToggleMutation=${hs} + /> + `:null} + + ${u===je.SETTINGS?S` + <${Wg} + auth=${t} + user=${te} + status=${l} + selectedRoles=${dr} + roleSearchInput=${k} + onRoleSearch=${Du} + roleSearchItems=${Ao} + saveRoleMutation=${ci} + onPhotoPreference=${R=>pr.mutate(R)} + logout=${re} + onDeleted=${re} + setStatus=${a} + /> + `:null} + + +
+ `}function s0({step:e,setStep:t,loginForm:n,setLoginForm:r,createResult:i,status:s,loginMutation:o,createMutation:l,saveRoleMutation:a,roleSearchInput:u,onRoleSearch:h,roleSearchItems:f,selectedRoles:d,pendingPartnerId:m,onFinish:$}){return i?S` +
+

Set up your profile

+

Keep these credentials. You need them to reopen the profile later.

+
+
+
Profile ID
+ ${i.id} +
Private pass
+ ${i.private_token} +
+ + ${m?S`
Invite saved for ${m}. The connection request will go out after this setup.
`:null} + +
+ Pick your roles +
Optional. You can edit these later in Settings.
+
+ h(k.target.value)} /> + ${d.length?S` +
+ ${d.map(k=>S` + <${_o} + key=${k.id} + kink=${k} + selected=${!0} + onToggle=${(w,p)=>a.mutate({kinkId:w,selected:p})} + /> + `)} +
+ `:S`
No roles selected yet.
`} + ${f.length?S` +
+ ${f.map(k=>S` + <${_o} + key=${k.kink.id} + kink=${k.kink} + selected=${!1} + onToggle=${(w,p)=>a.mutate({kinkId:w,selected:p})} + /> + `)} +
+ `:null} + + +
+ <${zr} text=${s} /> +
+ `:e==="login"?S` +
+

Welcome back

+

Open your profile to continue discovery.

+
+ ${m?S`
You were invited by ${m}.
`:null} + r(w=>({...w,user_id:k.target.value}))} + /> + r(w=>({...w,private_token:k.target.value}))} + /> + + +
+ <${zr} text=${s} /> +
+ `:S` +
+

Play List

+

Discover what you like. See what you share. Keep private things private by default.

+
+ ${m?S`
You were invited by ${m}. Create or open a profile and we’ll stage the connection for you.
`:null} + + +
+ <${zr} text=${s} /> +
+ `}function o0(){return S`<${rd} client=${Jg}><${i0} />`}bp(document.getElementById("root")).render(S`<${o0} />`);Pg(); diff --git a/frontend/dist/assets/web-vitals.attribution-C_HyTrBb.js b/frontend/dist/assets/web-vitals.attribution-C_HyTrBb.js new file mode 100644 index 0000000000000000000000000000000000000000..1331be3546067aea65c80950ad861180d4bbaa9a --- /dev/null +++ b/frontend/dist/assets/web-vitals.attribution-C_HyTrBb.js @@ -0,0 +1 @@ +var D,z,k=function(){var e=self.performance&&performance.getEntriesByType&&performance.getEntriesByType("navigation")[0];if(e&&e.responseStart>0&&e.responseStart(n||100)-1)return i||t;if(i=i?t+">"+i:t,r.id)break;e=r.parentNode}}catch{}return i},X=-1,ue=function(){return X},L=function(e){addEventListener("pageshow",function(n){n.persisted&&(X=n.timeStamp,e(n))},!0)},O=function(){var e=k();return e&&e.activationStart||0},v=function(e,n){var i=k(),r="navigate";return ue()>=0?r="back-forward-cache":i&&(document.prerendering||O()>0?r="prerender":document.wasDiscarded?r="restore":i.type&&(r=i.type.replace(/_/g,"-"))),{name:e,value:n===void 0?-1:n,rating:"good",delta:0,entries:[],id:"v4-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12),navigationType:r}},S=function(e,n,i){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){var r=new PerformanceObserver(function(t){Promise.resolve().then(function(){n(t.getEntries())})});return r.observe(Object.assign({type:e,buffered:!0},i||{})),r}}catch{}},h=function(e,n,i,r){var t,u;return function(a){n.value>=0&&(a||r)&&((u=n.value-(t||0))||t===void 0)&&(t=n.value,n.delta=u,n.rating=function(o,s){return o>s[1]?"poor":o>s[0]?"needs-improvement":"good"}(n.value,i),e(n))}},B=function(e){requestAnimationFrame(function(){return requestAnimationFrame(function(){return e()})})},w=function(e){document.addEventListener("visibilitychange",function(){document.visibilityState==="hidden"&&e()})},j=function(e){var n=!1;return function(){n||(e(),n=!0)}},T=-1,U=function(){return document.visibilityState!=="hidden"||document.prerendering?1/0:0},P=function(e){document.visibilityState==="hidden"&&T>-1&&(T=e.type==="visibilitychange"?e.timeStamp:0,se())},V=function(){addEventListener("visibilitychange",P,!0),addEventListener("prerenderingchange",P,!0)},se=function(){removeEventListener("visibilitychange",P,!0),removeEventListener("prerenderingchange",P,!0)},Y=function(){return T<0&&(T=U(),V(),L(function(){setTimeout(function(){T=U(),V()},0)})),{get firstHiddenTime(){return T}}},q=function(e){document.prerendering?addEventListener("prerenderingchange",function(){return e()},!0):e()},_=[1800,3e3],ce=function(e,n){n=n||{},q(function(){var i,r=Y(),t=v("FCP"),u=S("paint",function(a){a.forEach(function(o){o.name==="first-contentful-paint"&&(u.disconnect(),o.startTimeu.value&&(u.value=a,u.entries=o,t())},c=S("layout-shift",s);c&&(t=h(i,u,G,r.reportAllChanges),w(function(){s(c.takeRecords()),t(!0)}),L(function(){a=0,u=v("CLS",0),t=h(i,u,G,r.reportAllChanges),B(function(){return t()})}),setTimeout(t,0))}))})(function(i){var r=function(t){var u,a={};if(t.entries.length){var o=t.entries.reduce(function(c,f){return c&&c.value>f.value?c:f});if(o&&o.sources&&o.sources.length){var s=(u=o.sources).find(function(c){return c.node&&c.node.nodeType===1})||u[0];s&&(a={largestShiftTarget:N(s.node),largestShiftTime:o.startTime,largestShiftValue:o.value,largestShiftSource:s,largestShiftEntry:o,loadState:Q(o.startTime)})}}return Object.assign(t,{attribution:a})}(i);e(r)},n)},Z=0,x=1/0,I=0,fe=function(e){e.forEach(function(n){n.interactionId&&(x=Math.min(x,n.interactionId),I=Math.max(I,n.interactionId),Z=I?(I-x)/7+1:0)})},$=function(){return D?Z:performance.interactionCount||0},de=function(){"interactionCount"in performance||D||(D=S("event",fe,{type:"event",buffered:!0,durationThreshold:0}))},l=[],C=new Map,ee=0,le=function(){var e=Math.min(l.length-1,Math.floor(($()-ee)/50));return l[e]},ne=[],me=function(e){if(ne.forEach(function(t){return t(e)}),e.interactionId||e.entryType==="first-input"){var n=l[l.length-1],i=C.get(e.interactionId);if(i||l.length<10||e.duration>n.latency){if(i)e.duration>i.latency?(i.entries=[e],i.latency=e.duration):e.duration===i.latency&&e.startTime===i.entries[0].startTime&&i.entries.push(e);else{var r={id:e.interactionId,latency:e.duration,entries:[e]};C.set(r.id,r),l.push(r)}l.sort(function(t,u){return u.latency-t.latency}),l.length>10&&l.splice(10).forEach(function(t){return C.delete(t.id)})}}},H=function(e){var n=self.requestIdleCallback||self.setTimeout,i=-1;return e=j(e),document.visibilityState==="hidden"?e():(i=n(e),w(e)),i},J=[200,500],pe=function(e,n){"PerformanceEventTiming"in self&&"interactionId"in PerformanceEventTiming.prototype&&(n=n||{},q(function(){var i;de();var r,t=v("INP"),u=function(o){H(function(){o.forEach(me);var s=le();s&&s.latency!==t.value&&(t.value=s.latency,t.entries=s.entries,r())})},a=S("event",u,{durationThreshold:(i=n.durationThreshold)!==null&&i!==void 0?i:40});r=h(e,t,J,n.reportAllChanges),a&&(a.observe({type:"first-input",buffered:!0}),w(function(){u(a.takeRecords()),r(!0)}),L(function(){ee=$(),l.length=0,C.clear(),t=v("INP"),r=h(e,t,J,n.reportAllChanges)}))}))},y=[],p=[],F=0,W=new WeakMap,E=new Map,R=-1,ve=function(e){y=y.concat(e),te()},te=function(){R<0&&(R=H(he))},he=function(){E.size>10&&E.forEach(function(a,o){C.has(o)||E.delete(o)});var e=l.map(function(a){return W.get(a.entries[0])}),n=p.length-50;p=p.filter(function(a,o){return o>=n||e.includes(a)});for(var i=new Set,r=0;rF&&o>u||i.has(a)}),R=-1};ne.push(function(e){e.interactionId&&e.target&&!E.has(e.interactionId)&&E.set(e.interactionId,e.target)},function(e){var n,i=e.startTime+e.duration;F=Math.max(F,e.processingEnd);for(var r=p.length-1;r>=0;r--){var t=p[r];if(Math.abs(i-t.renderTime)<=8){(n=t).startTime=Math.min(e.startTime,n.startTime),n.processingStart=Math.min(e.processingStart,n.processingStart),n.processingEnd=Math.max(e.processingEnd,n.processingEnd),n.entries.push(e);break}}n||(n={startTime:e.startTime,processingStart:e.processingStart,processingEnd:e.processingEnd,renderTime:i,entries:[e]},p.push(n)),(e.interactionId||e.entryType==="first-input")&&W.set(e,n),te()});var re=function(e,n){for(var i,r=[],t=0;i=y[t];t++)if(!(i.startTime+i.durationn)break;r.push(i)}return r},Te=function(e,n){z||(z=S("long-animation-frame",ve)),pe(function(i){var r=function(t){var u=t.entries[0],a=W.get(u),o=u.processingStart,s=a.processingEnd,c=a.entries.sort(function(g,ae){return g.processingStart-ae.processingStart}),f=re(u.startTime,s),d=t.entries.find(function(g){return g.target}),m=d&&d.target||E.get(u.interactionId),b=[u.startTime+u.duration,s].concat(f.map(function(g){return g.startTime+g.duration})),M=Math.max.apply(Math,b),ie={interactionTarget:N(m),interactionTargetElement:m,interactionType:u.name.startsWith("key")?"keyboard":"pointer",interactionTime:u.startTime,nextPaintTime:M,processedEventEntries:c,longAnimationFrameEntries:f,inputDelay:o-u.startTime,processingDuration:s-o,presentationDelay:Math.max(M-s,0),loadState:Q(u.startTime)};return Object.assign(t,{attribution:ie})}(i);e(r)},n)},K=[2500,4e3],A={},ye=function(e,n){(function(i,r){r=r||{},q(function(){var t,u=Y(),a=v("LCP"),o=function(f){r.reportAllChanges||(f=f.slice(-1)),f.forEach(function(d){d.startTime + + + + + Play List + + + + + +
+ + diff --git a/frontend/index.html b/frontend/index.html index b482a9b8963c7e14070005a508a7410dce90b4df..8efe6e830a5f84404208f377af5e6604f40122d6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,12 +5,10 @@ Play List - - - +
- + diff --git a/frontend/mutations.js b/frontend/mutations.js index e9b624cbeb1e6f3a47734215266a171b3acb0870..e56b0f34bff21b6ba9996e878d93c23f4b3d6ef1 100644 --- a/frontend/mutations.js +++ b/frontend/mutations.js @@ -1,6 +1,6 @@ -import { useMutation, useQueryClient } from "https://esm.sh/@tanstack/react-query@5.59.20?deps=react@18.3.1"; -import { api, authHeaders } from "./api.js?v=33"; -import { optimisticBoard, removeKinkFromItemsPayload } from "./board-utils.js?v=33"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { api, authHeaders } from "./api.js"; +import { optimisticBoard, removeKinkFromItemsPayload } from "./board-utils.js"; const EMPTY_BOARD = { to_me: [], by_me: [], together: [], hidden: [], no_go: [] }; diff --git a/frontend/my-plays.js b/frontend/my-plays.js index 5251f87f0d69b2c8f77362de39f51ee41be2dfb0..75ad9ca93e7e8e1dc68baab348b1ac0cb3388038 100644 --- a/frontend/my-plays.js +++ b/frontend/my-plays.js @@ -1,15 +1,15 @@ -import React, { useState, useMemo, useCallback, useEffect, useRef } from "https://esm.sh/react@18.3.1"; -import { html, UI_REACTIONS, DIRECTION_PILLS } from "./constants.js?v=33"; -import { PlayChip, ReactionBar, DirectionPills, SearchOverlay } from "./components.js?v=41"; -import { useBoardQuery, useSearchQuery, useDetailQuery, useSimilarQuery } from "./queries.js?v=41"; -import { uiReactionFor } from "./api.js?v=33"; -import { planSimilarTentativeAdd } from "./my-plays-similar.js?v=33"; +import React, { useState, useMemo, useCallback, useEffect, useRef } from "react"; +import { html, UI_REACTIONS, DIRECTION_PILLS } from "./constants.js"; +import { PlayChip, ReactionBar, DirectionPills, SearchOverlay } from "./components.js"; +import { useBoardQuery, useSearchQuery, useDetailQuery, useSimilarQuery } from "./queries.js"; +import { uiReactionFor } from "./api.js"; +import { planSimilarTentativeAdd } from "./my-plays-similar.js"; import { DIRECTION_COLUMN_ORDER, directionColumnsForPlay, directionsAfterColumnDrop, -} from "./direction-logic.js?v=32"; -import { nearDuplicateHintsById } from "./name-fingerprint.js?v=32"; +} from "./direction-logic.js"; +import { nearDuplicateHintsById } from "./name-fingerprint.js"; const DIR_ORDER = [...DIRECTION_COLUMN_ORDER]; diff --git a/frontend/partner-groups.test.mjs b/frontend/partner-groups.test.mjs index 97afe0d3c438282f7295551ff453ca277706b2b0..350d565fb09f5cc7b2dc9cc10db0d0ae5be068af 100644 --- a/frontend/partner-groups.test.mjs +++ b/frontend/partner-groups.test.mjs @@ -34,3 +34,18 @@ test("resolvedPartnerGroups uses groups GET when user list not loaded yet", () = const groupsQuery = { items: [{ id: "g2" }] }; assert.deepEqual(resolvedPartnerGroups(null, groupsQuery), [{ id: "g2" }]); }); + +test("resolvedPartnerGroups returns an empty array for empty or malformed sources", () => { + assert.deepEqual(resolvedPartnerGroups({ partner_groups: [] }, { items: [] }), []); + assert.deepEqual(resolvedPartnerGroups({ partner_groups: "bad" }, { items: "bad" }), []); +}); + +test("preferredPartnerGroupId ignores stale selections and empty member placeholders", () => { + const groups = [ + { id: "solo", member_ids: [] }, + { id: "empty-member", member_ids: [""] }, + { id: "pair", member_ids: ["u2"] }, + ]; + + assert.equal(preferredPartnerGroupId(groups, "missing"), "pair"); +}); diff --git a/frontend/queries.js b/frontend/queries.js index 8afc3ed602be1bb1d5672e6481971a3ec870a4b6..b0b4c1a424827d5e302075db51f7b3d643b97f1d 100644 --- a/frontend/queries.js +++ b/frontend/queries.js @@ -1,5 +1,5 @@ -import { useQuery } from "https://esm.sh/@tanstack/react-query@5.59.20?deps=react@18.3.1"; -import { api, authHeaders } from "./api.js?v=33"; +import { useQuery } from "@tanstack/react-query"; +import { api, authHeaders } from "./api.js"; const protectedQueryOptions = { retry: false, refetchOnWindowFocus: false }; diff --git a/frontend/scenarios.js b/frontend/scenarios.js index 9d7e0fdd108d2b2ba6ffb821f5aa71d5bb304951..81ef4eed36325c250aa6e5f8aad430e5b5711378 100644 --- a/frontend/scenarios.js +++ b/frontend/scenarios.js @@ -1,9 +1,9 @@ -import React, { useEffect, useMemo, useState } from "https://esm.sh/react@18.3.1"; -import { html } from "./constants.js?v=33"; -import { uiReactionFor } from "./api.js?v=33"; -import { useScenarioParentsQuery, useScenariosQuery } from "./queries.js?v=41"; -import { ReactionBar } from "./components.js?v=41"; -import { sortScenarioItems } from "./scenarios-state.js?v=1"; +import React, { useEffect, useMemo, useState } from "react"; +import { html } from "./constants.js"; +import { uiReactionFor } from "./api.js"; +import { useScenarioParentsQuery, useScenariosQuery } from "./queries.js"; +import { ReactionBar } from "./components.js"; +import { sortScenarioItems } from "./scenarios-state.js"; export function ScenariosView({ auth, diff --git a/frontend/scenarios.test.mjs b/frontend/scenarios.test.mjs index 47151e9204116bdb1332d2667e5446717fa63fbd..153a58fe37340e04451b9b4d6709c840fb8b04dd 100644 --- a/frontend/scenarios.test.mjs +++ b/frontend/scenarios.test.mjs @@ -19,3 +19,55 @@ test("sortScenarioItems pins saved positives first for the active parent", () => assert.deepEqual(ordered.map((item) => item.scenario.id), ["a", "b", "c"]); }); + +test("sortScenarioItems ranks all local states and falls back to names", () => { + const items = [ + { scenario: { id: "hard", name: "Zulu" } }, + { kink: { id: "love", name: "Bravo" } }, + { id: "none", name: "Alpha" }, + { scenario: { id: "like", name: "Charlie" } }, + { scenario: { id: "other_parent", name: "Able" } }, + ]; + const prefs = { + love: { parent_kink_id: "anal", interest_state: "love" }, + like: { parent_kink_id: "anal", interest_state: "like" }, + hard: { parent_kink_id: "anal", interest_state: "hard_no" }, + other_parent: { parent_kink_id: "touching", interest_state: "love" }, + }; + + const ordered = sortScenarioItems(items, prefs, "anal"); + + assert.deepEqual(ordered.map((item) => (item.scenario || item.kink || item).id), [ + "love", + "like", + "other_parent", + "none", + "hard", + ]); +}); + +test("sortScenarioItems does not mutate input and ignores preferences from other parents", () => { + const items = [ + { scenario: { id: "z", name: "Zulu" } }, + { kink: { id: "a", name: "Alpha" } }, + ]; + const prefs = { + z: { parent_kink_id: "other", interest_state: "love" }, + a: { parent_kink_id: "anal", interest_state: "curious" }, + }; + + const ordered = sortScenarioItems(items, prefs, "anal"); + + assert.deepEqual(items.map((item) => (item.scenario || item.kink).id), ["z", "a"]); + assert.deepEqual(ordered.map((item) => (item.scenario || item.kink).id), ["a", "z"]); +}); + +test("sortScenarioItems orders raw, kink, and scenario shapes by normalized names", () => { + const ordered = sortScenarioItems([ + { id: "raw", name: "Charlie" }, + { kink: { id: "kink", name: "Bravo" } }, + { scenario: { id: "scenario", name: "Alpha" } }, + ]); + + assert.deepEqual(ordered.map((item) => (item.scenario || item.kink || item).id), ["scenario", "kink", "raw"]); +}); diff --git a/frontend/settings.js b/frontend/settings.js index 4340a84930e4db57253798f3fbedc3b727720ceb..2a5534db8841efdd37d2e2731fcaaa9b752de1e5 100644 --- a/frontend/settings.js +++ b/frontend/settings.js @@ -1,7 +1,7 @@ -import React, { useMemo, useState } from "https://esm.sh/react@18.3.1"; -import { html, partnerGroupChipLabel } from "./constants.js?v=32"; -import { api, authHeaders } from "./api.js?v=33"; -import { RoleChip, StatusText } from "./components.js?v=32"; +import React, { useMemo, useState } from "react"; +import { html, partnerGroupChipLabel } from "./constants.js"; +import { api, authHeaders } from "./api.js"; +import { RoleChip, StatusText } from "./components.js"; export function SettingsView({ auth, diff --git a/frontend/starter-flow.test.mjs b/frontend/starter-flow.test.mjs index ae5d1a405d737bdfd1ce8a7df5b6164830cf6561..2192075b8fef394b2586e15a7e0513c5d7533876 100644 --- a/frontend/starter-flow.test.mjs +++ b/frontend/starter-flow.test.mjs @@ -18,6 +18,72 @@ test("starterViewState keeps the starter gate up while target-crossing saves are ); }); +test("starterViewState is inactive until an authenticated loaded user exists", () => { + assert.equal( + starterViewState({ + hasAuth: false, + hasCreateResult: false, + userLoaded: true, + savedCount: 0, + starterTarget: 12, + pendingSaveCount: 0, + starterCompleted: false, + }), + "inactive", + ); + assert.equal( + starterViewState({ + hasAuth: true, + hasCreateResult: true, + userLoaded: true, + savedCount: 0, + starterTarget: 12, + pendingSaveCount: 0, + starterCompleted: false, + }), + "inactive", + ); + assert.equal( + starterViewState({ + hasAuth: true, + hasCreateResult: false, + userLoaded: false, + savedCount: 0, + starterTarget: 12, + pendingSaveCount: 0, + starterCompleted: false, + }), + "inactive", + ); +}); + +test("starterViewState returns starter below target and main once settled", () => { + assert.equal( + starterViewState({ + hasAuth: true, + hasCreateResult: false, + userLoaded: true, + savedCount: 11, + starterTarget: 12, + pendingSaveCount: 0, + starterCompleted: false, + }), + "starter", + ); + assert.equal( + starterViewState({ + hasAuth: true, + hasCreateResult: false, + userLoaded: true, + savedCount: 12, + starterTarget: 12, + pendingSaveCount: 0, + starterCompleted: false, + }), + "main", + ); +}); + test("starterViewState does not reactivate the starter gate after completion", () => { assert.equal( starterViewState({ diff --git a/frontend/stryker.conf.mjs b/frontend/stryker.conf.mjs new file mode 100644 index 0000000000000000000000000000000000000000..7be6e960c0db667413f697f356989626489a7b57 --- /dev/null +++ b/frontend/stryker.conf.mjs @@ -0,0 +1,28 @@ +export default { + packageManager: "npm", + reporters: ["clear-text", "progress"], + testRunner: "command", + commandRunner: { + command: "node --test *.test.mjs", + }, + coverageAnalysis: "off", + disableTypeChecks: false, + concurrency: 4, + cleanTempDir: "always", + thresholds: { + high: 85, + low: 80, + break: 80, + }, + mutate: [ + "board-utils.js", + "direction-logic.js", + "discover-flow.js", + "discover-scenarios.js", + "discover-state.js", + "my-plays-similar.js", + "partner-groups.js", + "scenarios-state.js", + "starter-flow.js", + ], +}; diff --git a/frontend/telemetry.js b/frontend/telemetry.js index d97624a06bd49f563ba45638af81d16c1d898d42..089d9d0f69c4d75ff81ff2b735f9ac4d0ad45080 100644 --- a/frontend/telemetry.js +++ b/frontend/telemetry.js @@ -12,7 +12,7 @@ export function sendTelemetry(event) { } export function initTelemetry() { - import("https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.js?module").then(({ onCLS, onLCP, onINP }) => { + import("web-vitals/attribution").then(({ onCLS, onLCP, onINP }) => { onCLS((metric) => { if (metric.value > 0.05) { send({ diff --git a/frontend/together.js b/frontend/together.js index dbbc929a61dac80864bd8db23be086e70bd7eaa9..88fec7123c85112aa0c3a01fe3f25e85e854dcc0 100644 --- a/frontend/together.js +++ b/frontend/together.js @@ -1,9 +1,9 @@ -import React, { useState, useEffect, useMemo } from "https://esm.sh/react@18.3.1"; -import { html, UI_REACTIONS, VIEWS, partnerGroupChipLabel, partnerGroupPartnerIdsTitle } from "./constants.js?v=33"; -import { ReactionBar } from "./components.js?v=32"; -import { useGroupsQuery, useOverlapQuery, usePartnerBoardQuery } from "./queries.js?v=41"; -import { uiReactionFor } from "./api.js?v=33"; -import { preferredPartnerGroupId, resolvedPartnerGroups } from "./partner-groups.js?v=33"; +import React, { useState, useEffect, useMemo } from "react"; +import { html, UI_REACTIONS, VIEWS, partnerGroupChipLabel, partnerGroupPartnerIdsTitle } from "./constants.js"; +import { ReactionBar } from "./components.js"; +import { useGroupsQuery, useOverlapQuery, usePartnerBoardQuery } from "./queries.js"; +import { uiReactionFor } from "./api.js"; +import { preferredPartnerGroupId, resolvedPartnerGroups } from "./partner-groups.js"; export function TogetherView({ auth, @@ -264,6 +264,7 @@ export function TogetherView({ currentPlays=${currentPlays} auth=${auth} onRate=${onRate} + onOpenScenarios=${onOpenScenarios} graphDebugUi=${graphDebugUi} />` : null} @@ -408,12 +409,14 @@ function WorthExploringTab({ currentPlays, auth, onRate, + onOpenScenarios, graphDebugUi = false, }) { const related = overlap?.related_matches || []; + const scenarioRelated = overlap?.scenario_related_matches || []; const similar = overlap?.similar_matches || []; - if (!related.length && !similar.length) { + if (!related.length && !scenarioRelated.length && !similar.length) { return html`
Add more overlap to unlock nearby ideas worth exploring together.
`; } @@ -457,6 +460,60 @@ function WorthExploringTab({ ` : null} + ${scenarioRelated.length + ? html` +
+ Connected scenarios +
+ ${scenarioRelated.map((item) => { + const parent = item.parent_kink; + const myScenario = item.left_user_id === auth.userId ? item.left_scenario : item.right_scenario; + const theirScenario = item.left_user_id === auth.userId ? item.right_scenario : item.left_scenario; + const theirUserId = item.left_user_id === auth.userId ? item.right_user_id : item.left_user_id; + const theirReaction = item.partner_reactions?.[theirUserId]?.interest_state || ""; + const reason = item.reasons?.[0] || `Both are scenarios under ${parent?.name || "the same base kink"}`; + const openScenario = () => { + if (parent?.id && theirScenario?.id) onOpenScenarios?.(parent.id, theirScenario.id); + }; + return html` +
{ + if (e.key !== "Enter" && e.key !== " ") return; + e.preventDefault(); + openScenario(); + }} + > +
+
Base kink
+
${parent?.name || "Shared base kink"}
+
You
+
${myScenario?.name || "Scenario"}
+
Them
+
${theirScenario?.name || "Scenario"}
+
+
+ + + ${theirUserId.slice(0, 8)}… + ${UI_REACTIONS.find((reaction) => reaction.api === theirReaction)?.icon || ""} + + + ${graphDebugUi + ? html`
${reason}
` + : html`
Open their scenario under the same base kink.
`} +
+
+ `; + })} + ` + : null} + ${similar.length ? html`
Suggested next
diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..bdcf9e0b783497d8bd31e38a1adf44e6bd5f85a1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3364 @@ +{ + "name": "kink-cli-frontend-quality", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kink-cli-frontend-quality", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "^5.90.10", + "htm": "^3.1.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "web-vitals": "^4.2.4" + }, + "devDependencies": { + "@stryker-mutator/core": "^9.6.1", + "vite": "^5.4.21" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", + "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-decorators": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", + "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.4.tgz", + "integrity": "sha512-w6KF8ZYRvqHhROkOTHXYC3qIV/KYEu5o12oLqQySvch61vrYtRxNSHTONSdJqWiFJPlCUQAHT5OgOIyuTr+MHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.9", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", + "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.9.tgz", + "integrity": "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.1.tgz", + "integrity": "sha512-6y11LgmNpmn5D2aB5FgnCfBUBK8ZstwLCalyJmORcJZ/WrhOjm16mu6eSqIx8DnErxDqSLr+Jkp+GP8/Nwd5tA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/external-editor": "^3.0.0", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.13.tgz", + "integrity": "sha512-dF2zvrFo9LshkcB23/O1il13kBkBltWIXzut1evfbuBLXMiGIuC45c+ZQ0uukjCDsvI8OWqun4FRYMnzFCQa3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.0.tgz", + "integrity": "sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", + "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.12.tgz", + "integrity": "sha512-uiMFBl4LqFzJClh80Q3f9hbOFJ6kgkDWI4LjAeBuyO6EanVVMF69AgOvpi1qdqjDSjDN6578B6nky9ceEpI+1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.12.tgz", + "integrity": "sha512-/vrwhEf7Xsuh+YlHF4IjSy3g1cyrQuPaSiHIxCEbLu8qnfvrcvJyCkoktOOF+xV9gSb77/G0n3h04RbMDW2sIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.12.tgz", + "integrity": "sha512-CBh7YHju623lxJRcAOo498ZUwIuMy63bqW/vVq0tQAZVv+lkWlHkP9ealYE1utWSisEShY5VMdzIXRmyEODzcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.2.tgz", + "integrity": "sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.1.4", + "@inquirer/confirm": "^6.0.12", + "@inquirer/editor": "^5.1.1", + "@inquirer/expand": "^5.0.13", + "@inquirer/input": "^5.0.12", + "@inquirer/number": "^4.0.12", + "@inquirer/password": "^5.0.12", + "@inquirer/rawlist": "^5.2.8", + "@inquirer/search": "^4.1.8", + "@inquirer/select": "^5.1.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.8.tgz", + "integrity": "sha512-Su7FQvp5buZmCymN3PPoYv31ZQQX4ve2j02k7piGgKAWgE+AQRB5YoYVveGXcl3TZ9ldgRMSxj56YfDFmmaqLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.8.tgz", + "integrity": "sha512-fGiHKGD6DyPIYUWxoXnQTeXeyYqSOUrasDMABBmMHUalH/LxkuzY0xVRtimXAt1sUeeyYkVuKQx1bebMuN11Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.4.tgz", + "integrity": "sha512-2kWcGKPMLAXAWRp1AH1SLsQmX+j0QjeljyXMUji9WMZC8nRDO0b7qquIGr6143E7KMLt3VAIGNXzwa/6PXQs4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.9", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", + "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/api": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-9.6.1.tgz", + "integrity": "sha512-g8VNoFWQWbx0pdal3Vt8jVCZW+v3sc3gi94iI0GVtVgUGTqphAjJF6EAruPTx0lqvtonsaAxn5TD36hcG1d6Wg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-metrics": "3.7.3", + "mutation-testing-report-schema": "3.7.3", + "tslib": "~2.8.0", + "typed-inject": "~5.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/core": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-9.6.1.tgz", + "integrity": "sha512-WMgnvf+Wyh/yiruhNZwc8w8DlzmmjXhPjSn5MR8RhAXzlnWji8TQrUYgBUkHk9bEgSaIlB3KZHm37iiU5Q2cLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inquirer/prompts": "^8.0.0", + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/instrumenter": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "ajv": "~8.18.0", + "chalk": "~5.6.0", + "commander": "~14.0.0", + "diff-match-patch": "1.0.5", + "emoji-regex": "~10.6.0", + "execa": "~9.6.0", + "json-rpc-2.0": "^1.7.0", + "lodash.groupby": "~4.6.0", + "minimatch": "~10.2.4", + "mutation-server-protocol": "~0.4.0", + "mutation-testing-elements": "3.7.3", + "mutation-testing-metrics": "3.7.3", + "mutation-testing-report-schema": "3.7.3", + "npm-run-path": "~6.0.0", + "progress": "~2.0.3", + "rxjs": "~7.8.1", + "semver": "^7.6.3", + "source-map": "~0.7.4", + "tree-kill": "~1.2.2", + "tslib": "2.8.1", + "typed-inject": "~5.0.0", + "typed-rest-client": "~2.3.0" + }, + "bin": { + "stryker": "bin/stryker.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/instrumenter": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-9.6.1.tgz", + "integrity": "sha512-5K8wH4Pthly25c2uKKik4Dfcoeou7sbJdFS6u3QIYHlulgFVDJwtEMWTZGkZfs7IiUEXIDNa0keRACq5jn5AvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "~7.29.0", + "@babel/generator": "~7.29.0", + "@babel/parser": "~7.29.0", + "@babel/plugin-proposal-decorators": "~7.29.0", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/preset-typescript": "~7.28.0", + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "angular-html-parser": "~10.4.0", + "semver": "~7.7.0", + "tslib": "2.8.1", + "weapon-regex": "~1.3.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/util": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-9.6.1.tgz", + "integrity": "sha512-Lk/ALVctJjFv1vvwR+CFoKzDCWvsBlq7flDUnmnpuwTrGbm156EdZD1Jjq4o8KdOap0ezUZqQNE9OAI1m2+pUQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.5.tgz", + "integrity": "sha512-t20KrhKkf0HXzqQkPbJ5erhFesup68BAbwFgYmTrS7bxMF7O5MdmL8jUkik4thsG7Hg00fblz30h6yF1d5TxGg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.5.tgz", + "integrity": "sha512-aNwj1mi2v2bQ9IxkyR1grLOUkv3BYWoykHy9KDyLNbjC3tsahbOHJibK+Wjtr1wRhG59/AvJhiJG5OlthaCgJA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/angular-html-parser": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-10.4.0.tgz", + "integrity": "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", + "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 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==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "license": "Apache-2.0" + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-rpc-2.0": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.7.1.tgz", + "integrity": "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mutation-server-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.4.1.tgz", + "integrity": "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.1.12" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mutation-testing-elements": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.7.3.tgz", + "integrity": "sha512-SMeIPxngJpfjfNYctFpYQQtlBlZaVO0aoB3FKdwrI8Ee/2bkyUuCZzAOCLv1U9fnmfA37dPFq0Owduoxs2XgGQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mutation-testing-metrics": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-3.7.3.tgz", + "integrity": "sha512-B8QrP0ZomErzTPNlhrzKWPNBln+3afwBZPHv0Q7N8wZZTYxMptzb/Gdm3ExXVmioVYrtZAtsDs7W/T/b2AixOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-report-schema": "3.7.3" + } + }, + "node_modules/mutation-testing-report-schema": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-3.7.3.tgz", + "integrity": "sha512-BHm3MYq+ckO+t5CtlG8zpqxc75rdJCkxVlE+fGuGJM3F7tNCQ/OW2N+TQVHN3BHsYa84+BFc6g3AwDYkUsw2MA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "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" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typed-inject": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-5.0.0.tgz", + "integrity": "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/typed-rest-client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.3.1.tgz", + "integrity": "sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "6.15.1", + "tunnel": "0.0.6", + "underscore": "^1.13.8" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/weapon-regex": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-1.3.6.tgz", + "integrity": "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..2a63f97fa9b5640f205336578ce1eacb249179dd --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "private": true, + "name": "kink-cli-frontend-quality", + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "vite build --config vite.config.mjs", + "test:frontend:node": "node --test frontend/*.test.mjs", + "test:frontend:mutation": "cd frontend && ../node_modules/.bin/stryker run stryker.conf.mjs" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.10", + "htm": "^3.1.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "web-vitals": "^4.2.4" + }, + "devDependencies": { + "@stryker-mutator/core": "^9.6.1", + "vite": "^5.4.21" + } +} diff --git a/pyproject.toml b/pyproject.toml index 39af20ec60980d353fe510371caf315c83270851..66ef79dde6b5815aa55628a451cbe5df0c961fcd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ include = ["backend*"] [dependency-groups] dev = [ "dead>=2.1.0", + "mutmut>=3.5.0", "ruff>=0.15.11", "semgrep>=1.161.0", ] @@ -43,6 +44,34 @@ dev = [ [tool.setuptools] py-modules = ["api", "models"] +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] + +[tool.mutmut] +paths_to_mutate = [ + "backend/partners.py", + "backend/scenarios.py", + "backend/shared_play.py", + "backend/starter_policy.py", +] +also_copy = [ + "api.py", + "models.py", + "backend/", + "scripts/", + "tests/fixtures/", +] +pytest_add_cli_args_test_selection = [ + "tests/test_adversarial_api.py", + "tests/test_partner_groups.py", + "tests/test_partner_related_matches.py", + "tests/test_scenarios.py", + "tests/test_starter_policy.py", + "tests/test_two_user_flow_tree.py", +] +runner = "python -m pytest -q" + [project.optional-dependencies] test = [ "blockbuster>=1.5", diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b079e0e8a9d54ab11a7f5b4d5e0ac44547c73b6c --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Importable maintenance and audit scripts.""" diff --git a/scripts/audit_data_quality.py b/scripts/audit_data_quality.py index 3b153d78d104f6e0e7e2ef3d6d4bc53e1b0fc83c..16aef5a2b8a7acb614a3d05191a13bb2477d0dac 100644 --- a/scripts/audit_data_quality.py +++ b/scripts/audit_data_quality.py @@ -1,11 +1,12 @@ from __future__ import annotations import json +import os import sqlite3 from pathlib import Path ROOT = Path(__file__).resolve().parents[1] -DB_PATH = ROOT / "data" / "store.db" +DB_PATH = Path(os.environ["KINK_STORE_PATH"]) if os.environ.get("KINK_STORE_PATH") else ROOT / "data" / "store.db" def scalar(conn: sqlite3.Connection, query: str, params: tuple = ()): diff --git a/scripts/audit_runner.py b/scripts/audit_runner.py index 2b8ab66e1293b8ac3b0c827a18378d873ceaa597..680ffda54e2fa8b0daa378d3fcc42eae65db50f2 100644 --- a/scripts/audit_runner.py +++ b/scripts/audit_runner.py @@ -5,6 +5,7 @@ import json import os import shutil import subprocess +import sys import time from pathlib import Path @@ -64,7 +65,7 @@ def header_probe() -> dict: def build_steps(tier: str, artifact_dir: Path) -> list[dict]: steps: list[dict] = [] env = {"KINK_AUDIT_ARTIFACT_DIR": str(artifact_dir), "KINK_AUDIT_BASE_URL": BASE_URL} - python = str(ROOT / ".venv" / "bin" / "python") + python = sys.executable core = [ ("diagnose_ui_flow", [python, "scripts/diagnose_ui_flow.py"], False), @@ -89,33 +90,69 @@ def build_steps(tier: str, artifact_dir: Path) -> list[dict]: if which("npm"): for name, cmd, optional in [ - ("axe_core", ["npm", "--prefix", str(AUDIT_DIR), "run", "axe"], True), - ("lhci", ["npm", "--prefix", str(AUDIT_DIR), "run", "lhci"], True), + ("axe_core", ["npm", "--prefix", str(AUDIT_DIR), "run", "axe"], False), + ("lhci", ["npm", "--prefix", str(AUDIT_DIR), "run", "lhci"], False), ]: steps.append(run_step(name, cmd, artifact_dir, env=env, optional=optional)) if which("bandit"): - steps.append(run_step("bandit", ["bandit", "-c", str(AUDIT_DIR / "config" / "bandit.yaml"), "-r", str(ROOT)], artifact_dir, optional=True)) + steps.append( + run_step( + "bandit", + [ + "bandit", + "-c", + str(AUDIT_DIR / "config" / "bandit.yaml"), + "-r", + str(ROOT / "api.py"), + str(ROOT / "backend"), + ], + artifact_dir, + optional=False, + ) + ) if which("pip-audit"): - steps.append(run_step("pip_audit", ["pip-audit"], artifact_dir, optional=True)) + steps.append(run_step("pip_audit", ["pip-audit"], artifact_dir, optional=False)) if which("semgrep"): - steps.append(run_step("semgrep", ["semgrep", "--config", str(AUDIT_DIR / "config" / "semgrep.yml"), str(ROOT)], artifact_dir, optional=True)) + steps.append( + run_step( + "semgrep", + [ + "semgrep", + "--config", + str(AUDIT_DIR / "config" / "semgrep.yml"), + "--exclude", + "node_modules", + "--exclude", + ".venv", + "--exclude", + ".cursor", + "--exclude", + "data", + "--exclude", + "frontend/dist", + str(ROOT), + ], + artifact_dir, + optional=False, + ) + ) if which("schemathesis"): steps.append(run_step( "schemathesis", [ "schemathesis", "run", f"{BASE_URL}/openapi.json", - "--config", str(AUDIT_DIR / "config" / "schemathesis.toml"), "--phases", "coverage", "--checks", "not_a_server_error,status_code_conformance,content_type_conformance,response_headers_conformance,response_schema_conformance,negative_data_rejection,unsupported_method", "--include-path-regex", r"^/$|^/health$|^/stats$|^/users$", "--max-failures=10", "--request-timeout", "5", "--max-examples", "3", + "--generation-allow-x00=false", "--workers", "1", ], artifact_dir, - optional=True, + optional=False, )) return steps diff --git a/scripts/build_hf_demo_db.py b/scripts/build_hf_demo_db.py index a964a10cdb316129dacebdb05cf5d1cedeea1ef2..da6a8ee8187aaf23d746de63ce7df9bd6aed84a5 100644 --- a/scripts/build_hf_demo_db.py +++ b/scripts/build_hf_demo_db.py @@ -54,13 +54,13 @@ def main() -> int: ("demo_kink_3", "Kissing", "Kissing and making out."), ("demo_kink_4", "Spanking", "Impact on the butt."), ("demo_kink_5", "Cuddles", "Non-sexual closeness."), - ("demo_kink_6", "Anal sex", "Starter demo row."), - ("demo_kink_7", "Blindfolds", "Starter demo row."), - ("demo_kink_8", "Breast nipple play", "Starter demo row."), - ("demo_kink_9", "Hair pulling", "Starter demo row."), - ("demo_kink_10", "Handcuffs", "Starter demo row."), - ("demo_kink_11", "Mutual masturbation", "Starter demo row."), - ("demo_kink_12", "Oral sex", "Starter demo row."), + ("demo_kink_6", "Anal sex", ""), + ("demo_kink_7", "Blindfolds", ""), + ("demo_kink_8", "Breast nipple play", ""), + ("demo_kink_9", "Hair pulling", ""), + ("demo_kink_10", "Handcuffs", ""), + ("demo_kink_11", "Mutual masturbation", ""), + ("demo_kink_12", "Oral sex", ""), ] for kid, name, short in kinks: b.ensure_kink(kid, name, "fetlife_fetish", short, "low", False, "") diff --git a/scripts/check_mutmut_stats.py b/scripts/check_mutmut_stats.py new file mode 100644 index 0000000000000000000000000000000000000000..a3f1edf4f2c51f987fb371b0ab99353e3a9bba15 --- /dev/null +++ b/scripts/check_mutmut_stats.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("stats_path", type=Path) + parser.add_argument("--min-score", type=float, default=100.0) + parser.add_argument("--max-survived", type=int, default=0) + parser.add_argument("--max-timeout", type=int, default=0) + parser.add_argument("--max-no-tests", type=int, default=0) + args = parser.parse_args() + + stats = json.loads(args.stats_path.read_text()) + killed = int(stats["killed"]) + total = int(stats["total"]) + survived = int(stats["survived"]) + timeout = int(stats["timeout"]) + no_tests = int(stats["no_tests"]) + score = killed / total * 100 + + print( + "mutmut score: " + f"{score:.2f}% ({killed}/{total} killed, " + f"{survived} survived, {timeout} timeout, {no_tests} no tests)" + ) + + failures = [] + if score < args.min_score: + failures.append(f"score {score:.2f}% < {args.min_score:.2f}%") + if survived > args.max_survived: + failures.append(f"survived {survived} > {args.max_survived}") + if timeout > args.max_timeout: + failures.append(f"timeout {timeout} > {args.max_timeout}") + if no_tests > args.max_no_tests: + failures.append(f"no_tests {no_tests} > {args.max_no_tests}") + + if failures: + print("mutmut gate failed: " + "; ".join(failures), file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/dev_reset_local.py b/scripts/dev_reset_local.py index 74d253e8755954d0361aa77a0364bb8b52bffee5..5796c9c3e79fa2f9598bdf328a7c1d9a81140033 100644 --- a/scripts/dev_reset_local.py +++ b/scripts/dev_reset_local.py @@ -33,13 +33,11 @@ def _kill_ports(ports: list[int], *, dry_run: bool) -> None: if r.returncode == 0: print(f"freed port {port} (fuser)") continue - r2 = subprocess.run( - f"lsof -ti:{port} | xargs -r kill -9", - shell=True, - capture_output=True, - text=True, - ) - if r2.returncode == 0: + r2 = subprocess.run(["lsof", f"-ti:{port}"], capture_output=True, text=True) + pids = [int(pid) for pid in r2.stdout.split() if pid.isdigit()] + for pid in pids: + os.kill(pid, 9) + if pids: print(f"freed port {port} (lsof)") else: print(f"note: no listener freed on port {port} (fuser/lsof)", file=sys.stderr) diff --git a/scripts/diagnose_ui_flow.py b/scripts/diagnose_ui_flow.py index 6c4c00dd36dbc1a3e3ade4a76d3237eb48be8740..241222daea5f7cce331ed8642bc95fb019fcb5fe 100644 --- a/scripts/diagnose_ui_flow.py +++ b/scripts/diagnose_ui_flow.py @@ -2,319 +2,200 @@ from __future__ import annotations import json import os -import re import time +import urllib.parse import urllib.request from dataclasses import dataclass from pathlib import Path +from typing import Callable from playwright.sync_api import Page, expect, sync_playwright -BASE_URL = os.environ.get("KINK_AUDIT_BASE_URL", "http://127.0.0.1:8011") -ARTIFACT_DIR = Path("/tmp/kink_diagnostics") +BASE_URL = os.environ.get("KINK_AUDIT_BASE_URL", "http://127.0.0.1:8011").rstrip("/") +ARTIFACT_DIR = Path(os.environ.get("KINK_AUDIT_ARTIFACT_DIR", "/tmp/kink_diagnostics")) ARTIFACT_DIR.mkdir(parents=True, exist_ok=True) +STARTER_TARGET = 12 -@dataclass +@dataclass(frozen=True) class Credentials: user_id: str private_token: str -def post_json(path: str, payload: dict, headers: dict[str, str] | None = None, timeout: int = 20) -> dict: - request_headers = {"Content-Type": "application/json"} - if headers: - request_headers.update(headers) - request = urllib.request.Request( - f"{BASE_URL}{path}", - data=json.dumps(payload).encode("utf-8"), - headers=request_headers, - method="POST", - ) +def auth_headers(creds: Credentials) -> dict[str, str]: + return {"x-private-token": creds.private_token} + + +def json_request( + path: str, + *, + method: str = "GET", + payload: dict | None = None, + headers: dict[str, str] | None = None, + timeout: int = 30, +) -> dict: + request_headers = dict(headers or {}) + data = None + if payload is not None: + request_headers["Content-Type"] = "application/json" + data = json.dumps(payload).encode("utf-8") + request = urllib.request.Request(f"{BASE_URL}{path}", data=data, headers=request_headers, method=method) with urllib.request.urlopen(request, timeout=timeout) as response: - return json.loads(response.read().decode("utf-8")) + body = response.read().decode("utf-8") + return json.loads(body) if body else {} def create_user() -> Credentials: - payload = post_json("/users", {}) + payload = json_request("/users", method="POST", payload={}) return Credentials(user_id=payload["id"], private_token=payload["private_token"]) +def save_play(creds: Credentials, kink_id: str, interest_state: str = "like") -> None: + json_request( + f"/users/{urllib.parse.quote(creds.user_id)}/plays", + method="POST", + payload={"kink_id": kink_id, "interest_state": interest_state, "directions": ["together"]}, + headers=auth_headers(creds), + ) + + +def seed_completed_user() -> Credentials: + creds = create_user() + recs = json_request( + f"/users/{urllib.parse.quote(creds.user_id)}/recommendations?limit={STARTER_TARGET}", + headers=auth_headers(creds), + timeout=60, + ).get("items", []) + if len(recs) < STARTER_TARGET: + raise AssertionError(f"expected at least {STARTER_TARGET} recommendations, got {len(recs)}") + for item in recs[:STARTER_TARGET]: + save_play(creds, item["kink"]["id"]) + return creds + + def capture_dom_state(page: Page) -> dict: return page.evaluate( """() => ({ - hasModal: !!document.querySelector('.modal'), - hasAuthStrip: !!document.querySelector('.auth-strip'), - hasRoot: !!document.querySelector('#app'), + hasRoot: !!document.querySelector('#root'), + viewTestIds: [...document.querySelectorAll('[data-testid$="-view"], [data-testid="starter-gate"], [data-testid^="onboarding-"]')] + .map((el) => el.getAttribute('data-testid')).slice(0, 20), bodyText: document.body.innerText.slice(0, 700), - buttons: [...document.querySelectorAll('button')].map(b => (b.textContent || '').trim()).filter(Boolean).slice(0, 25), - inputs: [...document.querySelectorAll('input')].map(i => i.placeholder || i.type || '').slice(0, 12) - })""" + buttons: [...document.querySelectorAll('button')] + .map((button) => (button.textContent || '').trim()).filter(Boolean).slice(0, 25), + inputs: [...document.querySelectorAll('input')].map((input) => input.placeholder || input.type || '').slice(0, 12) + })""", ) -def capture_modal_state(page: Page) -> dict: - return page.evaluate( - """() => { - const modal = document.querySelector('.modal'); - if (!modal) return { present: false }; - const buttons = [...modal.querySelectorAll('button')].map((b) => ({ - text: (b.textContent || '').trim(), - aria: b.getAttribute('aria-label'), - disabled: !!b.disabled, - className: b.className || '' - })); - const inputs = [...modal.querySelectorAll('input')].map((i) => i.placeholder || i.type || ''); - return { present: true, buttons, inputs, text: modal.innerText.slice(0, 400) }; - }""" +def install_auth(page: Page, creds: Credentials | None) -> None: + payload = json.dumps({"userId": creds.user_id, "token": creds.private_token}) if creds else "" + script = ( + f"localStorage.setItem('kink_auth', JSON.stringify({payload}));" + if creds + else "localStorage.removeItem('kink_auth');" ) + page.add_init_script(script) -def switch_modal_to_login(page: Page) -> str: - modal = page.locator(".modal") - login_input = modal.get_by_placeholder("Profile ID") - if login_input.count() and login_input.is_visible(): - return "already_login" - - button = modal.get_by_label("Switch to log in") - methods: list[str] = [] - - try: - button.click(timeout=2500) - methods.append("playwright_click") - except Exception: - methods.append("playwright_click_failed") - - page.wait_for_timeout(250) - if login_input.count() and login_input.is_visible(): - return ",".join(methods) - - page.evaluate( - """() => { - const btn = document.querySelector('.modal button[aria-label="Switch to log in"]'); - if (btn) btn.click(); - }""" - ) - methods.append("dom_click") - page.wait_for_timeout(250) - if login_input.count() and login_input.is_visible(): - return ",".join(methods) - - try: - button.click(timeout=2500, force=True) - methods.append("force_click") - except Exception: - methods.append("force_click_failed") - - page.wait_for_timeout(250) - return ",".join(methods) - - -def modal_login(page: Page, creds: Credentials, notes: dict | None = None) -> dict: - notes = notes or {} - if page.locator(".modal").count() == 0 and page.get_by_role("button", name="Log in / create").count(): - page.get_by_role("button", name="Log in / create").click() - notes["opened_modal_from_shell"] = True - - modal = page.locator(".modal") - expect(modal).to_be_visible(timeout=15000) - notes["before_switch"] = capture_modal_state(page) - notes["switch_method"] = switch_modal_to_login(page) - notes["after_switch"] = capture_modal_state(page) - - profile_id = modal.get_by_placeholder("Profile ID") - expect(profile_id).to_be_visible(timeout=15000) - profile_id.fill(creds.user_id) - modal.get_by_placeholder("Private pass").fill(creds.private_token) - notes["before_submit"] = capture_modal_state(page) - - submit = modal.locator('button[aria-label="Submit login"]') - try: - with page.expect_response(lambda r: "/auth/login" in r.url and r.request.method == "POST", timeout=15000): - submit.click(timeout=3000) - notes["submit_method"] = "playwright_click" - except Exception as exc: - notes["submit_error"] = str(exc) - with page.expect_response(lambda r: "/auth/login" in r.url and r.request.method == "POST", timeout=15000): - page.evaluate( - """() => { - const btn = document.querySelector('.modal button[aria-label="Submit login"]'); - if (btn) btn.click(); - }""" - ) - notes["submit_method"] = "dom_click_fallback" - - expect(page.locator(".auth-strip")).to_contain_text(creds.user_id, timeout=15000) - notes["after_submit_auth_strip"] = page.locator(".auth-strip").inner_text() - return notes - - -def run_browser_scenario(name: str, setup_auth: Credentials | None, action, seed: dict | None = None) -> dict: +def run_browser_scenario( + name: str, + action: Callable[[Page, dict], None], + *, + setup_auth: Credentials | None = None, +) -> dict: events: list[dict] = [] page_errors: list[str] = [] console_errors: list[str] = [] - start = time.perf_counter() + started = time.perf_counter() def rec(evt: dict) -> None: - evt = dict(evt) - evt["t_ms"] = round((time.perf_counter() - start) * 1000, 1) - events.append(evt) - - with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - page = browser.new_page(viewport={"width": 1440, "height": 1000}) - if setup_auth: - page.add_init_script( - f"localStorage.setItem('kink_auth', JSON.stringify({json.dumps({'userId': setup_auth.user_id, 'token': setup_auth.private_token})}));" - ) - else: - page.add_init_script("localStorage.removeItem('kink_auth');") + item = dict(evt) + item["t_ms"] = round((time.perf_counter() - started) * 1000, 1) + events.append(item) + + with sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=True, args=["--no-sandbox", "--disable-http-cache"]) + context = browser.new_context(viewport={"width": 1440, "height": 1000}) + page = context.new_page() + install_auth(page, setup_auth) page.on("pageerror", lambda exc: page_errors.append(str(exc))) page.on("console", lambda msg: console_errors.append(f"{msg.type}: {msg.text}") if msg.type == "error" else None) page.on("request", lambda req: rec({"kind": "request", "method": req.method, "url": req.url})) page.on("response", lambda res: rec({"kind": "response", "status": res.status, "url": res.url})) - scenario = { "name": name, "ok": True, "checkpoints": [], "page_errors": page_errors, "console_errors": console_errors, - "events": events, - **(seed or {}), } try: - page.goto(BASE_URL, wait_until="domcontentloaded") - page.wait_for_timeout(1000) - scenario["checkpoints"].append({"label": "after_domcontentloaded", "state": capture_dom_state(page)}) + page.goto(BASE_URL, wait_until="domcontentloaded", timeout=120_000) + scenario["checkpoints"].append({"label": "boot", "state": capture_dom_state(page)}) action(page, scenario) + scenario["checkpoints"].append({"label": "after_action", "state": capture_dom_state(page)}) except Exception as exc: scenario["ok"] = False scenario["failure"] = str(exc) finally: screenshot = ARTIFACT_DIR / f"{name}.png" - try: - page.screenshot(path=str(screenshot), full_page=True) - scenario["screenshot"] = str(screenshot) - except Exception as exc: - scenario["screenshot_error"] = str(exc) + page.screenshot(path=str(screenshot), full_page=True) + scenario["screenshot"] = str(screenshot) + context.close() browser.close() - scenario["app_events"] = [e for e in events if e.get("url", "").startswith(BASE_URL)] - summary = {} - for evt in scenario["app_events"]: - url = evt.get("url", "") - path = url.split(BASE_URL, 1)[-1] or "/" - path = path.split("?", 1)[0] - key = f"{evt.get('kind')} {evt.get('method', evt.get('status', ''))} {path}" - summary[key] = summary.get(key, 0) + 1 - scenario["request_summary"] = dict(sorted(summary.items())) - scenario["event_counts"] = { - "total": len(events), - "app_only": len(scenario["app_events"]), - } - scenario.pop("events", None) - return scenario - - -def scenario_boot_unauth(page: Page, scenario: dict) -> None: - page.wait_for_timeout(1500) - scenario["checkpoints"].append({"label": "settled", "state": capture_dom_state(page)}) - - -def scenario_boot_stored_auth(page: Page, scenario: dict) -> None: - expect(page.locator(".auth-strip")).to_be_visible(timeout=15000) - page.wait_for_timeout(1500) - scenario["checkpoints"].append({"label": "authed", "state": capture_dom_state(page)}) - - -def scenario_modal_login(page: Page, scenario: dict) -> None: - creds = Credentials(**scenario["creds"]) - notes = {} - try: - scenario["modal_metrics"] = modal_login(page, creds, notes) - except Exception: - scenario["modal_metrics"] = notes - raise - scenario["checkpoints"].append({"label": "logged_in", "state": capture_dom_state(page)}) - - -def scenario_detail_staged(page: Page, scenario: dict) -> None: - expect(page.locator(".auth-strip")).to_be_visible(timeout=15000) - page.get_by_placeholder("Search plays").fill("bondage") - chip = page.locator(".panel").filter(has=page.get_by_text("Add Plays")).locator(".play-chip").filter(has=page.get_by_role("button", name="Bondage")).first - expect(chip).to_be_visible(timeout=15000) - with page.expect_response(lambda r: "/kinks/" in r.url and r.request.method == "GET" and r.status == 200, timeout=15000): - chip.get_by_role("button", name="Bondage").click() - detail = page.locator(".panel").filter(has=page.get_by_role("heading", name="Play Details")) - expect(detail.get_by_text("Pick your reaction first, then choose where it belongs.")).to_be_visible(timeout=15000) - locked_before = detail.locator(".direction-pill.locked").count() - play_posts_before = len([e for e in scenario["events"] if e.get("kind") == "request" and e.get("method") == "POST" and "/plays" in e.get("url", "")]) - start = time.perf_counter() - detail.get_by_title("Love").click() - expect(detail.locator(".direction-pill.locked")).to_have_count(0, timeout=2000) - reaction_unlock_ms = round((time.perf_counter() - start) * 1000, 1) - play_posts_after_reaction = len([e for e in scenario["events"] if e.get("kind") == "request" and e.get("method") == "POST" and "/plays" in e.get("url", "")]) - with page.expect_response(lambda r: "/plays" in r.url and r.request.method == "POST" and r.status == 200, timeout=15000) as save_resp: - detail.locator(".direction-pill").filter(has_text=re.compile(r"To Me")).first.click() - expect(page.locator(".status")).to_contain_text("Saved", timeout=5000) - play_posts_after_direction = len([e for e in scenario["events"] if e.get("kind") == "request" and e.get("method") == "POST" and "/plays" in e.get("url", "")]) - scenario["detail_metrics"] = { - "locked_before": locked_before, - "reaction_unlock_ms": reaction_unlock_ms, - "play_posts_before": play_posts_before, - "play_posts_after_reaction": play_posts_after_reaction, - "play_posts_after_direction": play_posts_after_direction, - "save_url": save_resp.value.url, - "save_status": save_resp.value.status, - } - scenario["checkpoints"].append({"label": "detail_saved", "state": capture_dom_state(page)}) + app_events = [event for event in events if event.get("url", "").startswith(BASE_URL)] + summary: dict[str, int] = {} + for event in app_events: + path = event["url"].split(BASE_URL, 1)[-1].split("?", 1)[0] or "/" + key = f"{event['kind']} {event.get('method', event.get('status', ''))} {path}" + summary[key] = summary.get(key, 0) + 1 + scenario["request_summary"] = dict(sorted(summary.items())) + scenario["event_counts"] = {"total": len(events), "app_only": len(app_events)} + return scenario + + +def scenario_boot_unauth(page: Page, _scenario: dict) -> None: + expect(page.get_by_test_id("onboarding-welcome")).to_be_visible(timeout=30_000) + expect(page.get_by_test_id("onboarding-create-profile")).to_be_visible(timeout=30_000) + + +def scenario_boot_stored_auth(page: Page, _scenario: dict) -> None: + expect(page.get_by_test_id("discover-view")).to_be_visible(timeout=30_000) + expect(page.get_by_test_id("desktop-nav")).to_be_visible(timeout=30_000) + + +def scenario_login_existing(page: Page, _scenario: dict) -> None: + creds = create_user() + page.get_by_test_id("onboarding-have-profile").click() + expect(page.get_by_test_id("onboarding-login")).to_be_visible(timeout=15_000) + page.get_by_test_id("onboarding-login-user-id").fill(creds.user_id) + page.get_by_test_id("onboarding-login-private-token").fill(creds.private_token) + page.get_by_test_id("onboarding-open-profile").click() + expect(page.get_by_test_id("starter-gate")).to_be_visible(timeout=60_000) + + +def scenario_starter_gate(page: Page, _scenario: dict) -> None: + expect(page.get_by_test_id("starter-gate")).to_be_visible(timeout=60_000) + expect(page.get_by_test_id("starter-discover-view")).to_be_visible(timeout=60_000) + expect(page.get_by_test_id("discover-action-bar")).to_be_visible(timeout=60_000) def main() -> int: - boot_auth_creds = create_user() - modal_creds = create_user() - detail_creds = create_user() - scenarios = [] - scenarios.append(run_browser_scenario("boot_unauth", None, scenario_boot_unauth)) - scenarios.append(run_browser_scenario("boot_stored_auth", boot_auth_creds, scenario_boot_stored_auth)) - scenarios.append( - run_browser_scenario( - "modal_login", - None, - scenario_modal_login, - seed={"creds": {"user_id": modal_creds.user_id, "private_token": modal_creds.private_token}}, - ) - ) - scenarios.append(run_browser_scenario("detail_staged", detail_creds, scenario_detail_staged)) - - out = {"generated_at": time.time(), "scenarios": scenarios} - out_path = ARTIFACT_DIR / "diagnostics.json" - out_path.write_text(json.dumps(out, indent=2)) - print( - json.dumps( - { - "generated_at": out["generated_at"], - "scenarios": [ - { - "name": s["name"], - "ok": s["ok"], - "page_errors": s["page_errors"], - "console_errors": s["console_errors"], - "failure": s.get("failure"), - "detail_metrics": s.get("detail_metrics"), - "modal_metrics": s.get("modal_metrics"), - "event_counts": s.get("event_counts"), - "request_summary": s.get("request_summary"), - } - for s in scenarios - ], - }, - indent=2, - ) - ) - failures = [s["name"] for s in scenarios if not s.get("ok") or s.get("page_errors") or s.get("console_errors")] - return 1 if failures else 0 + completed = seed_completed_user() + starter = create_user() + scenarios = [ + run_browser_scenario("boot_unauth", scenario_boot_unauth), + run_browser_scenario("boot_stored_auth", scenario_boot_stored_auth, setup_auth=completed), + run_browser_scenario("login_existing", scenario_login_existing), + run_browser_scenario("starter_gate", scenario_starter_gate, setup_auth=starter), + ] + report = {"generated_at": time.time(), "scenarios": scenarios} + out_path = ARTIFACT_DIR / "diagnose_ui_flow.json" + out_path.write_text(json.dumps(report, indent=2), encoding="utf-8") + print(json.dumps(report, indent=2)) + return 0 if all(scenario["ok"] for scenario in scenarios) else 1 if __name__ == "__main__": diff --git a/scripts/e2e_smoke.py b/scripts/e2e_smoke.py index 3fab3edd2bddc4dafecd55a0a44a7d353f14e3bd..5c19553e8b25e233db76a016ddcbeb2c9ad2d944 100644 --- a/scripts/e2e_smoke.py +++ b/scripts/e2e_smoke.py @@ -159,8 +159,9 @@ def run_api_smoke(credentials: dict[str, str]) -> None: search = get_json("/search?q=bondage&limit=1") top = search["items"][0]["kink"] - assert_true(top["id"] == "fetlife_10", "bondage search is not canonical fetlife row") + assert_true(top["name"].strip().lower() == "bondage", "bondage search did not return exact bondage row") assert_true(bool(top["has_images"]), "canonical bondage result should report images") + bondage_id = top["id"] roles = get_json("/roles/search?q=dominant&limit=3") assert_true(all(item["kink"]["content_kind"] in {"role", "dynamic"} for item in roles["items"]), "role search leaked non-role items") @@ -168,7 +169,7 @@ def run_api_smoke(credentials: dict[str, str]) -> None: plays = get_json("/search?q=dominant&limit=3") assert_true(plays["items"] == [], "play search should not surface dynamic role terms by default") - detail = get_json("/kinks/fetlife_10") + detail = get_json(f"/kinks/{bondage_id}") assert_true(detail["filtered_asset_count"] > 0, "bondage detail missing trusted assets") recs = get_json(f"/users/{credentials['profile_id']}/recommendations?limit=8", headers=headers) @@ -189,12 +190,12 @@ def run_api_smoke(credentials: dict[str, str]) -> None: partner_headers = {"x-private-token": partner["private_token"]} post_json( f"/users/{credentials['profile_id']}/plays", - {"kink_id": "fetlife_10", "interest_state": "love", "directions": ["to_me"]}, + {"kink_id": bondage_id, "interest_state": "love", "directions": ["to_me"]}, headers=headers, ) post_json( f"/users/{partner['id']}/plays", - {"kink_id": "fetlife_10", "interest_state": "love", "directions": ["by_me"]}, + {"kink_id": bondage_id, "interest_state": "love", "directions": ["by_me"]}, headers=partner_headers, ) post_json( @@ -218,7 +219,7 @@ def run_api_smoke(credentials: dict[str, str]) -> None: f"/users/{credentials['profile_id']}/partner-groups/{group['id']}/overlap", headers=headers, ) - assert_true(any(item["id"] == "fetlife_10" for item in overlap["direct_matches"]), "group overlap missed compatible play") + assert_true(any(item["id"] == bondage_id for item in overlap["direct_matches"]), "group overlap missed compatible play") def main() -> int: diff --git a/scripts/playwright_product_flow.py b/scripts/playwright_product_flow.py index 19b4ff710be6fd572bb3cdf8cf07f8cf0e86cb53..3a4d3d1be72c0916a18c54800900094dde5fa88b 100644 --- a/scripts/playwright_product_flow.py +++ b/scripts/playwright_product_flow.py @@ -157,6 +157,15 @@ def find_first_exact_name_not_in(options: list[tuple[str, str]], excluded_ids: s raise AssertionError(f"Could not find any exact kink match outside excluded ids from: {[name for _, name in options]}") +def ensure_exact_options_absent(creds: Credentials, options: list[tuple[str, str]]) -> None: + for query, expected_name in options: + try: + kink_id = find_kink_id(query, expected_name) + except AssertionError: + continue + ensure_play_absent(creds, kink_id) + + def find_parent_with_scenarios() -> tuple[str, str, str]: candidates = [("anal", "Anal"), ("touching", "Touching"), ("oral sex", "Oral Sex"), ("oral", "Oral")] for query, expected_name in candidates: @@ -168,6 +177,21 @@ def find_parent_with_scenarios() -> tuple[str, str, str]: raise AssertionError("Could not find a parent kink with scenarios") +def find_parent_with_scenario_pair() -> tuple[str, str, str, str]: + candidates = [("anal", "Anal"), ("touching", "Touching"), ("oral sex", "Oral Sex"), ("oral", "Oral")] + for query, expected_name in candidates: + parent_id = find_kink_id(query, expected_name) + items = get_json(f"/kinks/{urllib.parse.quote(parent_id)}/scenarios?limit=12").get("items", []) + scenario_ids: list[str] = [] + for item in items: + scenario_id = item.get("scenario", {}).get("id") + if scenario_id and scenario_id not in scenario_ids: + scenario_ids.append(scenario_id) + if len(scenario_ids) >= 2: + return parent_id, expected_name, scenario_ids[0], scenario_ids[1] + raise AssertionError("Could not find a parent kink with at least two scenarios") + + def seed_user_with_recommendations(creds: Credentials, target_count: int) -> list[str]: user = get_user(creds) seen = set(user.get("plays", {})) @@ -502,17 +526,20 @@ def scenario_couple_audit(browser: Browser) -> dict[str, object]: expect(right_page.get_by_test_id("together-group-picker")).to_be_visible(timeout=25000) seed_user_with_recommendations(right, 50) - left_play_ids = set(get_user(left).get("plays", {})) - direct_id = find_kink_id("bondage", "Bondage") - left_only_related = find_kink_id("oral sex", "Oral Sex") - right_only_related, right_related_name = find_first_exact_name_not_in([ + related_options = [ ("blow jobs", "Blow Jobs"), ("cocksucking", "Cocksucking"), - ], left_play_ids) - right_private_only, private_only_name = find_first_exact_name_not_in([ + ] + private_options = [ ("foot worship", "Foot Worship"), ("lingerie", "Lingerie"), - ], left_play_ids) + ] + ensure_exact_options_absent(left, related_options + private_options) + left_play_ids = set(get_user(left).get("plays", {})) + direct_id = find_kink_id("bondage", "Bondage") + left_only_related = find_kink_id("oral sex", "Oral Sex") + right_only_related, right_related_name = find_first_exact_name_not_in(related_options, left_play_ids) + right_private_only, private_only_name = find_first_exact_name_not_in(private_options, left_play_ids) ensure_play_absent(right, left_only_related) ensure_play_absent(left, right_only_related) @@ -524,11 +551,12 @@ def scenario_couple_audit(browser: Browser) -> dict[str, object]: save_play(right, right_only_related, "like", ["together"]) save_play(right, right_private_only, "love", ["together"]) - parent_id, parent_name, scenario_id = find_parent_with_scenarios() + parent_id, parent_name, scenario_id, related_scenario_id = find_parent_with_scenario_pair() save_play(left, parent_id, "like", ["together"]) save_play(right, parent_id, "like", ["together"]) save_scenario(left, parent_id, scenario_id, "like", ["together"]) save_scenario(right, parent_id, scenario_id, "like", ["together"]) + save_scenario(right, parent_id, related_scenario_id, "like", ["together"]) group_id = group_id_for_pair(left, right.user_id) checkpoints.append( @@ -642,12 +670,18 @@ def scenario_couple_audit(browser: Browser) -> dict[str, object]: left_page.get_by_test_id("together-tab-explore").click() expect(left_page.get_by_test_id("together-explore-panel")).to_be_visible(timeout=15000) expect(left_page.get_by_test_id("together-explore-related-section")).to_be_visible(timeout=15000) + expect(left_page.get_by_test_id("together-explore-scenarios-section")).to_be_visible(timeout=15000) + expect(left_page.get_by_test_id(f"together-explore-scenario-{related_scenario_id}")).to_be_visible(timeout=15000) checkpoints.append( { "stage": "explore_visible", - "expected": "Together > Worth Exploring shows related but not identical likes", - "actual": {"left_name": "Oral Sex", "right_name": right_related_name}, - "summary": "related-match section rendered", + "expected": "Together > Worth Exploring shows related but not identical likes and connected scenarios", + "actual": { + "left_name": "Oral Sex", + "right_name": right_related_name, + "related_scenario_id": related_scenario_id, + }, + "summary": "related-match and connected-scenario sections rendered", } ) diff --git a/scripts/publish_hf_space.py b/scripts/publish_hf_space.py index 8de5cee96106f138291682b25a10f429b420a718..fa8275cbb12c1ab1cd653a342b3d52baccd1e942 100644 --- a/scripts/publish_hf_space.py +++ b/scripts/publish_hf_space.py @@ -45,6 +45,8 @@ _IGNORE = [ ".venv", ".venv/**", ".git", + ".env", + ".env.local", "**/__pycache__/**", ".pytest_cache/**", "data/**", diff --git a/scripts/record_product_flows.py b/scripts/record_product_flows.py index 5fe98f19a4f4035841537a42d16506bcac1f5cc2..194e312f58ccff7d06fca59ddcef580b09989c7c 100644 --- a/scripts/record_product_flows.py +++ b/scripts/record_product_flows.py @@ -2,530 +2,39 @@ from __future__ import annotations import json import os -import re +import sys import time -import urllib.error -import urllib.request -from dataclasses import dataclass +from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path -from playwright.sync_api import Page, expect, sync_playwright - -BASE_URL = os.environ.get("KINK_AUDIT_BASE_URL", "http://127.0.0.1:8011") -ARTIFACT_DIR = Path("/tmp/kink_product_recordings") +BASE_URL = os.environ.get("KINK_AUDIT_BASE_URL", "http://127.0.0.1:8011").rstrip("/") +ARTIFACT_DIR = Path(os.environ.get("KINK_AUDIT_ARTIFACT_DIR", "/tmp/kink_product_recordings")) ARTIFACT_DIR.mkdir(parents=True, exist_ok=True) -BONDAGE_ID = "fetlife_10" -SPANKING_ID = "fetlife_96" -ORAL_ID = "fetlife_71" -DOMINANT_ROLE_ID = "fetlife_kinktionary_dominant_wzwjm" - - -@dataclass -class Credentials: - user_id: str - private_token: str - - -def auth_headers(creds: Credentials) -> dict[str, str]: - return {"x-private-token": creds.private_token} - - -def post_json(path: str, payload: dict, headers: dict[str, str] | None = None, timeout: int = 20) -> dict: - request_headers = {"Content-Type": "application/json"} - if headers: - request_headers.update(headers) - request = urllib.request.Request( - f"{BASE_URL}{path}", - data=json.dumps(payload).encode("utf-8"), - headers=request_headers, - method="POST", - ) - with urllib.request.urlopen(request, timeout=timeout) as response: - return json.loads(response.read().decode("utf-8")) - - -def get_json(path: str, headers: dict[str, str] | None = None, timeout: int = 20) -> dict: - request = urllib.request.Request(f"{BASE_URL}{path}", headers=headers or {}) - with urllib.request.urlopen(request, timeout=timeout) as response: - return json.loads(response.read().decode("utf-8")) - - -def delete_json(path: str, headers: dict[str, str] | None = None, timeout: int = 20) -> dict: - request = urllib.request.Request(f"{BASE_URL}{path}", headers=headers or {}, method="DELETE") - with urllib.request.urlopen(request, timeout=timeout) as response: - return json.loads(response.read().decode("utf-8")) - - -def create_user() -> Credentials: - payload = post_json("/users", {}) - return Credentials(user_id=payload["id"], private_token=payload["private_token"]) - - -def save_play(creds: Credentials, kink_id: str, interest_state: str, directions: list[str]) -> dict: - return post_json( - f"/users/{creds.user_id}/plays", - {"kink_id": kink_id, "interest_state": interest_state, "directions": directions}, - headers=auth_headers(creds), - ) - - -def save_role(creds: Credentials, kink_id: str, selected: bool = True) -> dict: - return post_json( - f"/users/{creds.user_id}/roles", - {"kink_id": kink_id, "selected": selected}, - headers=auth_headers(creds), - timeout=60, - ) - - -def link_partner(creds: Credentials, partner_id: str) -> dict: - return post_json( - f"/users/{creds.user_id}/partners", - {"partner_id": partner_id}, - headers=auth_headers(creds), - ) - - -def create_partner_group(creds: Credentials, name: str, member_ids: list[str]) -> dict: - return post_json( - f"/users/{creds.user_id}/partner-groups", - {"name": name, "member_ids": member_ids}, - headers=auth_headers(creds), - ) - - -def add_partner_group_members(creds: Credentials, group_id: str, member_ids: list[str]) -> dict: - return post_json( - f"/users/{creds.user_id}/partner-groups/{group_id}/members", - {"member_ids": member_ids}, - headers=auth_headers(creds), - ) - - -def capture_shell_state(page: Page) -> dict: - return page.evaluate( - """() => ({ - bodyText: document.body.innerText.slice(0, 900), - authText: document.querySelector('.auth-strip')?.innerText || '', - statusText: document.querySelector('.status')?.innerText || '', - modalTitle: document.querySelector('.modal h2')?.textContent || '', - boardColumns: [...document.querySelectorAll('.board-grid .board-column')].map((col) => ({ - key: col.getAttribute('data-column-key') || '', - live: col.classList.contains('drop-live'), - text: col.innerText.slice(0, 220) - })) - })""" - ) - - -def modal_to_login(page: Page) -> None: - modal = page.locator('.modal') - expect(modal).to_be_visible(timeout=15000) - if modal.get_by_placeholder('Profile ID').count() and modal.get_by_placeholder('Profile ID').is_visible(): - return - btn = modal.get_by_label('Switch to log in') - try: - btn.click(timeout=2500) - except Exception: - page.evaluate("""() => document.querySelector('.modal button[aria-label=\"Switch to log in\"]')?.click()""") - expect(modal.get_by_placeholder('Profile ID')).to_be_visible(timeout=15000) - - -def login_via_modal(page: Page, creds: Credentials) -> None: - if page.locator('.auth-strip').count() and creds.user_id in page.locator('.auth-strip').inner_text(): - return - if page.locator('.modal').count() == 0: - page.get_by_role('button', name='Log in / create').click() - modal_to_login(page) - modal = page.locator('.modal') - modal.get_by_placeholder('Profile ID').fill(creds.user_id) - modal.get_by_placeholder('Private pass').fill(creds.private_token) - with page.expect_response(lambda r: '/auth/login' in r.url and r.request.method == 'POST' and r.status == 200, timeout=15000): - modal.get_by_role('button', name='Open profile').click() - expect(page.locator('.auth-strip')).to_contain_text(creds.user_id, timeout=15000) - - -def run_browser_scenario(name: str, action, setup_auth: Credentials | None = None, viewport: tuple[int, int] = (1440, 1000), init_script: str | None = None) -> dict: - events: list[dict] = [] - page_errors: list[str] = [] - console_errors: list[str] = [] - start = time.perf_counter() - def rec(evt: dict) -> None: - item = dict(evt) - item['t_ms'] = round((time.perf_counter() - start) * 1000, 1) - events.append(item) - with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - page = browser.new_page(viewport={'width': viewport[0], 'height': viewport[1]}) - if setup_auth: - page.add_init_script( - f"localStorage.setItem('kink_auth', JSON.stringify({json.dumps({'userId': setup_auth.user_id, 'token': setup_auth.private_token})}));" - ) - else: - page.add_init_script("localStorage.removeItem('kink_auth');") - if init_script: - page.add_init_script(init_script) - page.on('pageerror', lambda exc: page_errors.append(str(exc))) - page.on('console', lambda msg: console_errors.append(f"{msg.type}: {msg.text}") if msg.type == 'error' else None) - page.on('request', lambda req: rec({'kind': 'request', 'method': req.method, 'url': req.url})) - page.on('response', lambda res: rec({'kind': 'response', 'status': res.status, 'url': res.url})) - result = { - 'name': name, - 'ok': True, - 'page_errors': page_errors, - 'console_errors': console_errors, - 'checkpoints': [], - 'events': events, - } - try: - page.goto(BASE_URL, wait_until='domcontentloaded') - page.wait_for_timeout(1000) - result['checkpoints'].append({'label': 'boot', 'state': capture_shell_state(page)}) - action(page, result) - except Exception as exc: - result['ok'] = False - result['failure'] = str(exc) - finally: - shot = ARTIFACT_DIR / f'{name}.png' - try: - page.screenshot(path=str(shot), full_page=True) - result['screenshot'] = str(shot) - except Exception as exc: - result['screenshot_error'] = str(exc) - browser.close() - - if name == 'stale_auth': - console_errors[:] = [msg for msg in console_errors if '401 (Unauthorized)' not in msg] - app_events = [e for e in events if e.get('url', '').startswith(BASE_URL)] - summary: dict[str, int] = {} - for evt in app_events: - path = evt['url'].split(BASE_URL, 1)[-1].split('?', 1)[0] or '/' - key = f"{evt['kind']} {evt.get('method', evt.get('status', ''))} {path}" - summary[key] = summary.get(key, 0) + 1 - result['request_summary'] = dict(sorted(summary.items())) - result['event_counts'] = {'total': len(events), 'app_only': len(app_events)} - result.pop('events', None) - return result - - -def scenario_detail_staged(page: Page, result: dict) -> None: - expect(page.locator('.auth-strip')).to_be_visible(timeout=15000) - page.get_by_placeholder('Search plays').fill('bondage') - chip = page.locator('.panel').filter(has=page.get_by_text('Add Plays')).locator('.play-chip').filter(has=page.get_by_role('button', name='Bondage')).first - expect(chip).to_be_visible(timeout=15000) - with page.expect_response(lambda r: '/kinks/' in r.url and r.request.method == 'GET' and r.status == 200, timeout=15000): - chip.get_by_role('button', name='Bondage').click() - detail = page.locator('.panel').filter(has=page.get_by_role('heading', name='Play Details')) - expect(detail.get_by_text('Pick your reaction first, then choose where it belongs.')).to_be_visible(timeout=15000) - post_before = len([e for e in result['events'] if e.get('kind') == 'request' and e.get('method') == 'POST' and '/plays' in e.get('url', '')]) - started = time.perf_counter() - detail.get_by_title('Love').click() - expect(detail.locator('.direction-pill.locked')).to_have_count(0, timeout=2000) - unlock_ms = round((time.perf_counter() - started) * 1000, 1) - post_after_reaction = len([e for e in result['events'] if e.get('kind') == 'request' and e.get('method') == 'POST' and '/plays' in e.get('url', '')]) - started = time.perf_counter() - with page.expect_response(lambda r: '/plays' in r.url and r.request.method == 'POST' and r.status == 200, timeout=15000): - detail.locator('.direction-pill').filter(has_text=re.compile(r'To Me')).first.click() - expect(page.locator('.status')).to_contain_text('Saved', timeout=5000) - result['metrics'] = { - 'reaction_unlock_ms': unlock_ms, - 'direction_save_ms': round((time.perf_counter() - started) * 1000, 1), - 'play_posts_before': post_before, - 'play_posts_after_reaction': post_after_reaction, - 'play_posts_after_direction': len([e for e in result['events'] if e.get('kind') == 'request' and e.get('method') == 'POST' and '/plays' in e.get('url', '')]), - } - - -def scenario_board_drag(page: Page, result: dict) -> None: - expect(page.locator('.auth-strip')).to_be_visible(timeout=15000) - board_columns = page.locator('.board-grid .board-column') - expect(board_columns).to_have_count(3, timeout=15000) - together_column = board_columns.nth(2) - to_me_column = board_columns.nth(0) - source_chip = together_column.locator('.play-chip').filter(has=page.get_by_role('button', name='Bondage')).first - expect(source_chip).to_be_visible(timeout=15000) - target_zone = to_me_column.locator('.board-drop-zone') - box = target_zone.bounding_box() - if not box: - raise AssertionError('missing board target box') - started = time.perf_counter() - with page.expect_response(lambda r: '/plays' in r.url and r.request.method == 'POST' and r.status == 200, timeout=15000): - source_chip.drag_to(target_zone, target_position={'x': max(18, int(box['width'] * 0.55)), 'y': max(18, int(box['height'] * 0.82))}) - expect(to_me_column.locator('.play-chip').filter(has=page.get_by_role('button', name='Bondage')).first).to_be_visible(timeout=5000) - result['metrics'] = { - 'drag_save_ms': round((time.perf_counter() - started) * 1000, 1), - 'drop_live_count_after': page.locator('.board-column.drop-live').count(), - 'column_boxes': page.evaluate("""() => [...document.querySelectorAll('.board-grid .board-column')].map((col) => { const r = col.getBoundingClientRect(); return { key: col.getAttribute('data-column-key'), width: Math.round(r.width), height: Math.round(r.height) }; })"""), - } - - -def scenario_double_click_rate(page: Page, result: dict) -> None: - expect(page.locator('.auth-strip')).to_be_visible(timeout=15000) - page.get_by_placeholder('Search plays').fill('bondage') - chip = page.locator('.panel').filter(has=page.get_by_text('Add Plays')).locator('.play-chip').filter(has=page.get_by_role('button', name='Bondage')).first - expect(chip).to_be_visible(timeout=15000) - love_button = chip.get_by_title('Love') - before = len([e for e in result['events'] if e.get('kind') == 'request' and e.get('method') == 'POST' and '/plays' in e.get('url', '')]) - love_button.click() - love_button.click() - page.wait_for_timeout(1200) - after = len([e for e in result['events'] if e.get('kind') == 'request' and e.get('method') == 'POST' and '/plays' in e.get('url', '')]) - result['metrics'] = { - 'play_posts_for_two_identical_clicks': after - before, - 'status': page.locator('.status').inner_text(), - } - - -def scenario_onboarding_roles(page: Page, result: dict) -> None: - modal = page.locator('.modal') - expect(modal).to_be_visible(timeout=15000) - with page.expect_response(lambda r: '/users' in r.url and r.request.method == 'POST' and r.status == 200, timeout=15000): - modal.get_by_role('button', name='Create profile').click() - expect(modal).to_contain_text('Choose roles', timeout=15000) - codes = modal.locator('.creds code') - expect(codes).to_have_count(2) - creds = Credentials(user_id=codes.nth(0).inner_text().strip(), private_token=codes.nth(1).inner_text().strip()) - search = modal.get_by_placeholder('Search roles and dynamics') - search.fill('Dominant') - page.wait_for_timeout(400) - role_chip = modal.locator('.play-chip').filter(has_text='Dominant').first - if role_chip.count(): - role_chip.get_by_role('button', name=re.compile('Add role|Selected role')).click() - else: - save_role(creds, DOMINANT_ROLE_ID, True) - modal.get_by_role('button', name='Open Play List').click() - expect(page.locator('.auth-strip')).to_contain_text(creds.user_id, timeout=15000) - roles_payload = get_json(f'/users/{creds.user_id}/roles', headers=auth_headers(creds)) - names = [item['name'] for item in roles_payload['items']] - result['metrics'] = { - 'created_user': creds.user_id, - 'has_dominant_role': 'Dominant' in names, - 'role_count': len(names), - } - - -def scenario_prompt_answer(page: Page, result: dict, owner: Credentials) -> None: - expect(page.locator('.auth-strip')).to_be_visible(timeout=15000) - shared_panel = page.locator('.panel').filter(has=page.get_by_role('heading', name='Shared Play List')) - shared_panel.get_by_role('button', name=re.compile(r'Prompt Duo \(2\)')).click() - expect(page.get_by_placeholder('Select a group to view shared overlap')).to_have_value(re.compile('Prompt Duo'), timeout=15000) - card = page.locator('.panel').filter(has=page.get_by_role('heading', name='For You')).locator('.play-chip').filter(has=page.get_by_role('button', name='Bondage')).first - expect(card).to_be_visible(timeout=15000) - before = len([e for e in result['events'] if e.get('kind') == 'request' and '/prompts/' in e.get('url', '') and e.get('method') == 'POST']) - with page.expect_response(lambda r: '/prompts/' in r.url and r.request.method == 'POST' and r.status == 200, timeout=15000): - card.get_by_title('Like').click() - page.wait_for_timeout(700) - after = len([e for e in result['events'] if e.get('kind') == 'request' and '/prompts/' in e.get('url', '') and e.get('method') == 'POST']) - group_id = get_json(f"/users/{owner.user_id}/partner-groups", headers=auth_headers(owner))['items'][0]['id'] - prompts = get_json(f"/users/{owner.user_id}/partner-groups/{group_id}/prompts?limit=12", headers=auth_headers(owner)) - overlap = get_json(f"/users/{owner.user_id}/partner-groups/{group_id}/overlap", headers=auth_headers(owner)) - result['metrics'] = { - 'prompt_posts': after - before, - 'bondage_remaining_in_prompts': any(item['kink']['name'] == 'Bondage' for item in prompts['items']), - 'bondage_in_overlap': any(item['name'] == 'Bondage' for item in overlap['direct_matches']), - } - - -def scenario_undo_restore(page: Page, result: dict, creds: Credentials) -> None: - expect(page.locator('.auth-strip')).to_be_visible(timeout=15000) - page.get_by_placeholder('Search plays').fill('bondage') - chip = page.locator('.panel').filter(has=page.get_by_text('Add Plays')).locator('.play-chip').filter(has=page.get_by_role('button', name='Bondage')).first - expect(chip).to_be_visible(timeout=15000) - with page.expect_response(lambda r: '/plays' in r.url and r.request.method == 'POST' and r.status == 200, timeout=15000): - chip.get_by_title('Love').click() - expect(page.locator('.undo-toast')).to_have_count(0, timeout=3000) - page.wait_for_timeout(1200) - board = get_json(f"/users/{creds.user_id}/play-board", headers=auth_headers(creds)) - ids = {item['id'] for lane in ['to_me', 'by_me', 'together'] for item in board.get(lane, [])} - result['metrics'] = { - 'undo_toast_absent': True, - 'bondage_present_after_save': BONDAGE_ID in ids, - } - - -def scenario_stale_auth(page: Page, result: dict) -> None: - page.wait_for_timeout(1500) - modal = page.locator('.modal') - has_modal = modal.count() > 0 - auth_text = page.locator('.auth-strip').first.inner_text() if page.locator('.auth-strip').count() else '' - status_nodes = page.locator('.status') - status_texts = status_nodes.all_inner_texts() if status_nodes.count() else [] - result['metrics'] = { - 'has_modal_after_stale_auth': has_modal, - 'auth_strip_text': auth_text, - 'status_texts': status_texts, - 'modal_title': modal.locator('h2').first.inner_text() if has_modal and modal.locator('h2').count() else '', - 'status_count': len([item for item in status_texts if item.strip()]), - } - if not has_modal: - raise AssertionError('stale auth did not return to auth modal') - if result['metrics']['status_count'] != 1: - raise AssertionError(f"expected exactly one visible status message, got {result['metrics']['status_count']}") - - -def scenario_partner_group_overlap(page: Page, result: dict) -> None: - expect(page.locator('.auth-strip')).to_be_visible(timeout=15000) - shared_panel = page.locator('.panel').filter(has=page.get_by_role('heading', name='Shared Play List')) - group_chip = shared_panel.get_by_role('button', name=re.compile(r'Recorder Group \(2\)')).first - expect(group_chip).to_be_visible(timeout=15000) - group_chip.click() - expect(page.locator('.subhead').filter(has_text='Shared interests')).to_be_visible(timeout=15000) - text = page.locator('.shared-grid').inner_text() - result['metrics'] = { - 'has_bondage_overlap': 'Bondage' in text, - 'shared_text_excerpt': text[:500], - } - - -def scenario_group_edit_and_switch(page: Page, result: dict, owner: Credentials, beta: Credentials) -> None: - expect(page.locator('.auth-strip')).to_be_visible(timeout=15000) - shared_panel = page.locator('.panel').filter(has=page.get_by_role('heading', name='Shared Play List')) - duo_a = shared_panel.get_by_role('button', name='A Duo (2)') - duo_b = shared_panel.get_by_role('button', name='B Duo (2)') - trio = shared_panel.get_by_role('button', name='Trio (3)') - expect(duo_a).to_be_visible(timeout=15000) - duo_a.click() - expect(page.get_by_placeholder('Select a group to view shared overlap')).to_have_value(re.compile('A Duo'), timeout=15000) - a_text = page.locator('.shared-grid').inner_text() - duo_b.click() - expect(page.get_by_placeholder('Select a group to view shared overlap')).to_have_value(re.compile('B Duo'), timeout=15000) - b_text = page.locator('.shared-grid').inner_text() - trio.click() - expect(page.get_by_placeholder('Select a group to view shared overlap')).to_have_value(re.compile('Trio'), timeout=15000) - trio_text = page.locator('.shared-grid').inner_text() - - editable = shared_panel.get_by_role('button', name='Editable (3)') - editable.click() - expect(page.get_by_placeholder('Select a group to view shared overlap')).to_have_value(re.compile('Editable'), timeout=15000) - editable_group_id = get_json(f"/users/{owner.user_id}/partner-groups", headers=auth_headers(owner))['items'][-1]['id'] - overlap_path = f"/users/{owner.user_id}/partner-groups/{editable_group_id}/overlap" - prompts_path = f"/users/{owner.user_id}/partner-groups/{editable_group_id}/prompts" - recs_path = f"/users/{owner.user_id}/recommendations" - before_overlap = len([e for e in result['events'] if e.get('kind') == 'request' and overlap_path in e.get('url', '')]) - before_prompts = len([e for e in result['events'] if e.get('kind') == 'request' and prompts_path in e.get('url', '')]) - before_recs = len([e for e in result['events'] if e.get('kind') == 'request' and recs_path in e.get('url', '') and f'group_id={editable_group_id}' in e.get('url', '')]) - victim = beta.user_id - shared_panel.get_by_role('button', name=f'{victim} ×').click() - expect(shared_panel.get_by_role('button', name='Editable (2)')).to_be_visible(timeout=15000) - share_toggle = shared_panel.get_by_label('Share my full list') - with page.expect_response(lambda r: '/share-toggle' in r.url and r.request.method == 'POST' and r.status == 200, timeout=15000): - share_toggle.click() - page.wait_for_timeout(500) - groups = get_json(f"/users/{owner.user_id}/partner-groups", headers=auth_headers(owner))['items'] - editable_group = next(item for item in groups if item['name'] == 'Editable') - after_overlap = len([e for e in result['events'] if e.get('kind') == 'request' and overlap_path in e.get('url', '')]) - after_prompts = len([e for e in result['events'] if e.get('kind') == 'request' and prompts_path in e.get('url', '')]) - after_recs = len([e for e in result['events'] if e.get('kind') == 'request' and recs_path in e.get('url', '') and f'group_id={editable_group_id}' in e.get('url', '')]) - result['metrics'] = { - 'a_has_bondage': 'Bondage' in a_text and 'Oral Sex' not in a_text, - 'b_has_oral': 'Oral Sex' in b_text and 'Bondage' not in b_text, - 'trio_only_common': 'Spanking' in trio_text and 'Bondage' not in trio_text and 'Oral Sex' not in trio_text, - 'editable_member_removed': victim not in editable_group['member_ids'], - 'editable_share_enabled': bool(editable_group.get('my_share_full_list')), - 'editable_overlap_refreshes': after_overlap - before_overlap, - 'editable_prompts_refreshes': after_prompts - before_prompts, - 'editable_recommendation_refreshes': after_recs - before_recs, - } - if result['metrics']['editable_overlap_refreshes'] > 1: - raise AssertionError(f"too many editable overlap refreshes: {result['metrics']['editable_overlap_refreshes']}") - if result['metrics']['editable_prompts_refreshes'] > 1: - raise AssertionError(f"too many editable prompts refreshes: {result['metrics']['editable_prompts_refreshes']}") - if result['metrics']['editable_recommendation_refreshes'] > 1: - raise AssertionError(f"too many editable recommendation refreshes: {result['metrics']['editable_recommendation_refreshes']}") - - -def scenario_mobile_shell(page: Page, result: dict) -> None: - expect(page.locator('.auth-strip')).to_be_visible(timeout=15000) - expect(page.get_by_role('heading', name='For You', exact=True)).to_be_visible(timeout=15000) - result['metrics'] = page.evaluate( - """() => { - const header = document.querySelector('.header')?.getBoundingClientRect(); - const panels = [...document.querySelectorAll('.panel')].slice(0, 3).map((panel) => { - const r = panel.getBoundingClientRect(); - const h = panel.querySelector('h2, h3'); - return { heading: h?.textContent?.trim() || '', top: Math.round(r.top), height: Math.round(r.height) }; - }); - return { viewport: { width: window.innerWidth, height: window.innerHeight }, headerBottom: header ? Math.round(header.bottom) : null, firstPanels: panels }; - }""" - ) - for panel in result['metrics']['firstPanels']: - if panel['heading'] == 'For You' and panel['height'] > 920: - raise AssertionError(f"For You panel still too tall: {panel['height']}") - if panel['heading'] == 'Your Play List' and panel['top'] > 1450: - raise AssertionError(f"Your Play List starts too low: {panel['top']}") +def load_product_flow_module(): + path = Path(__file__).resolve().parent / "playwright_product_flow.py" + spec = spec_from_file_location("playwright_product_flow", path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Could not load {path}") + module = module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module def main() -> int: - detail_creds = create_user() - drag_creds = create_user(); save_play(drag_creds, BONDAGE_ID, 'love', ['together']) - double_click_creds = create_user() - - prompt_owner = create_user(); prompt_partner = create_user() - save_play(prompt_partner, BONDAGE_ID, 'love', ['together']) - link_partner(prompt_owner, prompt_partner.user_id) - create_partner_group(prompt_owner, 'Prompt Duo', [prompt_partner.user_id]) - - undo_creds = create_user() - - overlap_owner = create_user(); overlap_partner = create_user() - save_play(overlap_owner, BONDAGE_ID, 'love', ['together']) - save_play(overlap_partner, BONDAGE_ID, 'love', ['together']) - link_partner(overlap_owner, overlap_partner.user_id) - create_partner_group(overlap_owner, 'Recorder Group', [overlap_partner.user_id]) - - owner = create_user(); alpha = create_user(); beta = create_user() - save_play(owner, BONDAGE_ID, 'love', ['to_me']) - save_play(alpha, BONDAGE_ID, 'love', ['by_me']) - save_play(owner, ORAL_ID, 'love', ['together']) - save_play(beta, ORAL_ID, 'love', ['together']) - save_play(owner, SPANKING_ID, 'love', ['together']) - save_play(alpha, SPANKING_ID, 'love', ['together']) - save_play(beta, SPANKING_ID, 'love', ['together']) - link_partner(owner, alpha.user_id) - link_partner(owner, beta.user_id) - create_partner_group(owner, 'A Duo', [alpha.user_id]) - create_partner_group(owner, 'B Duo', [beta.user_id]) - create_partner_group(owner, 'Trio', [alpha.user_id, beta.user_id]) - editable = create_partner_group(owner, 'Editable', [alpha.user_id, beta.user_id]) - - mobile_creds = create_user(); save_play(mobile_creds, BONDAGE_ID, 'like', ['to_me']) - - scenarios = [ - run_browser_scenario('detail_staged_desktop', scenario_detail_staged, setup_auth=detail_creds), - run_browser_scenario('board_drag_whitespace', scenario_board_drag, setup_auth=drag_creds), - run_browser_scenario('double_click_identical_rate', scenario_double_click_rate, setup_auth=double_click_creds), - run_browser_scenario('onboarding_roles', scenario_onboarding_roles), - run_browser_scenario('prompt_answer_flow', lambda page, result: scenario_prompt_answer(page, result, prompt_owner), setup_auth=prompt_owner), - run_browser_scenario('undo_restore', lambda page, result: scenario_undo_restore(page, result, undo_creds), setup_auth=undo_creds), - run_browser_scenario('partner_group_overlap', scenario_partner_group_overlap, setup_auth=overlap_owner), - run_browser_scenario('group_edit_and_switch', lambda page, result: scenario_group_edit_and_switch(page, result, owner, beta), setup_auth=owner), - run_browser_scenario('stale_auth', scenario_stale_auth, init_script="localStorage.setItem('kink_auth', JSON.stringify({userId: 'stale-user', token: 'stale-token'}));"), - run_browser_scenario('mobile_shell', scenario_mobile_shell, setup_auth=mobile_creds, viewport=(390, 844)), - ] - - out = {'generated_at': time.time(), 'scenarios': scenarios} - out_path = ARTIFACT_DIR / 'product_recordings.json' - out_path.write_text(json.dumps(out, indent=2)) - print(json.dumps({ - 'generated_at': out['generated_at'], - 'scenarios': [ - { - 'name': s['name'], - 'ok': s['ok'], - 'failure': s.get('failure'), - 'page_errors': s['page_errors'], - 'console_errors': s['console_errors'], - 'metrics': s.get('metrics'), - 'event_counts': s.get('event_counts'), - 'request_summary': s.get('request_summary'), - } - for s in scenarios - ], - }, indent=2)) - return 1 if any((not s['ok']) or s['page_errors'] or s['console_errors'] for s in scenarios) else 0 - - -if __name__ == '__main__': + flow = load_product_flow_module() + flow.BASE_URL = BASE_URL + results = flow.run_selected_scenarios(["all"], headless=True) + payload = flow.results_to_json(results) + payload["generated_at"] = time.time() + payload["base_url"] = BASE_URL + out_path = ARTIFACT_DIR / "record_product_flows.json" + out_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") + print(json.dumps({"recording_json": str(out_path), "ok": payload["ok"], "summary": payload["summary"]}, indent=2)) + return 0 if payload["ok"] else 1 + + +if __name__ == "__main__": raise SystemExit(main()) diff --git a/scripts/ui_visual_audit.py b/scripts/ui_visual_audit.py index bd1cba7203cd0eeb1ab105695fb793378877776c..4babd02c8ee33ef62fc9851d8fcdbe5c8bc8f6ef 100644 --- a/scripts/ui_visual_audit.py +++ b/scripts/ui_visual_audit.py @@ -3,21 +3,21 @@ from __future__ import annotations import json import os import time +import urllib.parse import urllib.request from dataclasses import dataclass from pathlib import Path from playwright.sync_api import Page, expect, sync_playwright -BASE_URL = os.environ.get("KINK_AUDIT_BASE_URL", "http://127.0.0.1:8011") -ARTIFACT_DIR = Path("/tmp/kink_ui_audit") +BASE_URL = os.environ.get("KINK_AUDIT_BASE_URL", "http://127.0.0.1:8011").rstrip("/") +ARTIFACT_DIR = Path(os.environ.get("KINK_AUDIT_ARTIFACT_DIR", "/tmp/kink_ui_audit")) ARTIFACT_DIR.mkdir(parents=True, exist_ok=True) -BONDAGE_ID = "fetlife_10" -SPANKING_ID = "fetlife_96" -ORAL_ID = "fetlife_71" +STARTER_TARGET = 12 +TABS = ("discover", "scenarios", "plays", "together", "settings") -@dataclass +@dataclass(frozen=True) class Credentials: user_id: str private_token: str @@ -27,289 +27,175 @@ def auth_headers(creds: Credentials) -> dict[str, str]: return {"x-private-token": creds.private_token} -def post_json(path: str, payload: dict, headers: dict[str, str] | None = None, timeout: int = 20) -> dict: - request_headers = {"Content-Type": "application/json"} - if headers: - request_headers.update(headers) - request = urllib.request.Request( - f"{BASE_URL}{path}", - data=json.dumps(payload).encode("utf-8"), - headers=request_headers, - method="POST", - ) +def json_request( + path: str, + *, + method: str = "GET", + payload: dict | None = None, + headers: dict[str, str] | None = None, + timeout: int = 30, +) -> dict: + request_headers = dict(headers or {}) + data = None + if payload is not None: + request_headers["Content-Type"] = "application/json" + data = json.dumps(payload).encode("utf-8") + request = urllib.request.Request(f"{BASE_URL}{path}", data=data, headers=request_headers, method=method) with urllib.request.urlopen(request, timeout=timeout) as response: - return json.loads(response.read().decode("utf-8")) + body = response.read().decode("utf-8") + return json.loads(body) if body else {} def create_user() -> Credentials: - payload = post_json("/users", {}) + payload = json_request("/users", method="POST", payload={}) return Credentials(user_id=payload["id"], private_token=payload["private_token"]) -def save_play(creds: Credentials, kink_id: str, interest_state: str, directions: list[str]) -> dict: - return post_json( - f"/users/{creds.user_id}/plays", - {"kink_id": kink_id, "interest_state": interest_state, "directions": directions}, +def save_play(creds: Credentials, kink_id: str, interest_state: str = "like") -> None: + json_request( + f"/users/{urllib.parse.quote(creds.user_id)}/plays", + method="POST", + payload={"kink_id": kink_id, "interest_state": interest_state, "directions": ["together"]}, headers=auth_headers(creds), ) -def link_partner(creds: Credentials, partner_id: str) -> dict: - return post_json( - f"/users/{creds.user_id}/partners", - {"partner_id": partner_id}, +def send_partner_request(creds: Credentials, partner_id: str) -> None: + json_request( + f"/users/{urllib.parse.quote(creds.user_id)}/partners", + method="POST", + payload={"partner_id": partner_id}, headers=auth_headers(creds), ) -def create_partner_group(creds: Credentials, name: str, member_ids: list[str]) -> dict: - return post_json( - f"/users/{creds.user_id}/partner-groups", - {"name": name, "member_ids": member_ids}, +def accept_partner_request(creds: Credentials, from_user_id: str) -> None: + json_request( + f"/users/{urllib.parse.quote(creds.user_id)}/partner-requests/accept", + method="POST", + payload={"from_user_id": from_user_id}, headers=auth_headers(creds), ) -def seed_audit_users() -> tuple[Credentials, Credentials, str]: +def seed_audit_couple() -> tuple[Credentials, Credentials]: owner = create_user() partner = create_user() - save_play(owner, BONDAGE_ID, "love", ["to_me"]) - save_play(partner, BONDAGE_ID, "love", ["by_me"]) - save_play(owner, SPANKING_ID, "like", ["together"]) - save_play(partner, SPANKING_ID, "love", ["together"]) - save_play(owner, ORAL_ID, "love", ["together"]) - save_play(partner, ORAL_ID, "love", ["together"]) - link_partner(owner, partner.user_id) - group = create_partner_group(owner, "UI Audit Duo", [partner.user_id]) - return owner, partner, group["id"] - - -def capture_locator(locator, label: str, path: Path) -> dict: - if locator.count() == 0: - return {"label": label, "present": False} - target = locator.first - box = target.bounding_box() - if not box: - return {"label": label, "present": True, "box": None} - target.screenshot(path=str(path)) - return { - "label": label, - "present": True, - "box": { - "x": round(box["x"]), - "y": round(box["y"]), - "width": round(box["width"]), - "height": round(box["height"]), - }, - "path": str(path), - } - - -def capture_layout_state(page: Page) -> dict: - return page.evaluate( - """() => ({ - viewport: { width: window.innerWidth, height: window.innerHeight }, - bodyScrollHeight: document.body.scrollHeight, - headings: [...document.querySelectorAll('h1, h2, h3')].map((el) => ({ - text: (el.textContent || '').trim(), - top: Math.round(el.getBoundingClientRect().top), - })).slice(0, 20), - statusText: [...document.querySelectorAll('.status')].map((el) => (el.textContent || '').trim()).filter(Boolean), - authText: document.querySelector('.auth-strip')?.innerText || '', - selectedDetailTitle: document.querySelector('.detail-title')?.textContent || '' - })""" + recs = json_request( + f"/users/{urllib.parse.quote(owner.user_id)}/recommendations?limit={STARTER_TARGET}", + headers=auth_headers(owner), + timeout=60, + ).get("items", []) + if len(recs) < STARTER_TARGET: + raise AssertionError(f"expected at least {STARTER_TARGET} recommendations, got {len(recs)}") + for idx, item in enumerate(recs[:STARTER_TARGET]): + kink_id = item["kink"]["id"] + save_play(owner, kink_id, "love" if idx < 2 else "like") + save_play(partner, kink_id, "love" if idx < 2 else "like") + send_partner_request(owner, partner.user_id) + accept_partner_request(partner, owner.user_id) + return owner, partner + + +def install_auth(page: Page, creds: Credentials) -> None: + page.add_init_script( + f"localStorage.setItem('kink_auth', JSON.stringify({json.dumps({'userId': creds.user_id, 'token': creds.private_token})}));", ) -def round_box(box: dict | None) -> dict | None: - if not box: - return None - return { - "x": round(box["x"]), - "y": round(box["y"]), - "width": round(box["width"]), - "height": round(box["height"]), - } +def click_tab(page: Page, key: str) -> None: + desktop = page.get_by_test_id(f"nav-tab-{key}") + mobile = page.get_by_test_id(f"mobile-tab-{key}") + if desktop.count() and desktop.is_visible(): + desktop.click() + else: + mobile.click() + expect(page.get_by_test_id(f"{key}-view")).to_be_visible(timeout=30_000) -def first_box(locator) -> dict | None: - if locator.count() == 0: - return None - return round_box(locator.first.bounding_box()) - - -def assert_close(label: str, left: int, right: int, tolerance: int = 12) -> None: - if abs(left - right) > tolerance: - raise AssertionError(f"{label} misaligned by {abs(left - right)}px ({left} vs {right})") - - -def capture_alignment_metrics(page: Page) -> dict: - add_panel = page.locator('.panel').filter(has=page.get_by_role('heading', name='Add Plays')).first - for_you_panel = page.locator('.panel').filter(has=page.get_by_role('heading', name='For You', exact=True)).first - shared_panel = page.locator('.panel').filter(has=page.get_by_role('heading', name='Shared Play List')).first - detail_pane = page.locator('.side-stack').first - board_columns = page.locator('.board-grid .board-column') - - add_box = first_box(add_panel) - for_you_box = first_box(for_you_panel) - shared_box = first_box(shared_panel) - detail_box = first_box(detail_pane) - board_boxes = [round_box(board_columns.nth(i).bounding_box()) for i in range(board_columns.count())] - board_boxes = [box for box in board_boxes if box] - - add_heading = first_box(add_panel.locator('h2, h3').first) if add_panel.count() else None - for_you_heading = first_box(for_you_panel.locator('h2, h3').first) if for_you_panel.count() else None - shared_heading = first_box(shared_panel.locator('h2, h3').first) if shared_panel.count() else None - - return { - "panels": { - "add": add_box, - "for_you": for_you_box, - "shared": shared_box, - "detail": detail_box, - }, - "headings": { - "add": add_heading, - "for_you": for_you_heading, - "shared": shared_heading, - }, - "board_columns": board_boxes, - } +def layout_metrics(page: Page) -> dict: + return page.evaluate( + """() => { + const width = window.innerWidth; + const offenders = [...document.querySelectorAll('body *')] + .map((el) => ({ el, rect: el.getBoundingClientRect() })) + .filter(({ rect }) => rect.width > 1 && rect.height > 1 && (rect.left < -2 || rect.right > width + 2)) + .slice(0, 20) + .map(({ el, rect }) => ({ + tag: el.tagName.toLowerCase(), + testId: el.getAttribute('data-testid') || '', + className: String(el.className || '').slice(0, 80), + left: Math.round(rect.left), + right: Math.round(rect.right), + width: Math.round(rect.width) + })); + const emptyButtons = [...document.querySelectorAll('button')] + .filter((button) => !(button.textContent || '').trim() && !button.getAttribute('aria-label')) + .slice(0, 20) + .map((button) => button.getAttribute('data-testid') || button.className || button.outerHTML.slice(0, 80)); + return { + viewport: { width: window.innerWidth, height: window.innerHeight }, + scrollWidth: document.documentElement.scrollWidth, + scrollHeight: document.documentElement.scrollHeight, + activeViews: [...document.querySelectorAll('[data-testid$="-view"]')] + .filter((el) => el.getBoundingClientRect().height > 0) + .map((el) => el.getAttribute('data-testid')), + offenders, + emptyButtons + }; + }""", + ) -def assert_shell_alignment(metrics: dict, *, mobile: bool) -> None: - panels = metrics["panels"] - headings = metrics["headings"] - board_columns = metrics["board_columns"] - - required_panels = [panels["add"], panels["for_you"], panels["shared"]] - if any(panel is None for panel in required_panels): - raise AssertionError("missing panel box for alignment audit") - if any(heading is None for heading in [headings["add"], headings["for_you"], headings["shared"]]): - raise AssertionError("missing heading box for alignment audit") - - assert_close("add vs for_you left edge", panels["add"]["x"], panels["for_you"]["x"]) - assert_close("for_you vs shared left edge", panels["for_you"]["x"], panels["shared"]["x"]) - assert_close("add vs for_you heading inset", headings["add"]["x"] - panels["add"]["x"], headings["for_you"]["x"] - panels["for_you"]["x"]) - assert_close("for_you vs shared heading inset", headings["for_you"]["x"] - panels["for_you"]["x"], headings["shared"]["x"] - panels["shared"]["x"]) - - if not mobile: - if len(board_columns) != 3: - raise AssertionError(f"expected 3 board columns, got {len(board_columns)}") - widths = [box["width"] for box in board_columns] - heights = [box["height"] for box in board_columns] - left_edges = [box["x"] for box in board_columns] - if max(widths) - min(widths) > 8: - raise AssertionError(f"board column widths diverge too much: {widths}") - if max(heights) - min(heights) > 20: - raise AssertionError(f"board column heights diverge too much: {heights}") - gaps = [left_edges[i + 1] - left_edges[i] for i in range(len(left_edges) - 1)] - if gaps and max(gaps) - min(gaps) > 12: - raise AssertionError(f"board column spacing inconsistent: {left_edges}") - - -def login_storage_script(creds: Credentials) -> str: - payload = json.dumps({"userId": creds.user_id, "token": creds.private_token}) - return f"localStorage.setItem('kink_auth', JSON.stringify({payload}));" - - -def run_scenario(name: str, viewport: tuple[int, int], init_script: str, action) -> dict: - console_errors: list[str] = [] - start = time.perf_counter() - with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - page = browser.new_page(viewport={"width": viewport[0], "height": viewport[1]}) - page.add_init_script(init_script) - page.on("console", lambda msg: console_errors.append(f"{msg.type}: {msg.text}") if msg.type == "error" else None) - page.goto(BASE_URL, wait_until="domcontentloaded") - page.wait_for_timeout(1200) - result = { - "name": name, - "viewport": {"width": viewport[0], "height": viewport[1]}, - "console_errors": console_errors, - "checkpoints": [], - } - action(page, result) - full_path = ARTIFACT_DIR / f"{name}.png" - page.screenshot(path=str(full_path), full_page=True) - result["full_screenshot"] = str(full_path) - result["elapsed_ms"] = round((time.perf_counter() - start) * 1000, 1) +def assert_layout(metrics: dict) -> None: + viewport_width = metrics["viewport"]["width"] + if metrics["scrollWidth"] > viewport_width + 2: + raise AssertionError(f"horizontal overflow: scrollWidth={metrics['scrollWidth']} viewport={viewport_width}") + if metrics["offenders"]: + raise AssertionError(f"offscreen visible elements: {metrics['offenders']}") + if metrics["emptyButtons"]: + raise AssertionError(f"buttons without text or aria-label: {metrics['emptyButtons']}") + + +def audit_viewport(creds: Credentials, label: str, viewport: dict[str, int]) -> dict: + snapshots: list[dict] = [] + with sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=True, args=["--no-sandbox", "--disable-http-cache"]) + context = browser.new_context(viewport=viewport) + page = context.new_page() + install_auth(page, creds) + page.goto(BASE_URL, wait_until="domcontentloaded", timeout=120_000) + expect(page.get_by_test_id("discover-view")).to_be_visible(timeout=60_000) + for tab in TABS: + if tab != "discover": + click_tab(page, tab) + metrics = layout_metrics(page) + assert_layout(metrics) + shot = ARTIFACT_DIR / f"{label}_{tab}.png" + page.screenshot(path=str(shot), full_page=True) + snapshots.append({"tab": tab, "metrics": metrics, "screenshot": str(shot)}) + context.close() browser.close() - return result - - -def action_authed_shell(page: Page, result: dict) -> None: - expect(page.locator('.auth-strip')).to_be_visible(timeout=15000) - expect(page.get_by_role('heading', name='For You', exact=True)).to_be_visible(timeout=15000) - state = capture_layout_state(page) - alignment = capture_alignment_metrics(page) - assert_shell_alignment(alignment, mobile=page.viewport_size['width'] <= 480) - result['checkpoints'].append({"label": "shell", "state": state, "alignment": alignment}) - result['clips'] = [ - capture_locator(page.locator('.panel').filter(has=page.get_by_role('heading', name='For You', exact=True)), 'for_you_panel', ARTIFACT_DIR / f"{result['name']}_for_you.png"), - capture_locator(page.locator('.board-grid'), 'play_list_board', ARTIFACT_DIR / f"{result['name']}_play_list.png"), - capture_locator(page.locator('.panel').filter(has=page.get_by_role('heading', name='Shared Play List')), 'shared_panel', ARTIFACT_DIR / f"{result['name']}_shared_panel.png"), - ] - - -def action_detail_open(page: Page, result: dict) -> None: - expect(page.locator('.auth-strip')).to_be_visible(timeout=15000) - page.get_by_placeholder('Search plays').fill('bondage') - chip = page.locator('.panel').filter(has=page.get_by_text('Add Plays')).locator('.play-chip').filter(has=page.get_by_role('button', name='Bondage')).first - expect(chip).to_be_visible(timeout=15000) - chip.get_by_role('button', name='Bondage').click() - expect(page.locator('.detail-title')).to_have_text('Bondage', timeout=15000) - result['checkpoints'].append({"label": "detail_open", "state": capture_layout_state(page), "alignment": capture_alignment_metrics(page)}) - result['clips'] = [ - capture_locator(page.locator('.side-stack'), 'detail_pane', ARTIFACT_DIR / f"{result['name']}_detail.png"), - capture_locator(page.locator('.panel').filter(has=page.get_by_role('heading', name='For You', exact=True)), 'for_you_panel', ARTIFACT_DIR / f"{result['name']}_for_you.png"), - ] - - -def action_shared_group(page: Page, result: dict) -> None: - expect(page.locator('.auth-strip')).to_be_visible(timeout=15000) - shared_panel = page.locator('.panel').filter(has=page.get_by_role('heading', name='Shared Play List')) - shared_panel.get_by_role('button', name='UI Audit Duo (2)').click() - expect(page.locator('.shared-grid')).to_be_visible(timeout=15000) - result['checkpoints'].append({"label": "shared_group", "state": capture_layout_state(page)}) - result['clips'] = [ - capture_locator(page.locator('.shared-grid'), 'shared_grid', ARTIFACT_DIR / f"{result['name']}_shared.png"), - capture_locator(shared_panel.locator('.chips').first, 'group_picker', ARTIFACT_DIR / f"{result['name']}_group_picker.png"), - ] + return {"label": label, "viewport": viewport, "snapshots": snapshots} def main() -> int: - owner, _partner, _group_id = seed_audit_users() - scenarios = [ - run_scenario('desktop_authed_shell', (1440, 1000), login_storage_script(owner), action_authed_shell), - run_scenario('desktop_detail_open', (1440, 1000), login_storage_script(owner), action_detail_open), - run_scenario('desktop_shared_group', (1440, 1000), login_storage_script(owner), action_shared_group), - run_scenario('mobile_authed_shell', (390, 844), login_storage_script(owner), action_authed_shell), - run_scenario('mobile_detail_open', (390, 844), login_storage_script(owner), action_detail_open), - ] - out = { - 'generated_at': time.time(), - 'base_url': BASE_URL, - 'artifact_dir': str(ARTIFACT_DIR), - 'scenarios': scenarios, - } - out_path = ARTIFACT_DIR / 'audit.json' - out_path.write_text(json.dumps(out, indent=2)) - print(json.dumps({ - 'artifact_dir': str(ARTIFACT_DIR), - 'audit_json': str(out_path), - 'scenarios': [ - { - 'name': item['name'], - 'elapsed_ms': item['elapsed_ms'], - 'console_errors': item['console_errors'], - 'clip_count': len(item.get('clips', [])), - } - for item in scenarios + owner, partner = seed_audit_couple() + report = { + "generated_at": time.time(), + "base_url": BASE_URL, + "owner_user_id": owner.user_id, + "partner_user_id": partner.user_id, + "viewports": [ + audit_viewport(owner, "desktop", {"width": 1440, "height": 1000}), + audit_viewport(owner, "mobile", {"width": 390, "height": 844}), ], - }, indent=2)) + } + out_path = ARTIFACT_DIR / "ui_visual_audit.json" + out_path.write_text(json.dumps(report, indent=2), encoding="utf-8") + print(json.dumps({"audit_json": str(out_path), "viewports": len(report["viewports"])}, indent=2)) return 0 -if __name__ == '__main__': +if __name__ == "__main__": raise SystemExit(main()) diff --git a/scripts/verify_hf_stack.py b/scripts/verify_hf_stack.py index 115e5142e9e4b1a8abfe8b8b379fdfffb0782f0f..372163d2236accdf7a19d53346843468f28e2c7e 100644 --- a/scripts/verify_hf_stack.py +++ b/scripts/verify_hf_stack.py @@ -54,38 +54,24 @@ def _get_text(url: str, headers: dict[str, str] | None = None) -> str: def _assert_live_frontend_recs_error_shaping_once(root: str) -> tuple[str, str]: - """Single attempt: follow index → app.js → api.js / discover.js; return (api_path, disc_path) on success.""" + """Single attempt: follow index to the built Vite JS bundle and assert error shaping is deployed.""" base = root.rstrip("/") html = _get_text(f"{base}/") - m = re.search(r"""src=["'](/frontend/app\.js\?v=\d+)["']""", html) + m = re.search(r"""src=["'](/frontend/assets/[^"']+\.js)["']""", html) if not m: - raise RuntimeError("GET /: expected