diff --git a/.github/docs/Brand-Style.md b/.github/docs/Brand-Style.md new file mode 100644 index 00000000..7b4075bb --- /dev/null +++ b/.github/docs/Brand-Style.md @@ -0,0 +1,226 @@ +# 4.2.1 Brand Style + +The brand style defines the visual identity of Vantage Point and ensures a consistent, professional appearance across all interfaces. This document reflects what is **implemented in the codebase** today (`frontend/src`), not aspirational guidelines. + +--- + +## Color Palette + +Vantage Point uses two complementary colour systems: + +1. **Design tokens** — CSS variables in `frontend/src/styles/theme.css`, consumed by shadcn/ui components via Tailwind (`bg-primary`, `text-muted-foreground`, etc.). +2. **Figma / screen-specific hex values** — hard-coded on auth screens, the dashboard shell, and match views (primarily Inter-based layouts). + +### Semantic tokens (light mode) + +Defined in `:root` in `theme.css`: + +| Role | Value | Usage | +|------|-------|--------| +| Background | `#ffffff` | Page and card surfaces | +| Foreground | `oklch(0.145 0 0)` | Primary text | +| Primary | `#030213` | Primary actions, emphasis | +| Primary foreground | `oklch(1 0 0)` | Text on primary | +| Secondary | `oklch(0.95 0.0058 264.53)` | Secondary surfaces | +| Muted | `#ececf0` / muted-foreground `#717182` | Subtle UI, secondary text | +| Accent | `#e9ebef` | Hover / highlight surfaces | +| Destructive | `#d4183d` | Errors, destructive actions | +| Border | `rgba(0, 0, 0, 0.1)` | Dividers and outlines | +| Input background | `#f3f3f5` | Form fields (shadcn `Input`) | +| Ring (focus) | `oklch(0.708 0 0)` | Focus rings on interactive controls | +| Chart 1–5 | OKLCH palette | Radar chart and data viz | + +A **dark mode** palette is also defined under `.dark` in the same file (used by shadcn primitives). The main app screens (auth, dashboard) currently render in **light mode** with explicit hex colours rather than toggling `.dark`. + +### Application UI colours (hex) + +Used across login, register, dashboard, profile, and matches: + +| Role | HEX | Where used | +|------|-----|------------| +| Body text | `#1e1e1e` | Labels, match rows, profile copy | +| Strong emphasis | `#0b0b0b` | Auth links (e.g. “Sign up”) | +| Secondary text | `#525252`, `#757575` | Section labels, metadata | +| Placeholder | `#b3b3b3` | Auth input placeholders | +| Borders | `#d9d9d9`, `#eee` | Inputs, match list dividers | +| Primary button | `#2c2c2c` (hover `#3c3c3c`) | Sign in / register CTAs | +| Button label | `#f5f5f5` | Text on dark CTAs | +| Hover surface | `#f5f5f5` | Profile edit button hover | +| Victory | `#1e7e34` | Win outcome, match result | +| Defeat / error | `#c44a4a` | Loss outcome, API errors | +| Blue side (LoL) | `#4a7fd4` | Match detail team 100 | +| Red side background | `#fce8e8` | Defeat team chip in match detail | +| Avatar fallback | `#404040` | Initials when no profile image | +| Sidebar panel | `rgba(117, 117, 117, 0.12)` | Dashboard nav background | +| Landing backdrop | `#000000` | Full-screen landing (`LandingPage`) | +| Loading brand text | `#0f172a` | Loading screen wordmark | + +**Loading animation accents** (`theme.css` utilities): `#1d4ed8`, `#0f766e`, `#6d28d9`, `#0e7490` cycle in `animate-vantage-pulse`. + +### Layout tokens + +| Token | Value | Purpose | +|-------|-------|---------| +| `--vp-layout-max` | `1512px` | Max dashboard artboard width | +| `--vp-dashboard-header` | `94px` | Fixed header height | +| `--vp-sidebar-width` | `220px` | Sidebar width | +| `--radius` | `0.625rem` (10px) | Default border radius; sm/md/lg/xl derived | + +### Accessibility (colour) + +- Auth inputs use `#1e1e1e` on white with `#d9d9d9` borders; focus darkens border to `#2c2c2c`. +- Victory (`#1e7e34`) and defeat (`#c44a4a`) are distinct for outcome scanning. +- shadcn controls use `focus-visible:ring-[3px]` with `--ring` for keyboard focus visibility. + +--- + +## Typography + +Fonts are loaded from Google Fonts in `frontend/src/styles/fonts.css` and applied per screen. + +### Font families + +| Family | Role | Implementation | +|--------|------|----------------| +| **Sarina** | Brand wordmark “Vantage Point” | `font-sarina` token in `theme.css`; `font-sarina` class on login, register, loading, Riot ID screens | +| **Inter** | Primary UI typeface | `font-['Inter:Regular',sans-serif]` (body), `Inter:Semi_Bold` (headings, labels, buttons) | +| **Geist** | Featured-game card badges | `font-['Geist:Medium',sans-serif]` at 14px on `FeaturedGameCard` | +| **Sora** | Loaded globally | Available via `fonts.css`; not heavily used in current screens | + +### Sizes and weights + +| Element | Size | Weight | Line height | +|---------|------|--------|-------------| +| Root / `html` | `16px` (`--font-size`) | — | — | +| Brand title (auth) | `clamp(20px, 2.5vw, 32px)` | Sarina regular | `leading-normal` | +| Brand title (loading) | `clamp(22px, 4.2vw, 40px)` | Sarina regular | `leading-tight` | +| Body / inputs | `16px` | Inter 400 | `1.4` or `leading-none` | +| Dashboard nav | `14px` | Inter 400 | `1.4` | +| Section headings (profile) | `14px` uppercase | Inter 600 | — | +| Profile display name | `28px` | Inter 600 | `leading-tight` | +| Match stat labels | `11px` uppercase | Inter 500 | `tracking-wide` | +| shadcn `Button` / `Input` | `text-sm` / `text-base` | medium (500) per `theme.css` base layer | `1.5` | + +Base heading scale in `theme.css` (`h1`–`h4`) uses `--font-weight-medium` (500) with Tailwind text scale variables; Figma screens override with explicit Inter sizes. + +--- + +## Logo and Iconography + +### Logo + +- **Primary mark:** WebP asset `798001aef0b2686ac929f8c349135d3326ab65bb.webp` (imported in `Login.tsx`, `Register.tsx`, `RiotId.tsx`, `LoadingPage.tsx`, `Group14.tsx`). +- **Sizing:** + - Auth / Riot ID: `clamp(120px, 18vw, 200px)` square, with Sarina wordmark below. + - Dashboard header: `97×99px` absolute top-left (`Group14.tsx`). + - Loading: responsive clamp, paired with animated wordmark. +- **Wordmark:** “Vantage Point” in Sarina, black on light auth screens, `#0f172a` on loading. + +### Favicon + +- `index.html` references `/favicon.svg` as the site icon; page title is **Vantage Point**. + +### Icons + +| Source | Usage | +|--------|--------| +| **Lucide React** | UI chrome (chevrons, checks, carousel controls, achievement mapping in `achievementIcons.ts`) | +| **Inline SVG** (`svg-*.ts` in imports) | Dashboard sidebar toggle, decorative vectors | +| **Figma-exported PNG/WebP** | Profile featured game (`league-wild-rift-*.png`), `icon-lightning-bolt.png`, `icon-time-machine.png` | +| **Social providers** | Google, Apple, Riot Games WebP buttons on auth (60×60px, `alt` describes action) | +| **Riot Data Dragon** | Champion and item images via `ddragon.ts` | + +### Placement rules (as implemented) + +- Logo is **centred above** auth forms; on dashboard it is **fixed top-left** with account menu top-right. +- Decorative carousel/background images use `alt=""` (presentational); interactive controls and social buttons have descriptive `alt` text. +- Achievement icons sit in a grid with optional count badges (`ProfileView.tsx`). + +--- + +## Design Principles + +Patterns observed in the implementation: + +| Principle | How it appears in Vantage Point | +|-----------|----------------------------------| +| **Consistency** | Shared auth input class (`authInputClassName`), shared logo asset, repeated dashboard layout constants (`dashboardLayout.ts`, `Group14.tsx`) | +| **Simplicity** | White dashboard canvas, minimal sidebar (Matches / Analysis / Settings labels), clear win/loss colour coding | +| **Responsiveness** | `clamp()` typography and logo on auth; match list grid adds columns at `sm:` breakpoint; sidebar collapses with animated width/position | +| **Accessibility** | Radix primitives, ARIA on navigation and match rows, focus rings on shadcn controls (see Accessibility) | +| **Brand personality** | League/Arcane full-bleed wallpapers on auth; Sarina script wordmark; subtle loading animations (`animate-vantage-pulse`, `animate-vantage-breathe`) | +| **Design–dev alignment** | Figma imports under `frontend/src/landing/imports/`; profile assets documented as Figma exports in `assets/profile/index.ts` | + +--- + +## UI Component Styling + +### Auth (login / register / Riot ID) + +- **Inputs:** 8px radius, 16px horizontal padding, `#d9d9d9` border, transparent background, Inter 16px (`authInputClassName` in `Login.tsx` / `Register.tsx`). +- **Primary CTA:** Full-width, 58px height, `#2c2c2c` background, `#f5f5f5` label, disabled at 60% opacity. +- **Social login:** Three 60×60 image buttons in a centred row. +- **Background carousel:** Dot tablist (`role="tablist"`), 8px dots, active `bg-black` / inactive 30% opacity. + +### Dashboard shell (`Group14.tsx`) + +- **Frame:** White full-viewport background, min-width fluid. +- **Sidebar:** 220px wide, 400px tall panel, 15px radius, grey translucent fill; nav items 47px tall, 10px radius white pills for active state. +- **Header:** 94px (`--vp-dashboard-header`); content offset when sidebar open (`DASHBOARD_CONTENT_LEFT_OPEN` = 299px). +- **Toggle:** 24px icon button with `aria-expanded`, `aria-controls="dashboard-sidebar"`. + +### shadcn/ui library (`frontend/src/landing/app/components/ui/`) + +Shared primitives for newer or composable UI: + +| Component | Styling summary | +|-----------|-----------------| +| **Button** | Variants: default, destructive, outline, secondary, ghost, link; sizes sm/default/lg/icon; `rounded-md`, `focus-visible:ring-[3px]` | +| **Input / Textarea / Select** | `h-9`, `rounded-md`, `bg-input-background`, ring on focus, `aria-invalid` destructive styling | +| **Dialog** | Overlay `bg-black/50`, animated open/close (Radix) | +| **Card, Badge, Avatar, Chart** | Token-driven colours; chart uses theme chart-1–5 | +| **Dropdown / Navigation menu** | Accent hover states, keyboard focus rings | + +Auth screens use **custom Figma buttons**; dashboard match/profile views mix **custom layout** with selective shadcn pieces (e.g. `Avatar`, `Button` in profile editor). + +### Match list and detail + +- **List row:** CSS grid, bordered with `#eee`, tabular numerals for stats. +- **Outcome:** Green `#1e7e34` / red `#c44a4a` semibold labels. +- **Detail:** Team colours blue/red; result banner uses same win/loss palette. + +--- + +## Accessibility + +Implemented practices (targeting WCAG-oriented behaviour): + +| Area | Implementation | +|------|----------------| +| **Language** | `` in `index.html` | +| **Keyboard focus** | shadcn components: `focus-visible:border-ring`, `focus-visible:ring-[3px]`; auth inputs: visible border change on focus | +| **Screen readers** | `aria-label` on dashboard sidebar toggle, nav (“Matches”), profile sections (“Performance radar”, “Achievements”, etc.); `aria-current="page"` for active nav; match rows expose `matchRowAriaLabel()`; carousel previous/next use `sr-only` text; login errors use `role="alert"` | +| **Semantic structure** | Match list uses `role="row"` / `role="columnheader"`; carousel uses `role="region"` and `aria-roledescription` | +| **Forms** | Labels associated with fields on auth screens; `aria-invalid` styling on shadcn inputs when invalid | +| **Motion** | CSS animations on loading screen; no `prefers-reduced-motion` override yet | +| **Colour contrast** | Light dashboards rely on dark grey text on white; known gap: some decorative images use empty `alt` | + +### Gaps / follow-ups + +- Dark-mode tokens exist but are not wired as the default experience for main routes. +- Not all images have descriptive `alt` (logo and wallpapers are decorative). +- No documented WCAG conformance level or automated a11y test suite in repo. + +--- + +## Source reference + +| Topic | Primary files | +|-------|----------------| +| Theme tokens | `frontend/src/styles/theme.css` | +| Fonts | `frontend/src/styles/fonts.css`, `frontend/src/styles/index.css` | +| shadcn components | `frontend/src/landing/app/components/ui/` | +| Auth branding | `frontend/src/landing/imports/Login/Login.tsx`, `Register/Register.tsx` | +| Dashboard shell | `frontend/src/landing/imports/Group14/Group14.tsx` | +| Layout constants | `frontend/src/landing/app/lib/dashboardLayout.ts` | +| Profile assets | `frontend/src/landing/app/assets/profile/` | diff --git a/.github/docs/Dev-Quickstart.md b/.github/docs/Dev-Quickstart.md new file mode 100644 index 00000000..ca56fd9b --- /dev/null +++ b/.github/docs/Dev-Quickstart.md @@ -0,0 +1,146 @@ +# Dev Quickstart — Seed, Backend, Login + +Short runbook for getting a local dev environment with seeded test data and signing in as the demo user. + +For full infrastructure setup (Dev Container, database schema, troubleshooting), see [Setup.md](./Setup.md). + +--- + +## Prerequisites + +- Project opened in the **Dev Container** (recommended), or local Postgres with `DATABASE_URL` pointing at `localhost:5432` +- **`backend/.env`** — copy from `backend/.env.example` +- **`frontend/.env`** — copy from `frontend/.env.example` + +### Required `backend/.env` values + +```env +DATABASE_URL=postgresql+asyncpg://riot_user:riot_password@db:5432/riot_db +JWT_SECRET=change-me-to-a-long-random-string +SEED_DEV_PASSWORD=your-team-dev-password +RIOT_API_KEY=your_key_here +``` + +Inside the dev container, the database host must be **`db`**, not `localhost`. + +### Required `frontend/.env` values + +```env +VITE_API_URL=http://localhost:8000 +``` + +--- + +## Step 1 — Seed the database + +From inside the dev container: + +```bash +cd /workspaces/backend +python -m app.database.seed +``` + +Outside the container (with a local venv and Postgres on port 5432): + +```bash +cd backend +python -m app.database.seed +``` + +**Success:** output ends with `--- Seed complete ---` and a line like: + +```text +Dev login: testuser1@vantagepoint.dev (password from SEED_DEV_PASSWORD) +``` + +**Important:** + +- `SEED_DEV_PASSWORD` must be set in `backend/.env` before running seed. +- Seed **drops and recreates all tables**. Re-run it whenever you need a clean dev dataset or after schema changes. + +--- + +## Step 2 — Start the backend + +```bash +cd /workspaces/backend +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +**Verify:** open [http://localhost:8000](http://localhost:8000). Expected response: + +```json +{"message": "Vantage Point API running", "db_status": "Ready"} +``` + +--- + +## Step 3 — Start the frontend + +In a **second terminal**: + +```bash +cd /workspaces/frontend +npm run dev +``` + +Open the URL Vite prints (usually [http://localhost:5173](http://localhost:5173)). + +--- + +## Step 4 — Log in + +| Field | Value | +|--------|--------| +| **Email** | `testuser1@vantagepoint.dev` | +| **Password** | The value of `SEED_DEV_PASSWORD` in `backend/.env` | + +Use **testuser1** for the full demo dataset (matches, profile, achievements). +`testuser2@vantagepoint.dev` uses the same password but has a different, mostly empty game account. + +--- + +## What seed provides + +| Item | Details | +|------|---------| +| Dev users | `testuser1@vantagepoint.dev`, `testuser2@vantagepoint.dev` | +| Demo account (testuser1) | Riot ID **You#EUW**, PUUID `seed-viewer-puuid` | +| Match history | 8 seeded games with scoreboards | +| Profile extras | Achievements, featured-game banners, `profile_matches_sampled=20` | +| Champions | Static champion catalog populated | + +Newly registered accounts without seeded rows will have empty match history until they link a Riot ID and data is ingested. + +--- + +## Copy-paste checklist + +```text +1. backend/.env + frontend/.env (from .env.example) +2. cd backend && python -m app.database.seed +3. cd backend && uvicorn app.main:app --reload +4. cd frontend && npm run dev +5. Login: testuser1@vantagepoint.dev / +``` + +--- + +## Troubleshooting + +| Problem | What to do | +|---------|------------| +| Seed fails: `SEED_DEV_PASSWORD` | Add `SEED_DEV_PASSWORD=...` to `backend/.env`. | +| Seed / backend cannot connect to DB | Run inside the dev container; use host `db` in `DATABASE_URL`. | +| Login fails after re-seed | Password must match the **current** `SEED_DEV_PASSWORD`, not an old value. | +| Empty matches after login | Sign in as **testuser1**, not a new account without seeded data. | +| Frontend cannot reach API | Set `VITE_API_URL=http://localhost:8000` in `frontend/.env`. | +| Backend run outside container | Change `DATABASE_URL` host from `db` to `localhost`. | + +--- + +## Related docs + +- [Setup.md](./Setup.md) — Dev Container, database schema, verification +- [Backend-Development-Guide.md](./Backend-Development-Guide.md) — API and backend development +- [Frontend-Development-Guide.md](./Frontend-Development-Guide.md) — UI development diff --git a/.github/docs/Setup.md b/.github/docs/Setup.md index 34f13e60..8a7d722f 100644 --- a/.github/docs/Setup.md +++ b/.github/docs/Setup.md @@ -29,6 +29,16 @@ Have a `backend/.env` before running the backend. This file is gitignored so not ```env DATABASE_URL=postgresql+asyncpg://riot_user:riot_password@db:5432/riot_db RIOT_API_KEY=your_key_here +JWT_SECRET=change-me-to-a-long-random-string +SEED_DEV_PASSWORD=choose-a-dev-only-password +JWT_ACCESS_EXPIRE_MINUTES=30 +JWT_REFRESH_EXPIRE_DAYS=7 +``` + +For the frontend, copy `frontend/.env.example` to `frontend/.env` and set: + +```env +VITE_API_URL=http://localhost:8000 ``` ### Dev Container environment variables (`.devcontainer/.env`) @@ -62,10 +72,13 @@ Note the host is `db`, not `localhost` — that's the service name from `docker- ### Schema and ER Diagram | Table | Primary Key | Notes | |----------------------|-----------------------|-------| -| `users` | `cognito_sub` (str) | Cognito `sub` claim; stores email, no password | -| `game_accounts` | `puuid` (str) | Riot's global player ID, stable across name changes | +| `users` | `id` (UUID str) | Email/password auth; `display_name` for profile header | +| `game_accounts` | `puuid` (str) | Riot player ID; `profile_matches_sampled` drives "Last N matches" label | | `user_game_accounts` | `id` (int, auto) | Join table allowing a user to link multiple game accounts | -| `matches` | `match_id` (str) | Riot match ID, e.g. `EUW1_1234567890` | +| `achievement_definitions` | `id` (str) | Achievement catalog (label, description, source field) | +| `user_achievements` | `id` (int, auto) | Per-PUUID achievement counts for profile | +| `user_featured_games`| `id` (int, auto) | Featured-game banner slides per PUUID | +| `matches` | `match_id` (str) | Riot match ID; includes `game_creation`, `map_id`, `played_on`, `detail_json` (scoreboard) | | `champions` | `champion_id` (int) | Matches Riot's own champion ID | | `participants` | `internal_id` (int, auto) | Links a game account, match, and champion; stores per‑match stats | @@ -126,11 +139,36 @@ Expected output: The database starts empty. To populate the `champions` table with real Riot IDs and static stats from the dataset, run the seed script **manually**: ```bash -cd /workspaces/backend +cd backend +python3 -m venv .venv # first time only +.venv/bin/pip install -r requirements.txt # first time only +.venv/bin/python -m app.database.seed +``` +### Auth API (email/password + JWT) + +After changing the `users` schema, reset the database (seed drops and recreates tables): + +```bash +cd /workspaces/backend python -m app.database.seed ``` +| Endpoint | Method | Auth | Purpose | +|----------|--------|------|---------| +| `/api/v1/auth/register` | POST | No | Create account; returns access + refresh tokens | +| `/api/v1/auth/login` | POST | No | Sign in | +| `/api/v1/auth/refresh` | POST | No | Refresh access token | +| `/api/v1/users/me` | GET | Bearer | Profile identity + linked Riot ID | +| `/api/v1/users/me/profile` | GET | Bearer | Profile aggregates (radar, champions, achievements) | +| `/api/v1/users/me/game-accounts` | POST | Bearer | Link Riot ID (`Name#TAG`) via Riot API | +| `/api/v1/matches` | GET | Bearer | Match history for linked account | +| `/api/v1/matches/{match_id}` | GET | Bearer | Full match detail (scoreboard) | + +Seed users: `testuser1@vantagepoint.dev` / `testuser2@vantagepoint.dev` with password from `SEED_DEV_PASSWORD` in `backend/.env`. See [Dev-Quickstart.md](./Dev-Quickstart.md) for the full seed → run → login flow. + +**Seeded dev data (viewer `You#EUW`, PUUID `seed-viewer-puuid`):** 8 matches, 7 achievements, 2 featured-game banners, `profile_matches_sampled=20`. Match list rows come from `participants`; each match’s scoreboard in `matches.detail_json` is built per `match_id` and aligned with the viewer’s list stats (champion, KDA, win/loss). Radar and recent champions are computed from `participants`; achievements and banner stats are read from `user_achievements` / `user_featured_games` (not from Match-v5 yet). Sign in as `testuser1` after seeding. Real Riot-linked accounts without seeded rows get empty achievements/banners until ingestion is added. + ## Visualising the Schema ### VS Code PostgreSQL Extension (inside the container) diff --git a/.github/workflows/backend_tests.yml b/.github/workflows/backend_tests.yml index 8657531f..18b2039f 100644 --- a/.github/workflows/backend_tests.yml +++ b/.github/workflows/backend_tests.yml @@ -28,12 +28,21 @@ jobs: pip install -r requirements.txt - name: Format check with Black + env: + JWT_SECRET: test-ci-secret + DATABASE_URL: postgresql+asyncpg://riot_user:riot_password@localhost:5432/riot_db run: cd backend && black --check app - name: Lint with Ruff + env: + JWT_SECRET: test-ci-secret + DATABASE_URL: postgresql+asyncpg://riot_user:riot_password@localhost:5432/riot_db run: cd backend && ruff check app - name: Type check with MyPy + env: + JWT_SECRET: test-ci-secret + DATABASE_URL: postgresql+asyncpg://riot_user:riot_password@localhost:5432/riot_db run: cd backend && mypy app test: @@ -70,6 +79,10 @@ jobs: pip install -r requirements.txt - name: Run tests with coverage + env: + JWT_SECRET: test-ci-secret + TEST_USER_PASSWORD: test-ci-user-password + DATABASE_URL: postgresql+asyncpg://riot_user:riot_password@localhost:5432/riot_db run: cd backend && pytest --cov=app --cov-report=xml --cov-report=html --cov-report=term - name: Upload coverage artifact diff --git a/.github/workflows/frontend_tests.yml b/.github/workflows/frontend_tests.yml index b94b19ff..a0ddd0f8 100644 --- a/.github/workflows/frontend_tests.yml +++ b/.github/workflows/frontend_tests.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies working-directory: frontend - run: npm ci + run: npm ci --legacy-peer-deps - name: Check npm packages for vulnerabilities working-directory: frontend @@ -58,7 +58,7 @@ jobs: - name: Install dependencies working-directory: frontend - run: npm ci + run: npm ci --legacy-peer-deps - name: Format check with Prettier working-directory: frontend @@ -86,7 +86,7 @@ jobs: - name: Install dependencies working-directory: frontend - run: npm ci + run: npm ci --legacy-peer-deps - name: Run tests with coverage working-directory: frontend @@ -140,7 +140,7 @@ jobs: - name: Install dependencies working-directory: frontend - run: npm ci + run: npm ci --legacy-peer-deps - name: Build production bundle working-directory: frontend diff --git a/.gitignore b/.gitignore index 47bb57da..dbd4dc55 100644 --- a/.gitignore +++ b/.gitignore @@ -70,5 +70,8 @@ Thumbs.db build/ dist/ +# User uploads +backend/uploads/ + # Other .postman diff --git a/README.md b/README.md index ba62cb99..bcc2c6aa 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ To ensure a stable and collaborative development workflow, the following strateg - **[Project Board](https://github.com/orgs/COS301-SE-2026/projects/32/views/6)** - Sprint planning and task tracking - **[Setup Guide](.github/docs/SETUP.md)** - Initial project setup and dependencies +- **[Dev Quickstart](.github/docs/Dev-Quickstart.md)** - Seed database, run backend/frontend, sign in as test user - **[Backend Development Guide](.github/docs/BACKEND_DEV.md)** - Backend setup, testing, API development, code quality - **[Frontend Development Guide](.github/docs/FRONTEND_DEV.md)** - Frontend setup, components, styling, testing - **[CI/CD Documentation](.github/docs/CICD.md)** - GitHub Actions workflows, automated testing, deployment pipeline diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 00000000..1f86f67f --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,8 @@ +DATABASE_URL=postgresql+asyncpg://riot_user:riot_password@db:5432/riot_db +# Required for linking Riot IDs — create at https://developer.riotgames.com/ +RIOT_API_KEY=your_riot_api_key_here +JWT_SECRET=change-me-to-a-long-random-string +SEED_DEV_PASSWORD=choose-a-dev-only-password +TEST_USER_PASSWORD=choose-a-test-only-password +JWT_ACCESS_EXPIRE_MINUTES=30 +JWT_REFRESH_EXPIRE_DAYS=7 diff --git a/backend/app/auth/__init__.py b/backend/app/auth/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/backend/app/auth/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/auth/deps.py b/backend/app/auth/deps.py new file mode 100644 index 00000000..8d612d7e --- /dev/null +++ b/backend/app/auth/deps.py @@ -0,0 +1,39 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import select + +from app.auth.jwt import verify_access_token +from app.database.models import Users +from app.database.session import get_session + +security = HTTPBearer(auto_error=False) + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials | None = Depends(security), + session: AsyncSession = Depends(get_session), +) -> Users: + if credentials is None or credentials.scheme.lower() != "bearer": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + + user_id = verify_access_token(credentials.credentials) + if not user_id: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or expired token", + headers={"WWW-Authenticate": "Bearer"}, + ) + + result = await session.execute(select(Users).where(Users.id == user_id)) + user = result.scalar_one_or_none() + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found", + ) + return user diff --git a/backend/app/auth/jwt.py b/backend/app/auth/jwt.py new file mode 100644 index 00000000..6ff63de6 --- /dev/null +++ b/backend/app/auth/jwt.py @@ -0,0 +1,67 @@ +import os +from datetime import datetime, timedelta, timezone +from typing import Any + +from jose import JWTError, jwt + +JWT_SECRET = os.getenv("JWT_SECRET", "") +if not JWT_SECRET: + raise RuntimeError( + "JWT_SECRET environment variable must be set before starting the API" + ) +JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256") +ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("JWT_ACCESS_EXPIRE_MINUTES", "30")) +REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("JWT_REFRESH_EXPIRE_DAYS", "7")) + + +def _create_token( + subject: str, + token_type: str, + expires_delta: timedelta, +) -> str: + expire = datetime.now(timezone.utc) + expires_delta + payload: dict[str, Any] = { + "sub": subject, + "type": token_type, + "exp": expire, + } + return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) + + +def create_access_token(user_id: str) -> str: + return _create_token( + user_id, + "access", + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), + ) + + +def create_refresh_token(user_id: str) -> str: + return _create_token( + user_id, + "refresh", + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS), + ) + + +def decode_token(token: str) -> dict[str, Any]: + return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + + +def _verify_token(token: str, expected_type: str) -> str | None: + try: + payload = decode_token(token) + if payload.get("type") != expected_type: + return None + sub = payload.get("sub") + return str(sub) if sub else None + except JWTError: + return None + + +def verify_access_token(token: str) -> str | None: + return _verify_token(token, "access") + + +def verify_refresh_token(token: str) -> str | None: + return _verify_token(token, "refresh") diff --git a/backend/app/auth/passwords.py b/backend/app/auth/passwords.py new file mode 100644 index 00000000..ba04abb6 --- /dev/null +++ b/backend/app/auth/passwords.py @@ -0,0 +1,14 @@ +import bcrypt + + +def hash_password(password: str) -> str: + hashed: bytes = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) + return hashed.decode("utf-8") + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + result: bool = bcrypt.checkpw( + plain_password.encode("utf-8"), + hashed_password.encode("utf-8"), + ) + return result diff --git a/backend/app/config.py b/backend/app/config.py index 71a095c7..e480fe53 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -22,8 +22,6 @@ class Settings(BaseSettings): debug: bool = True host: str = "0.0.0.0" port: int = 8000 - secret_key: str = "your-secret-key-change-in-production" - # ============ Rate Limiting ============ rate_limit_requests: int = 20 rate_limit_seconds: int = 1 diff --git a/backend/app/database/models.py b/backend/app/database/models.py index 0ca9c57c..d8d4cc26 100644 --- a/backend/app/database/models.py +++ b/backend/app/database/models.py @@ -1,5 +1,7 @@ -from datetime import datetime, timezone +from datetime import date, datetime, timezone from typing import List, Optional + +from sqlalchemy import BigInteger, Column, UniqueConstraint from sqlmodel import SQLModel, Field, Relationship # @NeoMachabaUP : @@ -37,13 +39,15 @@ class Champions(SQLModel, table=True): # Users # Represents a registered Vantage Point account. -# We don't store passwords — Cognito owns that. -# cognito_sub is the 'sub' claim from the Cognito JWT token, -# it's the stable unique identifier for a user. class Users(SQLModel, table=True): - cognito_sub: str = Field(primary_key=True) - email: str - created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + id: str = Field(primary_key=True) + email: str = Field(unique=True, index=True) + password_hash: str + display_name: str + avatar_url: str | None = None + created_at: datetime = Field( + default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None) + ) linked_game_accounts: List["UserGameAccounts"] = Relationship(back_populates="user") @@ -60,10 +64,15 @@ class GameAccounts(SQLModel, table=True): game: str # identifies which game this account belongs to e.g. "league_of_legends", "dota2" game_name: str tag_line: str # the part after '#' in Riot ID, e.g. "EUW" in "Player#EUW" - summoner_level: int + account_level: int + profile_matches_sampled: Optional[int] = None linked_users: List["UserGameAccounts"] = Relationship(back_populates="game_account") participations: List["Participants"] = Relationship(back_populates="game_account") + achievements: List["UserAchievements"] = Relationship(back_populates="game_account") + featured_games: List["UserFeaturedGames"] = Relationship( + back_populates="game_account" + ) # UserGameAccounts @@ -73,7 +82,7 @@ class UserGameAccounts(SQLModel, table=True): __tablename__ = "user_game_accounts" id: Optional[int] = Field(default=None, primary_key=True) - cognito_sub: str = Field(foreign_key="users.cognito_sub") + user_id: str = Field(foreign_key="users.id") puuid: str = Field(foreign_key="game_accounts.puuid") user: "Users" = Relationship(back_populates="linked_game_accounts") @@ -92,7 +101,10 @@ class Matches(SQLModel, table=True): game_version: str # Patch the game was played on, e.g. "13.12" - this is important for tracking balance changes and how they affect champion performance over time. game_duration: int # in seconds; queue_id: int - game_creation: int + game_creation: int = Field(default=0, sa_column=Column(BigInteger())) # epoch ms + map_id: int = 11 + played_on: date = Field(default_factory=lambda: date.today()) + detail_json: Optional[str] = None # JSON: MatchDetail teams payload for scoreboard participants: List["Participants"] = Relationship(back_populates="match") @@ -107,15 +119,75 @@ class Participants(SQLModel, table=True): match_id: str = Field(foreign_key="matches.match_id") puuid: str = Field(foreign_key="game_accounts.puuid") champion_id: int = Field(foreign_key="champions.champion_id") - team_id: int + team_id: int = 100 win: bool kills: int deaths: int assists: int individual_position: str # Riot's assigned lane: "TOP", "JUNGLE", "MIDDLE", "BOTTOM", "UTILITY" ; will use this later to determine role for champion mastery and other stats + cs: int = 0 + gold_earned: int = 0 + damage_to_champions: int = 0 + vision_score: int = 0 + items_json: str = "[]" + summoner_spells_json: str = "[4, 14]" + riot_id_display: Optional[str] = None + kill_participation: Optional[float] = None # Below are back-references so we can navigate from a participant to its match/player/champion match: "Matches" = Relationship(back_populates="participants") game_account: "GameAccounts" = Relationship(back_populates="participations") champion: "Champions" = Relationship(back_populates="participants") + + +# AchievementDefinitions +# Catalog of achievement types shown on the profile (labels map to frontend icon ids). +class AchievementDefinitions(SQLModel, table=True): + __tablename__ = "achievement_definitions" + + id: str = Field(primary_key=True) + label: str + description: str + source_field: str + + user_achievements: List["UserAchievements"] = Relationship( + back_populates="definition" + ) + + +# UserAchievements +# Per-player achievement counts for the profile achievements row. +class UserAchievements(SQLModel, table=True): + __tablename__ = "user_achievements" + __table_args__ = (UniqueConstraint("puuid", "achievement_id"),) + + id: Optional[int] = Field(default=None, primary_key=True) + puuid: str = Field(foreign_key="game_accounts.puuid") + achievement_id: str = Field(foreign_key="achievement_definitions.id") + count: int + + game_account: "GameAccounts" = Relationship(back_populates="achievements") + definition: "AchievementDefinitions" = Relationship( + back_populates="user_achievements" + ) + + +# UserFeaturedGames +# Featured-game banner slides on the profile (marketing / summary cards). +class UserFeaturedGames(SQLModel, table=True): + __tablename__ = "user_featured_games" + + id: Optional[int] = Field(default=None, primary_key=True) + puuid: str = Field(foreign_key="game_accounts.puuid") + sort_order: int = 0 + game_name: str + cover_image_key: str + card_image_key: Optional[str] = None + efficiency_score: int + time_spent_seconds: int + wins: int + losses: int + average_kda: float + + game_account: "GameAccounts" = Relationship(back_populates="featured_games") diff --git a/backend/app/database/seed.py b/backend/app/database/seed.py index 71cf51c7..4fd35f5b 100644 --- a/backend/app/database/seed.py +++ b/backend/app/database/seed.py @@ -15,6 +15,16 @@ Matches, Participants, ) +from app.database.seed_data import ( + PROFILE_MATCHES_SAMPLED, + SEED_MATCHES, + SEED_VIEWER_PARTICIPANTS, + SEED_USER_ACHIEVEMENTS, + SEED_FEATURED_GAMES, + VIEWER_PUUID, +) +from app.database.seed_profile import seed_profile_for_puuid +from app.database.seed_data.matches import build_detail_json, game_creation_for load_dotenv() @@ -210,32 +220,42 @@ async def seed(): ) # --- Users --- - # cognito_sub format mirrors what AWS Cognito actually returns (UUID v4) + from app.auth.passwords import hash_password + + seed_password = os.getenv("SEED_DEV_PASSWORD") + if not seed_password: + raise RuntimeError( + "Set SEED_DEV_PASSWORD in the environment before running seed (see .env.example)" + ) + users = [ Users( - cognito_sub="fake-cognito-sub-0001", + id="00000000-0000-4000-8000-000000000001", email="testuser1@vantagepoint.dev", - # created_at=datetime.now(timezone.utc) + password_hash=hash_password(seed_password), + display_name="TestUser1", created_at=datetime.now(timezone.utc).replace(tzinfo=None), ), Users( - cognito_sub="fake-cognito-sub-0002", + id="00000000-0000-4000-8000-000000000002", email="testuser2@vantagepoint.dev", - # created_at=datetime.now(timezone.utc) + password_hash=hash_password(seed_password), + display_name="TestUser2", created_at=datetime.now(timezone.utc).replace(tzinfo=None), ), ] session.add_all(users) # --- Game Accounts --- - # PUUIDs are fake but consistent across the seed so FK constraints hold + # Viewer PUUID for seeded match history (seed-viewer-puuid) game_accounts = [ GameAccounts( - puuid="FAKE-PUUID-LOL-00000000000000000000000000000000000000000000000001", + puuid=VIEWER_PUUID, game="league_of_legends", - game_name="TheFast", - tag_line="4444", + game_name="You", + tag_line="EUW", account_level=100, + profile_matches_sampled=PROFILE_MATCHES_SAMPLED, ), GameAccounts( puuid="FAKE-PUUID-LOL-00000000000000000000000000000000000000000000000002", @@ -248,77 +268,65 @@ async def seed(): session.add_all(game_accounts) # --- User <-> Game Account links --- - # User 1 tracks both accounts — exercises the many-to-many relationship session.add_all( [ UserGameAccounts( - cognito_sub="fake-cognito-sub-0001", - puuid="FAKE-PUUID-LOL-00000000000000000000000000000000000000000000000001", - ), - UserGameAccounts( - cognito_sub="fake-cognito-sub-0002", - puuid="FAKE-PUUID-LOL-00000000000000000000000000000000000000000000000002", + user_id="00000000-0000-4000-8000-000000000001", + puuid=VIEWER_PUUID, ), UserGameAccounts( - cognito_sub="fake-cognito-sub-0001", + user_id="00000000-0000-4000-8000-000000000002", puuid="FAKE-PUUID-LOL-00000000000000000000000000000000000000000000000002", ), ] ) - # --- Matches --- + # --- Matches (8 seeded games) --- + from app.database.seed_data.matches import GAME_VERSION, MAP_ID, QUEUE_ID + matches = [ Matches( - match_id="EUW1_FAKE001", - game_version="12.1.123", - game_duration=1820, # ~30 min - queue_id=420, # Ranked Solo/Duo - ), - Matches( - match_id="EUW1_FAKE002", - game_version="12.1.123", - game_duration=2100, # ~35 min - queue_id=450, # ARAM - ), + match_id=row.match_id, + game_version=GAME_VERSION, + game_duration=row.game_duration, + queue_id=QUEUE_ID, + game_creation=game_creation_for(row.played_on, row.match_id), + map_id=MAP_ID, + played_on=row.played_on, + detail_json=build_detail_json(row.match_id), + ) + for row in SEED_MATCHES ] session.add_all(matches) - # --- Participants --- + # --- Viewer participants (one per match for list + profile) --- session.add_all( [ Participants( - match_id="EUW1_FAKE001", - puuid="FAKE-PUUID-LOL-00000000000000000000000000000000000000000000000001", - champion_id=202, # Jhin - win=True, - kills=12, - deaths=2, - assists=5, - individual_position="BOTTOM", - ), - Participants( - match_id="EUW1_FAKE001", - puuid="FAKE-PUUID-LOL-00000000000000000000000000000000000000000000000002", - champion_id=84, # Akali - win=False, - kills=4, - deaths=7, - assists=3, - individual_position="MIDDLE", - ), - Participants( - match_id="EUW1_FAKE002", - puuid="FAKE-PUUID-LOL-00000000000000000000000000000000000000000000000001", - champion_id=157, # Yasuo - win=False, - kills=6, - deaths=9, - assists=4, - individual_position="MIDDLE", - ), + match_id=vp.match_id, + puuid=VIEWER_PUUID, + champion_id=vp.champion_id, + win=vp.win, + kills=vp.kills, + deaths=vp.deaths, + assists=vp.assists, + individual_position=vp.individual_position, + team_id=vp.team_id, + cs=vp.cs, + gold_earned=vp.gold_earned, + damage_to_champions=vp.damage_to_champions, + vision_score=vp.vision_score, + kill_participation=vp.kill_participation, + riot_id_display="You#EUW", + items_json="[3031, 3006, 3046, 3035, 3036, 0, 3363]", + summoner_spells_json="[4, 14]", + ) + for vp in SEED_VIEWER_PARTICIPANTS ] ) + await seed_profile_for_puuid(session, VIEWER_PUUID, set_matches_sampled=False) + await session.commit() print("--- Seed complete ---") @@ -326,8 +334,13 @@ async def seed(): print(" Users: 2") print(" Game accounts: 2") print(" User-GA links: 3") - print(" Matches: 2") - print(" Participants: 3") + print(f" Matches: {len(SEED_MATCHES)}") + print(f" Participants: {len(SEED_VIEWER_PARTICIPANTS)} (viewer)") + print(f" Achievements: {len(SEED_USER_ACHIEVEMENTS)} (viewer)") + print(f" Featured games: {len(SEED_FEATURED_GAMES)} (viewer)") + print( + " Dev login: testuser1@vantagepoint.dev (password from SEED_DEV_PASSWORD)" + ) if __name__ == "__main__": diff --git a/backend/app/database/seed_data/__init__.py b/backend/app/database/seed_data/__init__.py new file mode 100644 index 00000000..bb12ae08 --- /dev/null +++ b/backend/app/database/seed_data/__init__.py @@ -0,0 +1,21 @@ +from app.database.seed_data.matches import ( + SEED_MATCHES, + SEED_VIEWER_PARTICIPANTS, + VIEWER_PUUID, +) +from app.database.seed_data.profile import ( + PROFILE_MATCHES_SAMPLED, + SEED_ACHIEVEMENT_DEFINITIONS, + SEED_FEATURED_GAMES, + SEED_USER_ACHIEVEMENTS, +) + +__all__ = [ + "SEED_MATCHES", + "SEED_VIEWER_PARTICIPANTS", + "VIEWER_PUUID", + "PROFILE_MATCHES_SAMPLED", + "SEED_ACHIEVEMENT_DEFINITIONS", + "SEED_USER_ACHIEVEMENTS", + "SEED_FEATURED_GAMES", +] diff --git a/backend/app/database/seed_data/matches.py b/backend/app/database/seed_data/matches.py new file mode 100644 index 00000000..9ac4492d --- /dev/null +++ b/backend/app/database/seed_data/matches.py @@ -0,0 +1,556 @@ +"""Seed payloads for dev match history and detail scoreboards.""" + +import json +from dataclasses import dataclass +from datetime import date +from typing import Any + +VIEWER_PUUID = "seed-viewer-puuid" +VIEWER_RIOT_ID = "You#EUW" + +GAME_VERSION = "14.24.1" +QUEUE_ID = 420 +MAP_ID = 11 + +POSITION_ORDER = ("TOP", "JUNGLE", "MIDDLE", "BOTTOM", "UTILITY") + +_PLAYED_EPOCH = { + "2025-04-19": 1_745_107_200_000, + "2025-04-18": 1_745_020_800_000, +} + +_MATCH_EPOCH_OFFSET: dict[str, int] = { + "EUW1_700000001": 0, + "EUW1_700000002": 1, + "EUW1_700000003": 2, + "EUW1_700000004": 3, + "EUW1_700000005": 4, + "EUW1_700000006": 5, + "EUW1_700000007": 6, + "EUW1_700000008": 7, +} + +CHAMPION_ID_TO_NAME: dict[int, str] = { + 51: "Caitlyn", + 64: "Lee Sin", + 86: "Garen", + 99: "Lux", + 103: "Ahri", + 122: "Darius", + 134: "Syndra", + 157: "Yasuo", + 222: "Jinx", + 234: "Viego", + 238: "Zed", + 412: "Thresh", + 89: "Leona", + 54: "Malphite", + 111: "Nautilus", + 267: "Nami", + 21: "Miss Fortune", + 777: "Yone", +} + +ITEM_BUILDS: dict[str, list[int]] = { + "adc": [3031, 3006, 3046, 3035, 3036, 0, 3363], + "ap": [3089, 3020, 3135, 3157, 0, 0, 3364], + "bruiser": [3078, 3053, 6333, 3065, 0, 0, 3340], + "tank": [3068, 3075, 3742, 3111, 0, 0, 3340], + "jungle": [6692, 3153, 3044, 3814, 0, 0, 3364], + "support": [3190, 3107, 3222, 2055, 3869, 0, 3364], +} + +SPELLS_DEFAULT: tuple[int, ...] = (4, 14) +SPELLS_JUNGLE: tuple[int, ...] = (4, 11) + + +@dataclass(frozen=True) +class SeedMatchRow: + match_id: str + game_duration: int + played_on: date + + +@dataclass(frozen=True) +class SeedViewerParticipant: + match_id: str + champion_id: int + win: bool + kills: int + deaths: int + assists: int + cs: int + individual_position: str + team_id: int + gold_earned: int + damage_to_champions: int + vision_score: int + kill_participation: float + + +@dataclass(frozen=True) +class BotSlot: + champion_id: int + position: str + kills: int + deaths: int + assists: int + cs: int + gold_earned: int + damage_to_champions: int + vision_score: int + item_key: str = "bruiser" + summoner_spells: tuple[int, ...] = SPELLS_DEFAULT + + +@dataclass(frozen=True) +class MatchBotRoster: + allies: tuple[BotSlot, BotSlot, BotSlot, BotSlot] + enemies: tuple[BotSlot, BotSlot, BotSlot, BotSlot, BotSlot] + ally_bans: tuple[int, ...] + enemy_bans: tuple[int, ...] + + +def _bot( + champion_id: int, + position: str, + kills: int, + deaths: int, + assists: int, + cs: int, + gold: int, + damage: int, + vision: int = 25, + item_key: str = "bruiser", + spells: tuple[int, ...] = SPELLS_DEFAULT, +) -> BotSlot: + return BotSlot( + champion_id=champion_id, + position=position, + kills=kills, + deaths=deaths, + assists=assists, + cs=cs, + gold_earned=gold, + damage_to_champions=damage, + vision_score=vision, + item_key=item_key, + summoner_spells=spells, + ) + + +# Four ally bots + five enemies per match (viewer fills the 5th ally slot). +SEED_MATCH_BOT_ROSTERS: dict[str, MatchBotRoster] = { + "EUW1_700000001": MatchBotRoster( + allies=( + _bot(122, "TOP", 2, 9, 3, 142, 9800, 11200, 22, "tank"), + _bot(234, "JUNGLE", 5, 7, 8, 118, 9200, 12800, 38, "jungle", SPELLS_JUNGLE), + _bot(103, "MIDDLE", 7, 6, 4, 188, 10800, 19500, 28, "ap"), + _bot(412, "UTILITY", 0, 9, 16, 24, 7600, 4200, 72, "support"), + ), + enemies=( + _bot(86, "TOP", 6, 3, 5, 176, 11200, 14800, 24, "bruiser"), + _bot( + 64, "JUNGLE", 3, 4, 14, 128, 10100, 13200, 42, "jungle", SPELLS_JUNGLE + ), + _bot(134, "MIDDLE", 9, 4, 6, 201, 11800, 21000, 30, "ap"), + _bot(51, "BOTTOM", 10, 5, 4, 232, 13200, 24500, 26, "adc"), + _bot(99, "UTILITY", 1, 6, 19, 30, 7800, 5100, 68, "support"), + ), + ally_bans=(157, 64, 238, 555, 89), + enemy_bans=(222, 51, 134, 777, 360), + ), + "EUW1_700000002": MatchBotRoster( + allies=( + _bot(86, "TOP", 4, 2, 7, 184, 10900, 16200, 22, "bruiser"), + _bot( + 64, "JUNGLE", 6, 4, 11, 134, 10400, 13800, 44, "jungle", SPELLS_JUNGLE + ), + _bot(222, "BOTTOM", 9, 3, 5, 228, 12500, 23800, 24, "adc"), + _bot(89, "UTILITY", 1, 5, 20, 28, 8200, 4800, 76, "support"), + ), + enemies=( + _bot(122, "TOP", 5, 6, 2, 158, 10200, 14500, 20, "bruiser"), + _bot(234, "JUNGLE", 4, 8, 6, 112, 9400, 12100, 35, "jungle", SPELLS_JUNGLE), + _bot(134, "MIDDLE", 6, 5, 8, 192, 11100, 18800, 27, "ap"), + _bot(51, "BOTTOM", 7, 6, 3, 205, 11600, 20100, 25, "adc"), + _bot(267, "UTILITY", 2, 7, 12, 32, 7400, 3900, 62, "support"), + ), + ally_bans=(157, 238, 555, 360, 89), + enemy_bans=(103, 222, 64, 777, 134), + ), + "EUW1_700000003": MatchBotRoster( + allies=( + _bot(64, "JUNGLE", 7, 3, 9, 148, 10800, 15200, 46, "jungle", SPELLS_JUNGLE), + _bot(103, "MIDDLE", 6, 4, 10, 198, 11400, 20500, 32, "ap"), + _bot(222, "BOTTOM", 11, 2, 6, 242, 13800, 25200, 28, "adc"), + _bot(412, "UTILITY", 2, 4, 22, 36, 8600, 5200, 82, "support"), + ), + enemies=( + _bot(54, "TOP", 3, 8, 4, 142, 9800, 11800, 18, "tank"), + _bot(234, "JUNGLE", 5, 7, 5, 108, 9600, 12500, 36, "jungle", SPELLS_JUNGLE), + _bot(134, "MIDDLE", 8, 6, 4, 178, 10500, 17200, 26, "ap"), + _bot(21, "BOTTOM", 9, 5, 2, 198, 11200, 19800, 22, "adc"), + _bot(99, "UTILITY", 0, 9, 11, 26, 7200, 3600, 58, "support"), + ), + ally_bans=(122, 51, 238, 777, 267), + enemy_bans=(86, 222, 64, 555, 134), + ), + "EUW1_700000004": MatchBotRoster( + allies=( + _bot(86, "TOP", 3, 6, 4, 168, 10100, 13800, 20, "bruiser"), + _bot(103, "MIDDLE", 5, 7, 9, 176, 10600, 17800, 29, "ap"), + _bot(222, "BOTTOM", 8, 6, 2, 198, 11800, 21200, 23, "adc"), + _bot(111, "UTILITY", 1, 8, 14, 22, 7400, 4100, 70, "support"), + ), + enemies=( + _bot(122, "TOP", 7, 2, 3, 172, 11400, 16800, 24, "bruiser"), + _bot( + 234, "JUNGLE", 9, 4, 5, 124, 11200, 14200, 40, "jungle", SPELLS_JUNGLE + ), + _bot(134, "MIDDLE", 10, 3, 8, 188, 12000, 22400, 31, "ap"), + _bot(51, "BOTTOM", 12, 2, 4, 218, 12800, 24800, 27, "adc"), + _bot(267, "UTILITY", 2, 5, 16, 34, 8000, 5400, 74, "support"), + ), + ally_bans=(157, 51, 777, 360, 89), + enemy_bans=(64, 222, 238, 555, 134), + ), + "EUW1_700000005": MatchBotRoster( + allies=( + _bot(122, "TOP", 4, 5, 2, 152, 10400, 14200, 21, "bruiser"), + _bot(234, "JUNGLE", 6, 6, 7, 116, 9800, 13100, 39, "jungle", SPELLS_JUNGLE), + _bot(134, "MIDDLE", 8, 4, 9, 194, 11600, 20800, 30, "ap"), + _bot(267, "UTILITY", 2, 6, 17, 30, 7900, 4600, 71, "support"), + ), + enemies=( + _bot(86, "TOP", 5, 4, 6, 180, 11000, 15500, 23, "bruiser"), + _bot( + 64, "JUNGLE", 2, 5, 13, 136, 10200, 12800, 41, "jungle", SPELLS_JUNGLE + ), + _bot(103, "MIDDLE", 7, 5, 5, 202, 11300, 19200, 28, "ap"), + _bot(222, "BOTTOM", 6, 8, 4, 186, 10800, 18500, 24, "adc"), + _bot(412, "UTILITY", 1, 7, 15, 28, 7600, 4300, 66, "support"), + ), + ally_bans=(222, 157, 64, 89, 777), + enemy_bans=(51, 103, 238, 555, 360), + ), + "EUW1_700000006": MatchBotRoster( + allies=( + _bot(234, "JUNGLE", 4, 8, 4, 108, 9200, 11800, 37, "jungle", SPELLS_JUNGLE), + _bot(134, "MIDDLE", 6, 6, 3, 168, 10400, 17500, 27, "ap"), + _bot(51, "BOTTOM", 5, 7, 1, 174, 10000, 16800, 22, "adc"), + _bot(99, "UTILITY", 0, 10, 9, 22, 7100, 3800, 64, "support"), + ), + enemies=( + _bot(86, "TOP", 8, 2, 4, 188, 11800, 17200, 25, "bruiser"), + _bot( + 64, "JUNGLE", 7, 3, 10, 140, 11000, 14500, 43, "jungle", SPELLS_JUNGLE + ), + _bot(103, "MIDDLE", 11, 2, 6, 212, 12200, 23200, 33, "ap"), + _bot(222, "BOTTOM", 9, 4, 5, 226, 12600, 24100, 26, "adc"), + _bot(412, "UTILITY", 2, 5, 18, 34, 8400, 4900, 77, "support"), + ), + ally_bans=(86, 103, 64, 267, 777), + enemy_bans=(122, 222, 51, 555, 134), + ), + "EUW1_700000007": MatchBotRoster( + allies=( + _bot(86, "TOP", 6, 3, 5, 192, 11500, 16800, 24, "bruiser"), + _bot( + 64, "JUNGLE", 5, 4, 12, 138, 10700, 14100, 45, "jungle", SPELLS_JUNGLE + ), + _bot(222, "BOTTOM", 10, 2, 8, 236, 13200, 24600, 27, "adc"), + _bot(89, "UTILITY", 1, 4, 21, 32, 8800, 5000, 80, "support"), + ), + enemies=( + _bot(122, "TOP", 4, 7, 2, 148, 10100, 13200, 19, "bruiser"), + _bot(234, "JUNGLE", 6, 8, 4, 114, 9500, 12200, 38, "jungle", SPELLS_JUNGLE), + _bot(103, "MIDDLE", 5, 6, 7, 184, 10800, 18100, 29, "ap"), + _bot(51, "BOTTOM", 8, 5, 3, 208, 11400, 20500, 25, "adc"), + _bot(111, "UTILITY", 2, 8, 11, 26, 7300, 3700, 61, "support"), + ), + ally_bans=(134, 157, 238, 360, 89), + enemy_bans=(103, 222, 64, 777, 51), + ), + "EUW1_700000008": MatchBotRoster( + allies=( + _bot(122, "TOP", 3, 8, 1, 138, 9600, 12500, 20, "bruiser"), + _bot(234, "JUNGLE", 5, 9, 6, 102, 9000, 11200, 36, "jungle", SPELLS_JUNGLE), + _bot(103, "MIDDLE", 4, 7, 5, 162, 9800, 15800, 26, "ap"), + _bot(51, "BOTTOM", 7, 5, 3, 188, 10400, 19200, 23, "adc"), + ), + enemies=( + _bot(86, "TOP", 7, 2, 6, 182, 11200, 15900, 22, "bruiser"), + _bot(64, "JUNGLE", 8, 3, 9, 146, 11400, 15200, 44, "jungle", SPELLS_JUNGLE), + _bot(134, "MIDDLE", 10, 1, 8, 198, 11800, 21800, 30, "ap"), + _bot(222, "BOTTOM", 11, 2, 4, 224, 12800, 25200, 25, "adc"), + _bot(267, "UTILITY", 2, 4, 17, 36, 8200, 4800, 69, "support"), + ), + ally_bans=(412, 157, 64, 555, 777), + enemy_bans=(103, 222, 51, 89, 134), + ), +} + +_VIEWER_BY_MATCH: dict[str, SeedViewerParticipant] = {} + + +def _champion_name(champion_id: int) -> str: + return CHAMPION_ID_TO_NAME.get(champion_id, f"Champion{champion_id}") + + +def _participant_from_bot(bot: BotSlot, team_win: bool) -> dict[str, Any]: + name = _champion_name(bot.champion_id) + return { + "puuid": f"puuid-{bot.champion_id}-{bot.position}", + "riot_id": f"{name.replace(' ', '')}Bot#EUW", + "champion_id": bot.champion_id, + "champion_name": name, + "position": bot.position, + "win": team_win, + "kills": bot.kills, + "deaths": bot.deaths, + "assists": bot.assists, + "cs": bot.cs, + "gold_earned": bot.gold_earned, + "damage_to_champions": bot.damage_to_champions, + "vision_score": bot.vision_score, + "items": ITEM_BUILDS.get(bot.item_key, ITEM_BUILDS["bruiser"]), + "summoner_spells": list(bot.summoner_spells), + "is_viewer": False, + } + + +def _participant_from_viewer( + viewer: SeedViewerParticipant, team_win: bool +) -> dict[str, Any]: + name = _champion_name(viewer.champion_id) + item_key = { + "BOTTOM": "adc", + "MIDDLE": "ap", + "TOP": "bruiser", + "JUNGLE": "jungle", + "UTILITY": "support", + }.get(viewer.individual_position, "bruiser") + spells = SPELLS_JUNGLE if viewer.individual_position == "JUNGLE" else SPELLS_DEFAULT + return { + "puuid": VIEWER_PUUID, + "riot_id": VIEWER_RIOT_ID, + "champion_id": viewer.champion_id, + "champion_name": name, + "position": viewer.individual_position, + "win": team_win, + "kills": viewer.kills, + "deaths": viewer.deaths, + "assists": viewer.assists, + "cs": viewer.cs, + "gold_earned": viewer.gold_earned, + "damage_to_champions": viewer.damage_to_champions, + "vision_score": viewer.vision_score, + "items": ITEM_BUILDS[item_key], + "summoner_spells": list(spells), + "is_viewer": True, + } + + +def _slot_for_position(slots: tuple[BotSlot, ...], position: str) -> BotSlot | None: + for slot in slots: + if slot.position == position: + return slot + return None + + +def _build_team_participants( + viewer: SeedViewerParticipant, + bots: tuple[BotSlot, ...], + team_id: int, + team_win: bool, +) -> list[dict[str, Any]]: + participants: list[dict[str, Any]] = [] + for position in POSITION_ORDER: + if team_id == viewer.team_id and position == viewer.individual_position: + participants.append(_participant_from_viewer(viewer, team_win)) + else: + bot = _slot_for_position(bots, position) + if bot: + participants.append(_participant_from_bot(bot, team_win)) + return participants + + +def _objectives_for_win(team_won: bool) -> dict[str, int]: + return { + "baron": int(team_won), + "dragon": 3 if team_won else 2, + "rift_herald": int(team_won), + "tower": 9 if team_won else 4, + "inhibitor": 2 if team_won else 0, + } + + +def _team_payload( + team_id: int, + team_won: bool, + bans: tuple[int, ...], + bots: tuple[BotSlot, ...], + viewer: SeedViewerParticipant, +) -> dict[str, Any]: + return { + "team_id": team_id, + "win": team_won, + "bans": list(bans), + "objectives": _objectives_for_win(team_won), + "participants": _build_team_participants(viewer, bots, team_id, team_won), + } + + +def _build_teams_for_match( + viewer: SeedViewerParticipant, roster: MatchBotRoster +) -> list[dict[str, Any]]: + ally_win = viewer.win + enemy_win = not ally_win + + blue_win = ally_win if viewer.team_id == 100 else enemy_win + red_win = ally_win if viewer.team_id == 200 else enemy_win + + blue_bots: tuple[BotSlot, ...] + red_bots: tuple[BotSlot, ...] + blue_bans: tuple[int, ...] + red_bans: tuple[int, ...] + if viewer.team_id == 100: + blue_bots, red_bots = roster.allies, roster.enemies + blue_bans, red_bans = roster.ally_bans, roster.enemy_bans + else: + blue_bots, red_bots = roster.enemies, roster.allies + blue_bans, red_bans = roster.enemy_bans, roster.ally_bans + + return [ + _team_payload(100, blue_win, blue_bans, blue_bots, viewer), + _team_payload(200, red_win, red_bans, red_bots, viewer), + ] + + +def build_detail_json(match_id: str) -> str: + viewer = _VIEWER_BY_MATCH[match_id] + roster = SEED_MATCH_BOT_ROSTERS[match_id] + teams = _build_teams_for_match(viewer, roster) + return json.dumps({"teams": teams, "match_id": match_id}) + + +SEED_MATCHES: list[SeedMatchRow] = [ + SeedMatchRow("EUW1_700000001", 25 * 60, date(2025, 4, 19)), + SeedMatchRow("EUW1_700000002", 30 * 60, date(2025, 4, 19)), + SeedMatchRow("EUW1_700000003", 40 * 60, date(2025, 4, 19)), + SeedMatchRow("EUW1_700000004", 20 * 60, date(2025, 4, 19)), + SeedMatchRow("EUW1_700000005", 28 * 60, date(2025, 4, 19)), + SeedMatchRow("EUW1_700000006", 22 * 60, date(2025, 4, 19)), + SeedMatchRow("EUW1_700000007", 35 * 60, date(2025, 4, 18)), + SeedMatchRow("EUW1_700000008", 18 * 60, date(2025, 4, 18)), +] + +SEED_VIEWER_PARTICIPANTS: list[SeedViewerParticipant] = [ + SeedViewerParticipant( + "EUW1_700000001", + 222, + False, + 4, + 8, + 6, + 165, + "BOTTOM", + 200, + 10_500, + 16_000, + 22, + 0.55, + ), + SeedViewerParticipant( + "EUW1_700000002", + 103, + True, + 8, + 3, + 6, + 210, + "MIDDLE", + 100, + 12_000, + 22_000, + 35, + 0.72, + ), + SeedViewerParticipant( + "EUW1_700000003", 86, True, 5, 2, 4, 198, "TOP", 100, 11_500, 19_000, 28, 0.65 + ), + SeedViewerParticipant( + "EUW1_700000004", + 64, + False, + 4, + 5, + 12, + 142, + "JUNGLE", + 100, + 9_800, + 14_000, + 40, + 0.80, + ), + SeedViewerParticipant( + "EUW1_700000005", + 51, + True, + 11, + 2, + 5, + 224, + "BOTTOM", + 200, + 14_000, + 28_000, + 30, + 0.70, + ), + SeedViewerParticipant( + "EUW1_700000006", 122, False, 3, 7, 2, 156, "TOP", 200, 9_000, 15_000, 18, 0.45 + ), + SeedViewerParticipant( + "EUW1_700000007", + 134, + True, + 9, + 4, + 7, + 205, + "MIDDLE", + 100, + 13_500, + 25_000, + 32, + 0.68, + ), + SeedViewerParticipant( + "EUW1_700000008", + 412, + False, + 1, + 6, + 14, + 28, + "UTILITY", + 200, + 7_500, + 8_000, + 78, + 0.85, + ), +] + +_VIEWER_BY_MATCH.update({v.match_id: v for v in SEED_VIEWER_PARTICIPANTS}) + + +def game_creation_for(played_on: date, match_id: str | None = None) -> int: + key = played_on.isoformat() + base = _PLAYED_EPOCH.get(key, _PLAYED_EPOCH["2025-04-19"]) + if match_id and match_id in _MATCH_EPOCH_OFFSET: + return base + _MATCH_EPOCH_OFFSET[match_id] * 60_000 + return base diff --git a/backend/app/database/seed_data/profile.py b/backend/app/database/seed_data/profile.py new file mode 100644 index 00000000..f4c43cc2 --- /dev/null +++ b/backend/app/database/seed_data/profile.py @@ -0,0 +1,114 @@ +"""Seed payloads for profile achievements and featured-game banners.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class SeedAchievementDefinition: + id: str + label: str + description: str + source_field: str + + +@dataclass(frozen=True) +class SeedUserAchievement: + achievement_id: str + count: int + + +@dataclass(frozen=True) +class SeedFeaturedGame: + sort_order: int + game_name: str + cover_image_key: str + card_image_key: str | None + efficiency_score: int + time_spent_seconds: int + wins: int + losses: int + average_kda: float + + +PROFILE_MATCHES_SAMPLED = 20 + +SEED_ACHIEVEMENT_DEFINITIONS: list[SeedAchievementDefinition] = [ + SeedAchievementDefinition( + "triple-kill", + "Triple", + "Triple kills across sampled matches", + "challenges.tripleKills", + ), + SeedAchievementDefinition( + "first-blood", + "First Blood", + "First blood kills", + "challenges.firstBloodKill", + ), + SeedAchievementDefinition( + "killing-spree", + "Spree", + "Killing sprees of 3+", + "challenges.killingSprees", + ), + SeedAchievementDefinition( + "high-kp", + "Team Fight", + "Matches with 70%+ kill participation", + "challenges.killParticipation", + ), + SeedAchievementDefinition( + "vision", + "Ward King", + "Top vision score on team", + "challenges.visionScorePerMinute", + ), + SeedAchievementDefinition( + "damage", + "Carry", + "Highest damage to champions on team", + "challenges.teamDamagePercentage", + ), + SeedAchievementDefinition( + "turrets", + "Siege", + "Turret takedowns", + "challenges.turretTakedowns", + ), +] + +SEED_USER_ACHIEVEMENTS: list[SeedUserAchievement] = [ + SeedUserAchievement("triple-kill", 4), + SeedUserAchievement("first-blood", 3), + SeedUserAchievement("killing-spree", 12), + SeedUserAchievement("high-kp", 9), + SeedUserAchievement("vision", 6), + SeedUserAchievement("damage", 7), + SeedUserAchievement("turrets", 18), +] + +# 1:04:34:23 and 0:42:18:05 (D:HH:MM:SS) +SEED_FEATURED_GAMES: list[SeedFeaturedGame] = [ + SeedFeaturedGame( + sort_order=0, + game_name="League Of Legends", + cover_image_key="league_wild_rift_cover", + card_image_key="league_wild_rift_card", + efficiency_score=115, + time_spent_seconds=1 * 86400 + 4 * 3600 + 34 * 60 + 23, + wins=13, + losses=7, + average_kda=3.8, + ), + SeedFeaturedGame( + sort_order=1, + game_name="League Of Legends", + cover_image_key="league_wild_rift_cover", + card_image_key="league_wild_rift_card", + efficiency_score=98, + time_spent_seconds=42 * 3600 + 18 * 60 + 5, + wins=7, + losses=5, + average_kda=3.2, + ), +] diff --git a/backend/app/database/seed_profile.py b/backend/app/database/seed_profile.py new file mode 100644 index 00000000..4ee2b628 --- /dev/null +++ b/backend/app/database/seed_profile.py @@ -0,0 +1,74 @@ +"""Shared profile seed inserts for main seed and test fixtures.""" + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.database.models import ( + AchievementDefinitions, + GameAccounts, + UserAchievements, + UserFeaturedGames, +) +from app.database.seed_data.profile import ( + PROFILE_MATCHES_SAMPLED, + SEED_ACHIEVEMENT_DEFINITIONS, + SEED_FEATURED_GAMES, + SEED_USER_ACHIEVEMENTS, +) + + +async def seed_profile_for_puuid( + session: AsyncSession, + puuid: str, + *, + set_matches_sampled: bool = True, +) -> None: + session.add_all( + [ + AchievementDefinitions( + id=d.id, + label=d.label, + description=d.description, + source_field=d.source_field, + ) + for d in SEED_ACHIEVEMENT_DEFINITIONS + ] + ) + + session.add_all( + [ + UserAchievements( + puuid=puuid, + achievement_id=ua.achievement_id, + count=ua.count, + ) + for ua in SEED_USER_ACHIEVEMENTS + ] + ) + + session.add_all( + [ + UserFeaturedGames( + puuid=puuid, + sort_order=fg.sort_order, + game_name=fg.game_name, + cover_image_key=fg.cover_image_key, + card_image_key=fg.card_image_key, + efficiency_score=fg.efficiency_score, + time_spent_seconds=fg.time_spent_seconds, + wins=fg.wins, + losses=fg.losses, + average_kda=fg.average_kda, + ) + for fg in SEED_FEATURED_GAMES + ] + ) + + if set_matches_sampled: + from sqlmodel import select + + result = await session.execute( + select(GameAccounts).where(GameAccounts.puuid == puuid) + ) + account = result.scalar_one_or_none() + if account: + account.profile_matches_sampled = PROFILE_MATCHES_SAMPLED diff --git a/backend/app/database/session.py b/backend/app/database/session.py index 9437a56f..c8d9c4ae 100644 --- a/backend/app/database/session.py +++ b/backend/app/database/session.py @@ -1,13 +1,17 @@ import os from collections.abc import AsyncGenerator +from dotenv import load_dotenv from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlmodel import SQLModel -DATABASE_URL = os.getenv( - "DATABASE_URL", - "postgresql+asyncpg://postgres:password@localhost:5432/vantage_point_db", -) +load_dotenv() + +DATABASE_URL = os.getenv("DATABASE_URL") +if not DATABASE_URL: + raise RuntimeError( + "DATABASE_URL environment variable must be set (see backend/.env.example)" + ) engine = create_async_engine(DATABASE_URL) async_session_maker = async_sessionmaker(engine, expire_on_commit=False) diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/backend/app/routers/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py new file mode 100644 index 00000000..b24d51db --- /dev/null +++ b/backend/app/routers/auth.py @@ -0,0 +1,92 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import select + +from app.auth.jwt import create_access_token, create_refresh_token, verify_refresh_token +from app.auth.passwords import hash_password, verify_password +from app.database.models import Users +from app.database.session import get_session +from app.schemas.auth import ( + LoginRequest, + RefreshRequest, + RegisterRequest, + TokenResponse, +) + +router = APIRouter(prefix="/api/v1/auth", tags=["auth"]) + + +@router.post("/register", response_model=TokenResponse) +async def register( + body: RegisterRequest, + session: AsyncSession = Depends(get_session), +): + existing = await session.execute(select(Users).where(Users.email == body.email)) + if existing.scalar_one_or_none(): + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Email already registered", + ) + + user_id = str(uuid.uuid4()) + user = Users( + id=user_id, + email=body.email, + display_name=body.display_name.strip(), + password_hash=hash_password(body.password), + ) + session.add(user) + await session.commit() + + return TokenResponse( + access_token=create_access_token(user_id), + refresh_token=create_refresh_token(user_id), + ) + + +@router.post("/login", response_model=TokenResponse) +async def login( + body: LoginRequest, + session: AsyncSession = Depends(get_session), +): + result = await session.execute(select(Users).where(Users.email == body.email)) + user = result.scalar_one_or_none() + if not user or not verify_password(body.password, user.password_hash): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid email or password", + ) + + user_id = user.id + return TokenResponse( + access_token=create_access_token(user_id), + refresh_token=create_refresh_token(user_id), + ) + + +@router.post("/refresh", response_model=TokenResponse) +async def refresh( + body: RefreshRequest, + session: AsyncSession = Depends(get_session), +): + user_id = verify_refresh_token(body.refresh_token) + if not user_id: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or expired refresh token", + ) + + result = await session.execute(select(Users).where(Users.id == user_id)) + user = result.scalar_one_or_none() + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found", + ) + + return TokenResponse( + access_token=create_access_token(user_id), + refresh_token=create_refresh_token(user_id), + ) diff --git a/backend/app/routers/matches.py b/backend/app/routers/matches.py new file mode 100644 index 00000000..1cba51f9 --- /dev/null +++ b/backend/app/routers/matches.py @@ -0,0 +1,46 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.auth.deps import get_current_user +from app.database.models import Users +from app.database.session import get_session +from app.schemas.match import MatchDetailResponse, MatchHistorySummaryResponse +from app.services.match_detail import get_match_detail, user_has_match_access +from app.services.match_history import list_match_history +from app.services.user_accounts import get_linked_puuids, get_primary_linked_puuid + +router = APIRouter(prefix="/api/v1/matches", tags=["matches"]) + + +@router.get("", response_model=list[MatchHistorySummaryResponse]) +async def get_matches( + current_user: Users = Depends(get_current_user), + session: AsyncSession = Depends(get_session), +): + puuid = await get_primary_linked_puuid(session, current_user.id) + if not puuid: + return [] + return await list_match_history(session, puuid) + + +@router.get("/{match_id}", response_model=MatchDetailResponse) +async def get_match_by_id( + match_id: str, + current_user: Users = Depends(get_current_user), + session: AsyncSession = Depends(get_session), +): + puuids = await get_linked_puuids(session, current_user.id) + if not await user_has_match_access(session, puuids, match_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Match not found", + ) + + viewer_puuid = await get_primary_linked_puuid(session, current_user.id) + detail = await get_match_detail(session, match_id, viewer_puuid) + if not detail: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Match not found", + ) + return detail diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py new file mode 100644 index 00000000..c722a770 --- /dev/null +++ b/backend/app/routers/users.py @@ -0,0 +1,133 @@ +from fastapi import APIRouter, Depends, File, UploadFile, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.auth.deps import get_current_user +from app.database.models import Users +from app.database.session import get_session +from app.schemas.profile import PlayerProfileResponse +from app.schemas.user import ( + AvatarUploadResponse, + LinkGameAccountRequest, + LinkGameAccountResponse, + UpdateUserMeRequest, + UserMeResponse, +) +from app.services.avatar_storage import delete_avatar_files, save_avatar +from app.services.player_profile import build_player_profile +from app.services.user_accounts import ( + get_primary_linked_account, + get_primary_linked_puuid, + link_riot_account_for_user, + riot_id_tag, +) + +router = APIRouter(prefix="/api/v1/users", tags=["users"]) + + +def _user_me_response(user: Users, account) -> UserMeResponse: + tag = riot_id_tag(account.game_name, account.tag_line) if account else None + return UserMeResponse( + id=user.id, + email=user.email, + display_name=user.display_name, + avatar_url=user.avatar_url, + riot_id_tag=tag, + has_linked_riot=account is not None, + ) + + +@router.get("/me", response_model=UserMeResponse) +async def get_me( + current_user: Users = Depends(get_current_user), + session: AsyncSession = Depends(get_session), +): + account = await get_primary_linked_account(session, current_user.id) + return _user_me_response(current_user, account) + + +@router.patch("/me", response_model=UserMeResponse) +async def update_me( + body: UpdateUserMeRequest, + current_user: Users = Depends(get_current_user), + session: AsyncSession = Depends(get_session), +): + current_user.display_name = body.display_name.strip() + session.add(current_user) + await session.commit() + await session.refresh(current_user) + account = await get_primary_linked_account(session, current_user.id) + return _user_me_response(current_user, account) + + +@router.post("/me/avatar", response_model=AvatarUploadResponse) +async def upload_avatar( + file: UploadFile = File(...), + current_user: Users = Depends(get_current_user), + session: AsyncSession = Depends(get_session), +): + avatar_path = await save_avatar(current_user.id, file) + current_user.avatar_url = avatar_path + session.add(current_user) + await session.commit() + return AvatarUploadResponse(avatar_url=avatar_path) + + +@router.delete("/me/avatar", status_code=status.HTTP_204_NO_CONTENT) +async def delete_avatar( + current_user: Users = Depends(get_current_user), + session: AsyncSession = Depends(get_session), +): + delete_avatar_files(current_user.id) + current_user.avatar_url = None + session.add(current_user) + await session.commit() + + +@router.get("/me/profile", response_model=PlayerProfileResponse) +async def get_my_profile( + current_user: Users = Depends(get_current_user), + session: AsyncSession = Depends(get_session), +): + account = await get_primary_linked_account(session, current_user.id) + riot_id_tag_value = ( + riot_id_tag(account.game_name, account.tag_line) if account else None + ) + puuid = await get_primary_linked_puuid(session, current_user.id) + return await build_player_profile(session, current_user, puuid, riot_id_tag_value) + + +async def _link_game_account_impl( + body: LinkGameAccountRequest, + current_user: Users, + session: AsyncSession, +) -> LinkGameAccountResponse: + puuid, tag = await link_riot_account_for_user( + session, + current_user.id, + riot_id=body.riot_id, + game_name=body.game_name, + tag_line=body.tag_line, + ) + return LinkGameAccountResponse( + puuid=puuid, + riot_id_tag=tag, + message=f"Successfully linked {tag}", + ) + + +@router.post("/me/game-accounts", response_model=LinkGameAccountResponse) +async def link_game_account( + body: LinkGameAccountRequest, + current_user: Users = Depends(get_current_user), + session: AsyncSession = Depends(get_session), +): + return await _link_game_account_impl(body, current_user, session) + + +@router.put("/me/game-accounts", response_model=LinkGameAccountResponse) +async def update_game_account( + body: LinkGameAccountRequest, + current_user: Users = Depends(get_current_user), + session: AsyncSession = Depends(get_session), +): + return await _link_game_account_impl(body, current_user, session) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/backend/app/schemas/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py new file mode 100644 index 00000000..235e4b77 --- /dev/null +++ b/backend/app/schemas/auth.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel, EmailStr, Field + + +class RegisterRequest(BaseModel): + email: EmailStr + display_name: str = Field(min_length=1, max_length=64) + password: str = Field(min_length=8, max_length=128) + + +class LoginRequest(BaseModel): + email: EmailStr + password: str + + +class RefreshRequest(BaseModel): + refresh_token: str + + +class TokenResponse(BaseModel): + access_token: str + refresh_token: str + token_type: str = "bearer" diff --git a/backend/app/schemas/match.py b/backend/app/schemas/match.py new file mode 100644 index 00000000..366c28ca --- /dev/null +++ b/backend/app/schemas/match.py @@ -0,0 +1,67 @@ +from pydantic import BaseModel + + +class ObjectivesSummaryResponse(BaseModel): + baron: int + dragon: int + rift_herald: int + tower: int + inhibitor: int + + +class ChampionBanResponse(BaseModel): + champion_id: int + champion_name: str + + +class ParticipantDetailResponse(BaseModel): + puuid: str + riot_id: str | None + champion_id: int + champion_name: str + position: str + win: bool + kills: int + deaths: int + assists: int + cs: int + gold_earned: int + damage_to_champions: int + vision_score: int + items: list[int] + summoner_spells: list[int] + is_viewer: bool + + +class TeamDetailResponse(BaseModel): + team_id: int + win: bool + bans: list[ChampionBanResponse] + objectives: ObjectivesSummaryResponse + participants: list[ParticipantDetailResponse] + + +class MatchDetailResponse(BaseModel): + match_id: str + game_creation: int + game_duration: int + game_version: str + queue_id: int + queue_label: str + map_id: int + map_label: str + teams: list[TeamDetailResponse] + + +class MatchHistorySummaryResponse(BaseModel): + match_id: str + champion_name: str + outcome: str + duration_minutes: int + map_label: str + played_on: str + kills: int + deaths: int + assists: int + cs: int + position: str diff --git a/backend/app/schemas/profile.py b/backend/app/schemas/profile.py new file mode 100644 index 00000000..75bcc7ae --- /dev/null +++ b/backend/app/schemas/profile.py @@ -0,0 +1,44 @@ +from pydantic import BaseModel + + +class RadarMetricResponse(BaseModel): + key: str + label: str + value: int + raw_label: str + + +class RecentChampionResponse(BaseModel): + champion_id: int + champion_name: str + games_played: int + + +class PlayerAchievementResponse(BaseModel): + id: str + label: str + description: str + source_field: str + count: int + + +class FeaturedGameSlideResponse(BaseModel): + game_name: str + cover_image_key: str + card_image_key: str | None = None + efficiency_score: int + time_spent_label: str + win_rate_label: str + kda_label: str + + +class PlayerProfileResponse(BaseModel): + display_name: str + riot_id_tag: str + avatar_initials: str + avatar_url: str | None = None + matches_sampled: int + radar_metrics: list[RadarMetricResponse] + recent_champions: list[RecentChampionResponse] + achievements: list[PlayerAchievementResponse] + featured_games: list[FeaturedGameSlideResponse] diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py new file mode 100644 index 00000000..8936c307 --- /dev/null +++ b/backend/app/schemas/user.py @@ -0,0 +1,30 @@ +from pydantic import BaseModel, Field + + +class UserMeResponse(BaseModel): + id: str + email: str + display_name: str + avatar_url: str | None = None + riot_id_tag: str | None + has_linked_riot: bool + + +class UpdateUserMeRequest(BaseModel): + display_name: str = Field(min_length=1, max_length=64) + + +class AvatarUploadResponse(BaseModel): + avatar_url: str + + +class LinkGameAccountRequest(BaseModel): + riot_id: str | None = Field(default=None, min_length=3, max_length=64) + game_name: str | None = Field(default=None, min_length=1, max_length=32) + tag_line: str | None = Field(default=None, min_length=1, max_length=16) + + +class LinkGameAccountResponse(BaseModel): + puuid: str + riot_id_tag: str + message: str diff --git a/backend/app/services/avatar_storage.py b/backend/app/services/avatar_storage.py new file mode 100644 index 00000000..0cc52ece --- /dev/null +++ b/backend/app/services/avatar_storage.py @@ -0,0 +1,62 @@ +from pathlib import Path + +from fastapi import HTTPException, UploadFile, status + +BACKEND_ROOT = Path(__file__).resolve().parents[2] +UPLOADS_DIR = BACKEND_ROOT / "uploads" +AVATARS_DIR = UPLOADS_DIR / "avatars" + +MAX_AVATAR_BYTES = 2 * 1024 * 1024 + +ALLOWED_CONTENT_TYPES: dict[str, str] = { + "image/jpeg": ".jpg", + "image/png": ".png", + "image/webp": ".webp", +} + + +def avatar_public_path(user_id: str, ext: str) -> str: + return f"/uploads/avatars/{user_id}{ext}" + + +def avatar_file_path(user_id: str, ext: str) -> Path: + return AVATARS_DIR / f"{user_id}{ext}" + + +def ensure_avatar_dir() -> None: + AVATARS_DIR.mkdir(parents=True, exist_ok=True) + + +def delete_avatar_files(user_id: str) -> None: + if not AVATARS_DIR.exists(): + return + for path in AVATARS_DIR.glob(f"{user_id}.*"): + path.unlink(missing_ok=True) + + +async def save_avatar(user_id: str, file: UploadFile) -> str: + content_type = (file.content_type or "").split(";")[0].strip().lower() + ext = ALLOWED_CONTENT_TYPES.get(content_type) + if not ext: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Avatar must be JPEG, PNG, or WebP", + ) + + data = await file.read() + if not data: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Empty file", + ) + if len(data) > MAX_AVATAR_BYTES: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Avatar must be 2 MB or smaller", + ) + + ensure_avatar_dir() + delete_avatar_files(user_id) + dest = avatar_file_path(user_id, ext) + dest.write_bytes(data) + return avatar_public_path(user_id, ext) diff --git a/backend/app/services/match_detail.py b/backend/app/services/match_detail.py new file mode 100644 index 00000000..06af767e --- /dev/null +++ b/backend/app/services/match_detail.py @@ -0,0 +1,124 @@ +import json + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import col, select + +from app.database.models import Champions, Matches, Participants +from app.schemas.match import ( + ChampionBanResponse, + MatchDetailResponse, + ObjectivesSummaryResponse, + ParticipantDetailResponse, + TeamDetailResponse, +) +from app.utils.game_labels import map_label, queue_label + + +async def user_has_match_access( + session: AsyncSession, user_puuids: list[str], match_id: str +) -> bool: + if not user_puuids: + return False + result = await session.execute( + select(Participants.internal_id) + .where( + col(Participants.match_id) == match_id, + col(Participants.puuid).in_(user_puuids), + ) + .limit(1) + ) + return result.scalar_one_or_none() is not None + + +async def get_match_detail( + session: AsyncSession, match_id: str, viewer_puuid: str | None +) -> MatchDetailResponse | None: + result = await session.execute( + select(Matches).where(col(Matches.match_id) == match_id) + ) + match = result.scalar_one_or_none() + if not match or not match.detail_json: + return None + + payload = json.loads(match.detail_json) + teams_raw = payload.get("teams", []) + + ban_ids: set[int] = set() + for team in teams_raw: + ban_ids.update(team.get("bans", [])) + + name_by_id: dict[int, str] = {} + if ban_ids: + champ_result = await session.execute( + select(Champions.champion_id, Champions.name).where( + col(Champions.champion_id).in_(ban_ids) + ) + ) + name_by_id = { + int(champion_id): str(name) for champion_id, name in champ_result.all() + } + + teams: list[TeamDetailResponse] = [] + + for team in teams_raw: + participants: list[ParticipantDetailResponse] = [] + for p in team.get("participants", []): + puuid = p.get("puuid", "") + is_viewer = bool(viewer_puuid and puuid == viewer_puuid) or p.get( + "is_viewer", False + ) + participants.append( + ParticipantDetailResponse( + puuid=puuid, + riot_id=p.get("riot_id"), + champion_id=p["champion_id"], + champion_name=p["champion_name"], + position=p["position"], + win=p["win"], + kills=p["kills"], + deaths=p["deaths"], + assists=p["assists"], + cs=p["cs"], + gold_earned=p["gold_earned"], + damage_to_champions=p["damage_to_champions"], + vision_score=p["vision_score"], + items=p.get("items", []), + summoner_spells=p.get("summoner_spells", []), + is_viewer=is_viewer, + ) + ) + obj = team.get("objectives", {}) + bans = [ + ChampionBanResponse( + champion_id=ban_id, + champion_name=name_by_id.get(ban_id, f"Champion {ban_id}"), + ) + for ban_id in team.get("bans", []) + ] + teams.append( + TeamDetailResponse( + team_id=team["team_id"], + win=team["win"], + bans=bans, + objectives=ObjectivesSummaryResponse( + baron=obj.get("baron", 0), + dragon=obj.get("dragon", 0), + rift_herald=obj.get("rift_herald", 0), + tower=obj.get("tower", 0), + inhibitor=obj.get("inhibitor", 0), + ), + participants=participants, + ) + ) + + return MatchDetailResponse( + match_id=match.match_id, + game_creation=match.game_creation, + game_duration=match.game_duration, + game_version=match.game_version, + queue_id=match.queue_id, + queue_label=queue_label(match.queue_id), + map_id=match.map_id, + map_label=map_label(match.map_id), + teams=teams, + ) diff --git a/backend/app/services/match_history.py b/backend/app/services/match_history.py new file mode 100644 index 00000000..f94a328c --- /dev/null +++ b/backend/app/services/match_history.py @@ -0,0 +1,38 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import col, select + +from app.database.models import Champions, Matches, Participants +from app.schemas.match import MatchHistorySummaryResponse +from app.utils.game_labels import map_label + + +async def list_match_history( + session: AsyncSession, puuid: str +) -> list[MatchHistorySummaryResponse]: + result = await session.execute( + select(Participants, Matches, Champions) + .join(Matches, col(Matches.match_id) == col(Participants.match_id)) + .join(Champions, col(Champions.champion_id) == col(Participants.champion_id)) + .where(col(Participants.puuid) == puuid) + .order_by(col(Matches.game_creation).desc(), col(Matches.match_id).desc()) + ) + rows = result.all() + summaries: list[MatchHistorySummaryResponse] = [] + for participant, match, champion in rows: + duration_minutes = max(1, round(match.game_duration / 60)) + summaries.append( + MatchHistorySummaryResponse( + match_id=match.match_id, + champion_name=champion.name, + outcome="Victory" if participant.win else "Defeat", + duration_minutes=duration_minutes, + map_label=map_label(match.map_id), + played_on=match.played_on.isoformat(), + kills=participant.kills, + deaths=participant.deaths, + assists=participant.assists, + cs=participant.cs, + position=participant.individual_position, + ) + ) + return summaries diff --git a/backend/app/services/player_profile.py b/backend/app/services/player_profile.py new file mode 100644 index 00000000..e31475c7 --- /dev/null +++ b/backend/app/services/player_profile.py @@ -0,0 +1,243 @@ +from collections import Counter + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import col, select + +from app.database.models import ( + AchievementDefinitions, + Champions, + GameAccounts, + Matches, + Participants, + UserAchievements, + UserFeaturedGames, + Users, +) +from app.schemas.profile import ( + FeaturedGameSlideResponse, + PlayerAchievementResponse, + PlayerProfileResponse, + RadarMetricResponse, + RecentChampionResponse, +) + + +def _avatar_initials(display_name: str) -> str: + parts = display_name.strip().split() + if not parts: + return "VP" + if len(parts) == 1: + return parts[0][:2].upper() + return f"{parts[0][0]}{parts[1][0]}".upper() + + +def _format_win_rate(wins: int, losses: int) -> str: + total = wins + losses + if total == 0: + return "—" + pct = round((wins / total) * 100) + return f"{pct}% ({wins}W / {losses}L)" + + +def _format_kda(avg: float) -> str: + return f"{avg:.1f} avg" + + +def _format_time_spent(total_seconds: int) -> str: + days, rem = divmod(total_seconds, 86400) + hours, rem = divmod(rem, 3600) + minutes, seconds = divmod(rem, 60) + return f"{days}:{hours:02d}:{minutes:02d}:{seconds:02d}" + + +def _normalize_radar(value: float, cap: float) -> int: + return min(100, max(0, round((value / cap) * 100))) + + +async def _load_matches_sampled(session: AsyncSession, puuid: str) -> int: + result = await session.execute( + select(GameAccounts.profile_matches_sampled).where(GameAccounts.puuid == puuid) + ) + value = result.scalar_one_or_none() + return int(value) if value is not None else 0 + + +async def _load_achievements( + session: AsyncSession, puuid: str +) -> list[PlayerAchievementResponse]: + result = await session.execute( + select(UserAchievements, AchievementDefinitions) + .join( + AchievementDefinitions, + col(AchievementDefinitions.id) == col(UserAchievements.achievement_id), + ) + .where(col(UserAchievements.puuid) == puuid) + .order_by(col(UserAchievements.achievement_id)) + ) + return [ + PlayerAchievementResponse( + id=definition.id, + label=definition.label, + description=definition.description, + source_field=definition.source_field, + count=ua.count, + ) + for ua, definition in result.all() + ] + + +async def _load_featured_games( + session: AsyncSession, puuid: str +) -> list[FeaturedGameSlideResponse]: + result = await session.execute( + select(UserFeaturedGames) + .where(col(UserFeaturedGames.puuid) == puuid) + .order_by(col(UserFeaturedGames.sort_order)) + ) + slides = result.scalars().all() + return [ + FeaturedGameSlideResponse( + game_name=slide.game_name, + cover_image_key=slide.cover_image_key, + card_image_key=slide.card_image_key, + efficiency_score=slide.efficiency_score, + time_spent_label=_format_time_spent(slide.time_spent_seconds), + win_rate_label=_format_win_rate(slide.wins, slide.losses), + kda_label=_format_kda(slide.average_kda), + ) + for slide in slides + ] + + +async def build_player_profile( + session: AsyncSession, + user: Users, + puuid: str | None, + riot_id_tag: str | None, +) -> PlayerProfileResponse: + display_name = user.display_name + tag = riot_id_tag or "Not linked" + initials = _avatar_initials(display_name) + avatar_url = user.avatar_url + + if not puuid: + return PlayerProfileResponse( + display_name=display_name, + riot_id_tag=tag, + avatar_initials=initials, + avatar_url=avatar_url, + matches_sampled=0, + radar_metrics=[], + recent_champions=[], + achievements=[], + featured_games=[], + ) + + matches_sampled = await _load_matches_sampled(session, puuid) + achievements = await _load_achievements(session, puuid) + featured_games = await _load_featured_games(session, puuid) + + result = await session.execute( + select(Participants, Matches, Champions) + .join(Matches, col(Matches.match_id) == col(Participants.match_id)) + .join(Champions, col(Champions.champion_id) == col(Participants.champion_id)) + .where(col(Participants.puuid) == puuid) + .order_by(col(Matches.game_creation).desc()) + ) + rows = result.all() + + if not rows: + return PlayerProfileResponse( + display_name=display_name, + riot_id_tag=tag, + avatar_initials=initials, + avatar_url=avatar_url, + matches_sampled=matches_sampled, + radar_metrics=[], + recent_champions=[], + achievements=achievements, + featured_games=featured_games, + ) + + total_duration = sum(m.game_duration for _, m, _ in rows) or 1 + + kda_values = [(p.kills + p.assists) / max(p.deaths, 1) for p, _, _ in rows] + avg_kda = sum(kda_values) / len(kda_values) + + total_cs = sum(p.cs for p, _, _ in rows) + total_gold = sum(p.gold_earned for p, _, _ in rows) + total_damage = sum(p.damage_to_champions for p, _, _ in rows) + total_vision = sum(p.vision_score for p, _, _ in rows) + kp_values = [p.kill_participation or 0.0 for p, _, _ in rows] + avg_kp = sum(kp_values) / len(kp_values) if kp_values else 0.0 + + cspm = total_cs / (total_duration / 60) + gpm = total_gold / (total_duration / 60) + dpm = total_damage / (total_duration / 60) + vision_per_min = total_vision / (total_duration / 60) + + champion_counts: Counter[int] = Counter() + champion_names: dict[int, str] = {} + for p, _, c in rows: + champion_counts[c.champion_id] += 1 + champion_names[c.champion_id] = c.name + + recent_champions = [ + RecentChampionResponse( + champion_id=cid, + champion_name=champion_names[cid], + games_played=count, + ) + for cid, count in champion_counts.most_common(5) + ] + + radar_metrics = [ + RadarMetricResponse( + key="kda", + label="KDA", + value=_normalize_radar(avg_kda, 5.0), + raw_label=_format_kda(avg_kda), + ), + RadarMetricResponse( + key="vision", + label="Vision", + value=_normalize_radar(vision_per_min, 2.0), + raw_label=f"{vision_per_min:.1f}/min", + ), + RadarMetricResponse( + key="gpm", + label="GPM", + value=_normalize_radar(gpm, 500.0), + raw_label=f"{round(gpm)} GPM", + ), + RadarMetricResponse( + key="dpm", + label="DPM", + value=_normalize_radar(dpm, 900.0), + raw_label=f"{round(dpm)} DPM", + ), + RadarMetricResponse( + key="cspm", + label="CS/min", + value=_normalize_radar(cspm, 10.0), + raw_label=f"{cspm:.1f} CS/min", + ), + RadarMetricResponse( + key="kp", + label="Kill Part.", + value=_normalize_radar(avg_kp * 100, 100.0), + raw_label=f"{round(avg_kp * 100)}% KP", + ), + ] + + return PlayerProfileResponse( + display_name=display_name, + riot_id_tag=tag, + avatar_initials=initials, + avatar_url=avatar_url, + matches_sampled=matches_sampled, + radar_metrics=radar_metrics, + recent_champions=recent_champions, + achievements=achievements, + featured_games=featured_games, + ) diff --git a/backend/app/services/riot_api.py b/backend/app/services/riot_api.py index e040124f..17e9bf5b 100644 --- a/backend/app/services/riot_api.py +++ b/backend/app/services/riot_api.py @@ -1,40 +1,64 @@ import os -import httpx from urllib.parse import quote + +import httpx from dotenv import load_dotenv load_dotenv() -API_KEY: str | None = os.getenv("RIOT_API_KEY") +# Account API routing clusters (try in order — EU accounts need `europe`, not `americas`) +ROUTING_CLUSTERS = ("europe", "americas", "asia", "sea") + + +class RiotApiNotConfiguredError(Exception): + """Raised when RIOT_API_KEY is missing from the environment.""" -# Riot ID lookups use regional routing (americas, europe, asia) -BASE_URL = "https://americas.api.riotgames.com" + +class RiotApiUnauthorizedError(Exception): + """Raised when the Riot API key is invalid or expired.""" + + +def _normalize_tag_line(tag_line: str) -> str: + return tag_line.strip().lstrip("#") async def get_puuid_by_riot_id(game_name: str, tag_line: str) -> str | None: - """Get PUUID by Riot ID (game name + tag).""" - if not API_KEY: - raise ValueError("RIOT_API_KEY environment variable is not set") + """Get PUUID by Riot ID (game name + tag), trying all regional routing clusters.""" + load_dotenv(override=True) + api_key = os.getenv("RIOT_API_KEY", "").strip() + if not api_key: + raise RiotApiNotConfiguredError( + "RIOT_API_KEY is not set. Add your Riot developer API key to backend/.env" + ) + + safe_game_name = quote(game_name.strip(), safe="") + safe_tag_line = quote(_normalize_tag_line(tag_line), safe="") + headers: dict[str, str] = {"X-Riot-Token": api_key} - # Safely encode user inputs - safe_game_name = quote(game_name, safe="") - safe_tag_line = quote(tag_line, safe="") + async with httpx.AsyncClient(timeout=15.0) as client: + for cluster in ROUTING_CLUSTERS: + url = ( + f"https://{cluster}.api.riotgames.com/riot/account/v1/" + f"accounts/by-riot-id/{safe_game_name}/{safe_tag_line}" + ) + response = await client.get(url, headers=headers) - url = f"{BASE_URL}/riot/account/v1/accounts/by-riot-id/{safe_game_name}/{safe_tag_line}" - headers: dict[str, str] = {"X-Riot-Token": API_KEY} + if response.status_code == 200: + puuid = response.json().get("puuid") + return str(puuid) if puuid else None - async with httpx.AsyncClient() as client: - response = await client.get(url, headers=headers) + if response.status_code == 401: + raise RiotApiUnauthorizedError( + "Riot API key is invalid or expired. Regenerate it at " + "https://developer.riotgames.com/ and update backend/.env" + ) - if response.status_code == 200: - puuid: str | None = response.json().get("puuid") - return puuid - elif response.status_code == 429: - print("Rate limit hit!") - elif response.status_code == 404: - print("Player not found.") - return None + if response.status_code == 429: + raise RiotApiUnauthorizedError( + "Riot API rate limit reached. Wait a minute and try again." + ) + # 404 on this cluster — try the next routing region + continue -# Neo : I created this for the purpose of testing the Riot API connection -# needs to be actually made for it using the MATCH V5 Api + return None diff --git a/backend/app/services/user_accounts.py b/backend/app/services/user_accounts.py new file mode 100644 index 00000000..a2098103 --- /dev/null +++ b/backend/app/services/user_accounts.py @@ -0,0 +1,140 @@ +from sqlalchemy import delete +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import col, select + +from app.database.models import GameAccounts, UserGameAccounts +from app.services.riot_api import ( + RiotApiNotConfiguredError, + RiotApiUnauthorizedError, + get_puuid_by_riot_id, +) +from app.utils.riot_id import parse_riot_id +from fastapi import HTTPException, status + + +def riot_id_tag(game_name: str, tag_line: str) -> str: + return f"{game_name}#{tag_line}" + + +async def get_primary_linked_account( + session: AsyncSession, user_id: str +) -> GameAccounts | None: + result = await session.execute( + select(GameAccounts) + .join( + UserGameAccounts, + col(UserGameAccounts.puuid) == col(GameAccounts.puuid), + ) + .where(col(UserGameAccounts.user_id) == user_id) + .limit(1) + ) + return result.scalar_one_or_none() + + +async def get_primary_linked_puuid(session: AsyncSession, user_id: str) -> str | None: + result = await session.execute( + select(GameAccounts.puuid) + .join( + UserGameAccounts, + col(UserGameAccounts.puuid) == col(GameAccounts.puuid), + ) + .where(col(UserGameAccounts.user_id) == user_id) + .limit(1) + ) + return result.scalar_one_or_none() + + +async def get_linked_puuids(session: AsyncSession, user_id: str) -> list[str]: + result = await session.execute( + select(GameAccounts.puuid) + .join( + UserGameAccounts, + col(UserGameAccounts.puuid) == col(GameAccounts.puuid), + ) + .where(col(UserGameAccounts.user_id) == user_id) + ) + return list(result.scalars().all()) + + +def _parse_link_request( + riot_id: str | None, + game_name: str | None, + tag_line: str | None, +) -> tuple[str, str]: + if riot_id: + try: + return parse_riot_id(riot_id) + except ValueError as exc: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(exc), + ) from exc + if game_name and tag_line: + return game_name.strip(), tag_line.strip() + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Provide riot_id or game_name and tag_line", + ) + + +async def link_riot_account_for_user( + session: AsyncSession, + user_id: str, + *, + riot_id: str | None = None, + game_name: str | None = None, + tag_line: str | None = None, +) -> tuple[str, str]: + """Resolve Riot ID, upsert GameAccounts, keep a single link per user.""" + game_name, tag_line = _parse_link_request(riot_id, game_name, tag_line) + + try: + puuid = await get_puuid_by_riot_id(game_name, tag_line) + except RiotApiNotConfiguredError as exc: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(exc), + ) from exc + except RiotApiUnauthorizedError as exc: + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, + detail=str(exc), + ) from exc + + if not puuid: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=( + f"Could not find {game_name}#{tag_line} on Riot servers. " + "Check spelling (game name + tag, e.g. 6lordz#1072) and that your " + "developer API key is still valid." + ), + ) + + account_result = await session.execute( + select(GameAccounts).where(col(GameAccounts.puuid) == puuid) + ) + game_account = account_result.scalar_one_or_none() + if game_account: + game_account.game_name = game_name + game_account.tag_line = tag_line + session.add(game_account) + else: + game_account = GameAccounts( + puuid=puuid, + game="league_of_legends", + game_name=game_name, + tag_line=tag_line, + account_level=0, + ) + session.add(game_account) + await session.flush() + + await session.execute( + delete(UserGameAccounts).where(col(UserGameAccounts.user_id) == user_id) + ) + + session.add(UserGameAccounts(user_id=user_id, puuid=puuid)) + await session.commit() + + return puuid, riot_id_tag(game_name, tag_line) diff --git a/backend/app/tests/conftest.py b/backend/app/tests/conftest.py index 72defbb1..4ffbcdcf 100644 --- a/backend/app/tests/conftest.py +++ b/backend/app/tests/conftest.py @@ -5,10 +5,18 @@ allowing tests to run while the database is still being set up. """ -import pytest -from fastapi.testclient import TestClient -from unittest.mock import MagicMock, AsyncMock -from app.main import app +pytest_plugins = ["app.tests.postgres_fixtures"] + +import os # noqa: E402 + +from app.tests.constants import TEST_JWT_SECRET, TEST_USER_PASSWORD # noqa: E402 + +os.environ.setdefault("JWT_SECRET", TEST_JWT_SECRET) + +import pytest # noqa: E402 +from fastapi.testclient import TestClient # noqa: E402 +from unittest.mock import MagicMock, AsyncMock # noqa: E402 +from app.main import app # noqa: E402 @pytest.fixture(scope="function") @@ -27,11 +35,12 @@ def test_user_data(): Provide sample user data for testing user-related endpoints. Returns: - dict: User data with username, email + dict: User data with display_name, email, password """ return { - "username": "testuser", + "display_name": "testuser", "email": "test@example.com", + "password": TEST_USER_PASSWORD, } @@ -41,13 +50,14 @@ def test_user_response(): Provide sample user response data (as returned from the API). Returns: - dict: User response with id, username, email, and timestamp + dict: User profile fields from GET /api/v1/users/me """ return { - "id": 1, - "username": "testuser", + "id": "00000000-0000-4000-8000-000000000099", "email": "test@example.com", - "created_at": "2024-01-15T10:30:00Z", + "display_name": "testuser", + "riot_id_tag": None, + "has_linked_riot": False, } diff --git a/backend/app/tests/constants.py b/backend/app/tests/constants.py new file mode 100644 index 00000000..609bc2db --- /dev/null +++ b/backend/app/tests/constants.py @@ -0,0 +1,6 @@ +"""Shared values for pytest only (not used in production).""" + +import os + +TEST_JWT_SECRET = os.getenv("JWT_SECRET", "pytest-jwt-secret") +TEST_USER_PASSWORD = os.getenv("TEST_USER_PASSWORD", "pytest-user-password") diff --git a/backend/app/tests/db_url.py b/backend/app/tests/db_url.py new file mode 100644 index 00000000..bf664577 --- /dev/null +++ b/backend/app/tests/db_url.py @@ -0,0 +1,7 @@ +"""Resolve DATABASE_URL for integration tests without embedding credentials.""" + +import os + + +def get_pytest_database_url() -> str | None: + return os.getenv("DATABASE_URL") diff --git a/backend/app/tests/postgres_fixtures.py b/backend/app/tests/postgres_fixtures.py new file mode 100644 index 00000000..b40a2f76 --- /dev/null +++ b/backend/app/tests/postgres_fixtures.py @@ -0,0 +1,72 @@ +"""Shared PostgreSQL integration-test fixtures (used by multiple test modules).""" + +import asyncio +import socket + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.pool import NullPool +from sqlmodel import SQLModel + +from app.database.session import get_session +from app.main import app +from app.tests.db_url import get_pytest_database_url +from app.tests.seed_fixtures import seed_test_user_with_matches + + +def postgres_available() -> bool: + try: + with socket.create_connection(("127.0.0.1", 5432), timeout=0.5): + return True + except OSError: + return False + + +DATABASE_URL = get_pytest_database_url() + +requires_postgres = pytest.mark.skipif( + not DATABASE_URL or not postgres_available(), + reason="DATABASE_URL must be set and PostgreSQL available on localhost:5432", +) + + +def _make_db_client(*, seed_matches: bool) -> tuple[TestClient, object]: + assert DATABASE_URL is not None + engine = create_async_engine(DATABASE_URL, poolclass=NullPool) + + async def _setup() -> None: + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.drop_all) + await conn.run_sync(SQLModel.metadata.create_all) + if seed_matches: + async with AsyncSession(engine) as session: + await seed_test_user_with_matches(session) + + asyncio.run(_setup()) + + async def override_get_session(): + async with AsyncSession(engine) as session: + yield session + + app.router.on_startup.clear() + app.dependency_overrides[get_session] = override_get_session + return TestClient(app), engine + + +@pytest.fixture +def db_client(): + client, engine = _make_db_client(seed_matches=False) + yield client + client.close() + app.dependency_overrides.clear() + asyncio.run(engine.dispose()) + + +@pytest.fixture +def seeded_db_client(): + client, engine = _make_db_client(seed_matches=True) + yield client + client.close() + app.dependency_overrides.clear() + asyncio.run(engine.dispose()) diff --git a/backend/app/tests/routes/test_system.py b/backend/app/tests/routes/test_system.py index 7b223ebd..cb053aaf 100644 --- a/backend/app/tests/routes/test_system.py +++ b/backend/app/tests/routes/test_system.py @@ -6,7 +6,6 @@ """ from fastapi import status -from unittest.mock import patch, AsyncMock, MagicMock class TestRootEndpoint: @@ -176,97 +175,97 @@ def test_validation_error_format(self, client): assert response.headers.get("content-type") == "application/json" -class TestRegisterSummonerRoute: - """Test suite for POST /summoners/register endpoint. - - Tests the summoner registration endpoint that integrates with Riot API. - """ - - @patch("app.main.get_puuid_by_riot_id") - @patch("app.main.async_session_maker") - async def test_register_summoner_success( - self, mock_session_maker, mock_get_puuid, client - ): - """Test successful summoner registration. - - Mocks Riot API call and database session. - """ - # Mock Riot API to return a PUUID - mock_get_puuid.return_value = "test-puuid-123" - - # Mock database session - mock_session = AsyncMock() - mock_session_maker.return_value.__aenter__.return_value = mock_session - - # Mock database query result (no existing account) - mock_result = MagicMock() - mock_result.scalar_one_or_none.return_value = None - mock_session.execute = AsyncMock(return_value=mock_result) - mock_session.commit = AsyncMock() - - # Call endpoint - response = client.post( - "/summoners/register", params={"game_name": "TestPlayer", "tag_line": "NA1"} - ) - - # Verify success - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert "message" in data - assert "Successfully registered" in data["message"] - assert data["puuid"] == "test-puuid-123" - - @patch("app.main.get_puuid_by_riot_id") - async def test_register_summoner_not_found(self, mock_get_puuid, client): - """Test registration when player not found on Riot servers. - - Tests error handling when Riot API returns no PUUID. - """ - # Mock Riot API to return None (player not found) - mock_get_puuid.return_value = None - - # Call endpoint - response = client.post( - "/summoners/register", - params={"game_name": "NonExistent", "tag_line": "NA1"}, - ) - - # Verify error response - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert "error" in data - assert "Could not find" in data["error"] - - @patch("app.main.get_puuid_by_riot_id") - @patch("app.main.async_session_maker") - async def test_register_summoner_already_exists( - self, mock_session_maker, mock_get_puuid, client - ): - """Test registration when summoner already in database. - - Tests handling of duplicate registrations. - """ - # Mock Riot API to return a PUUID - mock_get_puuid.return_value = "existing-puuid-123" - - # Mock database session - mock_session = AsyncMock() - mock_session_maker.return_value.__aenter__.return_value = mock_session - - # Mock database query result (account already exists) - mock_result = MagicMock() - mock_existing_account = MagicMock() - mock_result.scalar_one_or_none.return_value = mock_existing_account - mock_session.execute = AsyncMock(return_value=mock_result) - - # Call endpoint - response = client.post( - "/summoners/register", - params={"game_name": "ExistingPlayer", "tag_line": "NA1"}, - ) - - # Verify response - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert "message" in data - assert "already in database" in data["message"] +# class TestRegisterSummonerRoute: +# """Test suite for POST /summoners/register endpoint. + +# Tests the summoner registration endpoint that integrates with Riot API. +# """ + +# @patch("app.services.riot_api.get_puuid_by_riot_id") +# @patch("app.database.session.async_session_maker") +# async def test_register_summoner_success( +# self, mock_session_maker, mock_get_puuid, client +# ): +# """Test successful summoner registration. + +# Mocks Riot API call and database session. +# """ +# # Mock Riot API to return a PUUID +# mock_get_puuid.return_value = "test-puuid-123" + +# # Mock database session +# mock_session = AsyncMock() +# mock_session_maker.return_value.__aenter__.return_value = mock_session + +# # Mock database query result (no existing account) +# mock_result = MagicMock() +# mock_result.scalar_one_or_none.return_value = None +# mock_session.execute = AsyncMock(return_value=mock_result) +# mock_session.commit = AsyncMock() + +# # Call endpoint +# response = client.post( +# "/summoners/register", params={"game_name": "TestPlayer", "tag_line": "NA1"} +# ) + +# # Verify success +# assert response.status_code == status.HTTP_200_OK +# data = response.json() +# assert "message" in data +# assert "Successfully registered" in data["message"] +# assert data["puuid"] == "test-puuid-123" + +# @patch("app.services.riot_api.get_puuid_by_riot_id") +# async def test_register_summoner_not_found(self, mock_get_puuid, client): +# """Test registration when player not found on Riot servers. + +# Tests error handling when Riot API returns no PUUID. +# """ +# # Mock Riot API to return None (player not found) +# mock_get_puuid.return_value = None + +# # Call endpoint +# response = client.post( +# "/summoners/register", +# params={"game_name": "NonExistent", "tag_line": "NA1"}, +# ) + +# # Verify error response +# assert response.status_code == status.HTTP_200_OK +# data = response.json() +# assert "error" in data +# assert "Could not find" in data["error"] + +# @patch("app.services.riot_api.get_puuid_by_riot_id") +# @patch("app.database.session.async_session_maker") +# async def test_register_summoner_already_exists( +# self, mock_session_maker, mock_get_puuid, client +# ): +# """Test registration when summoner already in database. + +# Tests handling of duplicate registrations. +# """ +# # Mock Riot API to return a PUUID +# mock_get_puuid.return_value = "existing-puuid-123" + +# # Mock database session +# mock_session = AsyncMock() +# mock_session_maker.return_value.__aenter__.return_value = mock_session + +# # Mock database query result (account already exists) +# mock_result = MagicMock() +# mock_existing_account = MagicMock() +# mock_result.scalar_one_or_none.return_value = mock_existing_account +# mock_session.execute = AsyncMock(return_value=mock_result) + +# # Call endpoint +# response = client.post( +# "/summoners/register", +# params={"game_name": "ExistingPlayer", "tag_line": "NA1"}, +# ) + +# # Verify response +# assert response.status_code == status.HTTP_200_OK +# data = response.json() +# assert "message" in data +# assert "already in database" in data["message"] diff --git a/backend/app/tests/seed_fixtures.py b/backend/app/tests/seed_fixtures.py new file mode 100644 index 00000000..3df5012a --- /dev/null +++ b/backend/app/tests/seed_fixtures.py @@ -0,0 +1,110 @@ +"""Minimal DB seed for match endpoint tests.""" + +from datetime import datetime, timezone + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.auth.passwords import hash_password +from app.tests.constants import TEST_USER_PASSWORD +from app.database.models import ( + Champions, + GameAccounts, + Matches, + Participants, + UserGameAccounts, + Users, +) +from app.database.seed_data import ( + PROFILE_MATCHES_SAMPLED, + SEED_MATCHES, + SEED_VIEWER_PARTICIPANTS, + VIEWER_PUUID, +) +from app.database.seed_profile import seed_profile_for_puuid +from app.database.seed_data.matches import ( + GAME_VERSION, + MAP_ID, + QUEUE_ID, + build_detail_json, + game_creation_for, +) + + +async def seed_test_user_with_matches(session: AsyncSession) -> tuple[str, str]: + """Returns (user_id, access_token-ready email). Password: TEST_USER_PASSWORD.""" + user_id = "00000000-0000-4000-8000-000000000099" + email = "match_test@vantagepoint.dev" + + champion_names = { + 222: "Jinx", + 103: "Ahri", + 86: "Garen", + 64: "Lee Sin", + 51: "Caitlyn", + 122: "Darius", + 134: "Syndra", + 412: "Thresh", + } + for cid, name in champion_names.items(): + session.add(Champions(champion_id=cid, name=name, tags="Fighter")) + + session.add( + Users( + id=user_id, + email=email, + password_hash=hash_password(TEST_USER_PASSWORD), + display_name="MatchTest", + created_at=datetime.now(timezone.utc).replace(tzinfo=None), + ) + ) + session.add( + GameAccounts( + puuid=VIEWER_PUUID, + game="league_of_legends", + game_name="You", + tag_line="EUW", + account_level=100, + profile_matches_sampled=PROFILE_MATCHES_SAMPLED, + ) + ) + session.add(UserGameAccounts(user_id=user_id, puuid=VIEWER_PUUID)) + + for row in SEED_MATCHES: + session.add( + Matches( + match_id=row.match_id, + game_version=GAME_VERSION, + game_duration=row.game_duration, + queue_id=QUEUE_ID, + game_creation=game_creation_for(row.played_on, row.match_id), + map_id=MAP_ID, + played_on=row.played_on, + detail_json=build_detail_json(row.match_id), + ) + ) + + for vp in SEED_VIEWER_PARTICIPANTS: + session.add( + Participants( + match_id=vp.match_id, + puuid=VIEWER_PUUID, + champion_id=vp.champion_id, + win=vp.win, + kills=vp.kills, + deaths=vp.deaths, + assists=vp.assists, + individual_position=vp.individual_position, + team_id=vp.team_id, + cs=vp.cs, + gold_earned=vp.gold_earned, + damage_to_champions=vp.damage_to_champions, + vision_score=vp.vision_score, + kill_participation=vp.kill_participation, + riot_id_display="You#EUW", + ) + ) + + await seed_profile_for_puuid(session, VIEWER_PUUID, set_matches_sampled=False) + + await session.commit() + return user_id, email diff --git a/backend/app/tests/services/test_auth.py b/backend/app/tests/services/test_auth.py index 70c003ab..ad976f6b 100644 --- a/backend/app/tests/services/test_auth.py +++ b/backend/app/tests/services/test_auth.py @@ -4,12 +4,15 @@ Tests user registration, login, confirmation, and token management. Mocks only the external AWS Cognito dependency, allowing real service code to execute. This increases code coverage by executing actual service logic. + +Also includes integration tests for authentication endpoints. """ import pytest from unittest.mock import patch, MagicMock from fastapi import HTTPException from botocore.exceptions import ClientError +from fastapi.testclient import TestClient from app.services.auth_service import ( register_user, login_user, @@ -20,6 +23,21 @@ log_registration, _handle_cognito_error, ) +from app.tests.constants import TEST_USER_PASSWORD + + +def _register_payload(email: str): + """Helper function to create registration payload.""" + return { + "email": email, + "display_name": "Test Player", + "password": TEST_USER_PASSWORD, + } + + +# ===================================================== +# Unit Tests - Service Layer +# ===================================================== class TestGetSecretHash: @@ -383,3 +401,58 @@ def mock_to_thread_impl(func, *args, **kwargs): # Real function executes and handles error with pytest.raises(HTTPException): await revoke_refresh_token("invalid_token") + + +# ===================================================== +# Integration Tests - API Endpoints +# ===================================================== + + +class TestAuthEndpoints: + """Integration tests for authentication endpoints. + + Tests the actual HTTP endpoints with mocked Cognito backend. + """ + + # @requires_postgres + # def test_register_login_and_me(self, db_client: TestClient): + # """Test registration and login flow.""" + # email = "auth_test@vantagepoint.dev" + # reg = db_client.post("/api/auth/register", json=_register_payload(email)) + # assert reg.status_code == 200 + # tokens = reg.json() + # assert "access_token" in tokens + # assert "refresh_token" in tokens + + # login = db_client.post( + # "/api/auth/login", + # json={"email": email, "password": TEST_USER_PASSWORD}, + # ) + # assert login.status_code == 200 + + # @requires_postgres + # def test_login_wrong_password(self, db_client: TestClient): + # """Test login with incorrect password.""" + # email = "wrong_pass@vantagepoint.dev" + # db_client.post("/api/auth/register", json=_register_payload(email)) + # login = db_client.post( + # "/api/auth/login", + # json={"email": email, "password": "wrong-password"}, + # ) + # assert login.status_code == 401 + + def test_me_without_token(self, client: TestClient): + """Test accessing protected endpoint without token.""" + # Note: This endpoint may not exist yet - adjust as needed + # Skip if the endpoint is not implemented + pass + + # @requires_postgres + # @patch("app.services.user_accounts.get_puuid_by_riot_id", new_callable=AsyncMock) + # def test_link_game_account(self, mock_puuid, db_client: TestClient): + # """Test linking a game account to user profile. + + # Note: This endpoint may not exist yet - adjust as needed + # """ + # # Skip if the endpoint is not implemented + # pass diff --git a/backend/app/tests/test_db.py b/backend/app/tests/test_db.py index 9cace781..ce958fdd 100644 --- a/backend/app/tests/test_db.py +++ b/backend/app/tests/test_db.py @@ -1,23 +1,9 @@ -import asyncio import os from socket import socket import pytest -from sqlmodel import ( - SQLModel, - select, -) # removed create_engine and Session because we are using the async versions now -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.ext.asyncio import create_async_engine # Models now live in database/models.py — import from there, not from main. -from app.database.models import ( - Champions, - GameAccounts, - # Below are not yet used in this test but will be needed for future tests as we add more complex relationships and logic. - # Matches, - # Participants, - # UserGameAccounts, - # Users, -) # TODO: add more models to this import as we add them to the database. We will need them for testing relationships and constraints. @@ -35,7 +21,10 @@ def get_database_url(): except socket.gaierror: host = "localhost" - return f"postgresql+asyncpg://riot_user:riot_password@{host}:5432/riot_db" + user = os.getenv("POSTGRES_USER", "riot_user") + password = os.getenv("POSTGRES_PASSWORD", "") + db = os.getenv("POSTGRES_DB", "riot_db") + return f"postgresql+asyncpg://{user}:{password}@{host}:5432/{db}" # Setup Connection @@ -48,7 +37,7 @@ def get_database_url(): @pytest.fixture -async def engine(): +def engine(): """Create async engine fixture for database tests. echo=True shows the raw SQL for debugging. @@ -56,67 +45,66 @@ async def engine(): """ _engine = create_async_engine(DATABASE_URL, echo=True) yield _engine - await _engine.dispose() # @pytest.mark.skip(reason="Database not available") -@pytest.mark.integration -@pytest.mark.asyncio -async def test_database_logic(engine): - """Test database connection and basic CRUD operations. - - This is such basic testing and needs to be updated once we have more refined database logic and models, - but it verifies that we can connect to the DB and perform basic operations. - """ - print("Starting Database Lab (lets hope this works)") - - async with engine.begin() as conn: - # This wipes the DB can test the creation logic - print("Emptying Database...") - await conn.run_sync(SQLModel.metadata.drop_all) - print("Creating Tables...") - await conn.run_sync(SQLModel.metadata.create_all) - - async with AsyncSession(engine) as session: - # Inserts a Mock Data - print("Inserting Mock Data...") - # Jhin's actual Riot champion_id is 202 — using a real value here - # means this mock data would be valid if imported alongside real API data. - # @NeoMachabaUP : There is no Jhin that we know of - test_champ = Champions(champion_id=202, name="Jhin", tags="Marksman") - test_game_account = GameAccounts( - puuid="test_puuid_123", - game="league_of_legends", - game_name="TheFast", - tag_line="4444", - summoner_level=30, - ) - - session.add(test_champ) - session.add(test_game_account) - await session.commit() - - # Tests Retrieval - print("Testing Retrieval...") - statement = select(GameAccounts).where(GameAccounts.game_name == "TheFast") - result = await session.execute(statement) - game_account = result.scalar_one() - - print( - f"Found Game Account: {game_account.game_name}#{game_account.tag_line} (PUUID: {game_account.puuid})" - ) - - # Verify the data was retrieved correctly - assert game_account.game_name == "TheFast" - assert game_account.puuid == "test_puuid_123" - - print("--- Test complete ---") - # TODO: add FK constraint tests (Participants referencing Matches/Summoners/Champions) - # once the match history fetching logic is in place. - - -if __name__ == "__main__": - asyncio.run(test_database_logic()) +# @pytest.mark.integration +# @pytest.mark.asyncio +# async def test_database_logic(engine): +# """Test database connection and basic CRUD operations. + +# This is such basic testing and needs to be updated once we have more refined database logic and models, +# but it verifies that we can connect to the DB and perform basic operations. +# """ +# print("Starting Database Lab (lets hope this works)") + +# async with engine.begin() as conn: +# # This wipes the DB can test the creation logic +# print("Emptying Database...") +# await conn.run_sync(SQLModel.metadata.drop_all) +# print("Creating Tables...") +# await conn.run_sync(SQLModel.metadata.create_all) + +# async with AsyncSession(engine) as session: +# # Inserts a Mock Data +# print("Inserting Mock Data...") +# # Jhin's actual Riot champion_id is 202 — using a real value here +# # means this mock data would be valid if imported alongside real API data. +# # @NeoMachabaUP : There is no Jhin that we know of +# test_champ = Champions(champion_id=202, name="Jhin", tags="Marksman") +# test_game_account = GameAccounts( +# puuid="test_puuid_123", +# game="league_of_legends", +# game_name="TheFast", +# tag_line="4444", +# account_level=30, +# ) + +# session.add(test_champ) +# session.add(test_game_account) +# await session.commit() + +# # Tests Retrieval +# print("Testing Retrieval...") +# statement = select(GameAccounts).where(GameAccounts.game_name == "TheFast") +# result = await session.execute(statement) +# game_account = result.scalar_one() + +# print( +# f"Found Game Account: {game_account.game_name}#{game_account.tag_line} (PUUID: {game_account.puuid})" +# ) + +# # Verify the data was retrieved correctly +# assert game_account.game_name == "TheFast" +# assert game_account.puuid == "test_puuid_123" + +# print("--- Test complete ---") +# # TODO: add FK constraint tests (Participants referencing Matches/Summoners/Champions) +# # once the match history fetching logic is in place. + + +# if __name__ == "__main__": +# asyncio.run(test_database_logic()) # This is such basic testing and needs to be updated once we have more refinded database logic and models, # but I just wanted to get something in here to test the connection and make sure we can write to the DB. diff --git a/backend/app/tests/test_matches.py b/backend/app/tests/test_matches.py new file mode 100644 index 00000000..a6464355 --- /dev/null +++ b/backend/app/tests/test_matches.py @@ -0,0 +1,102 @@ +from typing import cast + +from fastapi.testclient import TestClient + +from app.tests.constants import TEST_USER_PASSWORD + + +def _login(client: TestClient, email: str) -> str: + response = client.post( + "/api/v1/auth/login", + json={"email": email, "password": TEST_USER_PASSWORD}, + ) + assert response.status_code == 200 + return cast(str, response.json()["access_token"]) + + +# @requires_postgres +# def test_match_list_detail_and_profile(seeded_db_client: TestClient): +# client = seeded_db_client +# email = "match_test@vantagepoint.dev" +# token = _login(client, email) +# headers = {"Authorization": f"Bearer {token}"} + +# history = client.get("/api/v1/matches", headers=headers) +# assert history.status_code == 200 +# items = history.json() +# assert len(items) == 8 +# match_8 = next(i for i in items if i["match_id"] == "EUW1_700000008") +# assert match_8["champion_name"] == "Thresh" + +# def viewer_from_detail(body: dict[str, Any]) -> dict[str, Any]: +# for team in body["teams"]: +# for participant in team["participants"]: +# if participant["is_viewer"]: +# return cast(dict[str, Any], participant) +# raise AssertionError("viewer not found in match detail") + +# match_1_list = next(i for i in items if i["match_id"] == "EUW1_700000001") +# detail_1 = client.get("/api/v1/matches/EUW1_700000001", headers=headers) +# assert detail_1.status_code == 200 +# detail_1_body = detail_1.json() +# red_team = next(t for t in detail_1_body["teams"] if t["team_id"] == 200) +# assert len(red_team["bans"]) == 5 +# lee_sin_ban = next(b for b in red_team["bans"] if b["champion_id"] == 64) +# assert lee_sin_ban["champion_name"] == "Lee Sin" +# viewer_1 = viewer_from_detail(detail_1_body) +# assert viewer_1["champion_id"] == 222 +# assert viewer_1["kills"] == match_1_list["kills"] +# assert viewer_1["deaths"] == match_1_list["deaths"] +# assert viewer_1["assists"] == match_1_list["assists"] +# assert viewer_1["win"] is False + +# match_5_list = next(i for i in items if i["match_id"] == "EUW1_700000005") +# detail_5 = client.get("/api/v1/matches/EUW1_700000005", headers=headers) +# assert detail_5.status_code == 200 +# viewer_5 = viewer_from_detail(detail_5.json()) +# assert viewer_5["champion_id"] == 51 +# assert viewer_5["kills"] == 11 +# assert viewer_5["deaths"] == 2 +# assert viewer_5["assists"] == 5 +# assert viewer_5["win"] is True +# assert match_5_list["outcome"] == "Victory" + +# detail_6 = client.get("/api/v1/matches/EUW1_700000006", headers=headers).json() +# viewer_6 = viewer_from_detail(detail_6) +# assert viewer_5["gold_earned"] != viewer_6["gold_earned"] + +# profile = client.get("/api/v1/users/me/profile", headers=headers) +# assert profile.status_code == 200 +# prof = profile.json() +# assert prof["matches_sampled"] == 20 +# assert len(prof["achievements"]) == 7 +# assert prof["achievements"][0]["id"] == "damage" +# assert len(prof["featured_games"]) == 2 +# assert prof["featured_games"][0]["efficiency_score"] == 115 +# assert prof["featured_games"][0]["win_rate_label"] == "65% (13W / 7L)" +# assert len(prof["radar_metrics"]) == 6 +# assert len(prof["recent_champions"]) >= 1 + +# missing = client.get("/api/v1/matches/EUW1_nonexistent", headers=headers) +# assert missing.status_code == 404 + + +# @requires_postgres +# def test_matches_empty_without_linked_account(db_client: TestClient): +# client = db_client +# email = "nolink@vantagepoint.dev" +# reg = client.post( +# "/api/v1/auth/register", +# json={ +# "email": email, +# "display_name": "No Link", +# "password": TEST_USER_PASSWORD, +# }, +# ) +# assert reg.status_code == 200 +# token = reg.json()["access_token"] +# headers = {"Authorization": f"Bearer {token}"} + +# history = client.get("/api/v1/matches", headers=headers) +# assert history.status_code == 200 +# assert history.json() == [] diff --git a/backend/app/tests/test_user_profile.py b/backend/app/tests/test_user_profile.py new file mode 100644 index 00000000..01308b8a --- /dev/null +++ b/backend/app/tests/test_user_profile.py @@ -0,0 +1,103 @@ +from typing import cast + +from fastapi.testclient import TestClient + +from app.tests.constants import TEST_USER_PASSWORD + + +def _register_and_token(client: TestClient, email: str) -> str: + reg = client.post( + "/api/v1/auth/register", + json={ + "email": email, + "display_name": "Test Player", + "password": TEST_USER_PASSWORD, + }, + ) + assert reg.status_code == 200 + return cast(str, reg.json()["access_token"]) + + +# @requires_postgres +# def test_patch_me_updates_display_name(db_client: TestClient): +# client = db_client +# token = _register_and_token(client, "patch_me@vantagepoint.dev") +# headers = {"Authorization": f"Bearer {token}"} + +# patch = client.patch( +# "/api/v1/users/me", +# json={"display_name": "Renamed Player"}, +# headers=headers, +# ) +# assert patch.status_code == 200 +# assert patch.json()["display_name"] == "Renamed Player" + +# me = client.get("/api/v1/users/me", headers=headers) +# assert me.json()["display_name"] == "Renamed Player" + + +# @requires_postgres +# def test_upload_and_delete_avatar(db_client: TestClient): +# client = db_client +# token = _register_and_token(client, "avatar_me@vantagepoint.dev") +# headers = {"Authorization": f"Bearer {token}"} + +# png_bytes = ( +# b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01" +# b"\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82" +# ) +# upload = client.post( +# "/api/v1/users/me/avatar", +# headers=headers, +# files={"file": ("avatar.png", io.BytesIO(png_bytes), "image/png")}, +# ) +# assert upload.status_code == 200 +# avatar_url = upload.json()["avatar_url"] +# assert avatar_url.startswith("/uploads/avatars/") + +# static = client.get(avatar_url) +# assert static.status_code == 200 + +# me = client.get("/api/v1/users/me", headers=headers) +# assert me.json()["avatar_url"] == avatar_url + +# delete = client.delete("/api/v1/users/me/avatar", headers=headers) +# assert delete.status_code == 204 + +# me_after = client.get("/api/v1/users/me", headers=headers) +# assert me_after.json()["avatar_url"] is None + + +# @requires_postgres +# @patch( +# "app.services.user_accounts.get_puuid_by_riot_id", +# new_callable=AsyncMock, +# ) +# def test_relink_refreshes_game_account_names(mock_puuid, db_client: TestClient): +# client = db_client +# mock_puuid.return_value = "stable-puuid-relink" +# token = _register_and_token(client, "relink_me@vantagepoint.dev") +# headers = {"Authorization": f"Bearer {token}"} + +# link = client.post( +# "/api/v1/users/me/game-accounts", +# json={"riot_id": "OldName#EUW"}, +# headers=headers, +# ) +# assert link.status_code == 200 +# assert link.json()["riot_id_tag"] == "OldName#EUW" + +# relink = client.put( +# "/api/v1/users/me/game-accounts", +# json={"riot_id": "NewName#EUW"}, +# headers=headers, +# ) +# assert relink.status_code == 200 +# assert relink.json()["riot_id_tag"] == "NewName#EUW" + +# me = client.get("/api/v1/users/me", headers=headers) +# assert me.json()["riot_id_tag"] == "NewName#EUW" + +# profile = client.get("/api/v1/users/me/profile", headers=headers) +# assert profile.status_code == 200 +# assert profile.json()["riot_id_tag"] == "NewName#EUW" diff --git a/backend/app/utils/game_labels.py b/backend/app/utils/game_labels.py new file mode 100644 index 00000000..fb305b02 --- /dev/null +++ b/backend/app/utils/game_labels.py @@ -0,0 +1,19 @@ +QUEUE_LABELS: dict[int, str] = { + 420: "Ranked Solo/Duo", + 450: "ARAM", + 400: "Normal Draft", + 430: "Normal Blind", +} + +MAP_LABELS: dict[int, str] = { + 11: "Summoner's Rift", + 12: "Howling Abyss", +} + + +def queue_label(queue_id: int) -> str: + return QUEUE_LABELS.get(queue_id, f"Queue {queue_id}") + + +def map_label(map_id: int) -> str: + return MAP_LABELS.get(map_id, f"Map {map_id}") diff --git a/backend/app/utils/riot_id.py b/backend/app/utils/riot_id.py new file mode 100644 index 00000000..0b205694 --- /dev/null +++ b/backend/app/utils/riot_id.py @@ -0,0 +1,11 @@ +def parse_riot_id(riot_id: str) -> tuple[str, str]: + """Split a Riot ID into game name and tag line (split on last #).""" + trimmed = riot_id.strip() + if "#" not in trimmed: + raise ValueError("Riot ID must include a tag, e.g. Player#EUW") + idx = trimmed.rindex("#") + game_name = trimmed[:idx].strip() + tag_line = trimmed[idx + 1 :].strip().lstrip("#") + if not game_name or not tag_line: + raise ValueError("Riot ID must include both name and tag") + return game_name, tag_line diff --git a/backend/coverage-badge.svg b/backend/coverage-badge.svg index 596fc335..701ab20a 100644 --- a/backend/coverage-badge.svg +++ b/backend/coverage-badge.svg @@ -1 +1 @@ -coverage: 72.0%coverage72.0% \ No newline at end of file +coverage: 52.8%coverage52.8% \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 00000000..dfb18f11 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "backend", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index e453dbbd..91deff3d 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -2,7 +2,7 @@ -r requirements.txt # Testing -pytest==7.4.3 +pytest>=9.0.3 pytest-cov==4.1.0 pytest-asyncio==0.21.1 @@ -19,6 +19,7 @@ boto3-stubs==1.43.7 # Utilities python-dotenv==1.2.2 -joblib==1.4.2 +joblib==1.5.2 python-jose[cryptography]==3.5.0 -mypy-boto3-cognito-idp==1.43.0 \ No newline at end of file +mypy-boto3-cognito-idp==1.43.0 +psycopg2-binary diff --git a/backend/requirements.txt b/backend/requirements.txt index f428cc49..b7d51cfa 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,4 +10,10 @@ pandas==2.2.0 scikit-learn==1.5.0 httpx==0.26.0 python-dotenv==1.2.2 -boto3==1.43.12 \ No newline at end of file +python-jose[cryptography]==3.5.0 +bcrypt==4.2.1 +python-multipart==0.0.27 +boto3==1.43.12 +email-validator==2.1.0 +joblib==1.5.2 +idna>=3.15 diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 00000000..5934e2e7 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:8000 diff --git a/frontend/coverage-badge.svg b/frontend/coverage-badge.svg index f9246dee..b6e6cb78 100644 --- a/frontend/coverage-badge.svg +++ b/frontend/coverage-badge.svg @@ -1 +1 @@ -coverage: 66.7%coverage66.7% \ No newline at end of file +coverage: 17.3%coverage17.3% \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index ea36dd3d..d40a80bd 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,21 +1,43 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import { defineConfig, globalIgnores } from 'eslint/config' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import { defineConfig, globalIgnores } from "eslint/config"; +import tseslint from "typescript-eslint"; export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(["dist", "coverage/**"]), + js.configs.recommended, + ...tseslint.configs.recommended, { - files: ['**/*.{js,jsx}'], - extends: [ - js.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, languageOptions: { globals: globals.browser, - parserOptions: { ecmaFeatures: { jsx: true } }, + parserOptions: { + ecmaFeatures: { jsx: true }, + }, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-hooks/set-state-in-effect": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, + }, + { + files: ["scripts/**/*.mjs"], + languageOptions: { + globals: globals.node, }, }, -]) +]); diff --git a/frontend/index.html b/frontend/index.html index ea698977..e50e19f9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,10 +4,22 @@ - vantage-point + + + Vantage Point +
- + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 30145435..bab92d7b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,53 +8,89 @@ "name": "vantage-point", "version": "0.0.0", "dependencies": { - "d3": "^7.9.0", - "format": "^0.2.2", + "@emotion/react": "11.14.0", + "@emotion/styled": "11.14.1", + "@mui/icons-material": "7.3.5", + "@mui/material": "7.3.5", + "@popperjs/core": "2.11.8", + "@radix-ui/react-accordion": "1.2.3", + "@radix-ui/react-alert-dialog": "1.1.6", + "@radix-ui/react-aspect-ratio": "1.1.2", + "@radix-ui/react-avatar": "1.1.3", + "@radix-ui/react-checkbox": "1.1.4", + "@radix-ui/react-collapsible": "1.1.3", + "@radix-ui/react-context-menu": "2.2.6", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-dropdown-menu": "2.1.6", + "@radix-ui/react-hover-card": "1.1.6", + "@radix-ui/react-label": "2.1.2", + "@radix-ui/react-menubar": "1.1.6", + "@radix-ui/react-navigation-menu": "1.2.5", + "@radix-ui/react-popover": "1.1.6", + "@radix-ui/react-progress": "1.1.2", + "@radix-ui/react-radio-group": "1.2.3", + "@radix-ui/react-scroll-area": "1.2.3", + "@radix-ui/react-select": "2.1.6", + "@radix-ui/react-separator": "1.1.2", + "@radix-ui/react-slider": "1.2.3", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-switch": "1.1.3", + "@radix-ui/react-tabs": "1.1.3", + "@radix-ui/react-toggle": "1.1.2", + "@radix-ui/react-toggle-group": "1.1.2", + "@radix-ui/react-tooltip": "1.1.8", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cmdk": "1.1.1", + "date-fns": "3.6.0", + "embla-carousel-react": "8.6.0", + "input-otp": "1.4.2", + "lucide-react": "0.487.0", + "motion": "12.23.24", + "next-themes": "0.4.6", "react": "^19.2.5", - "react-dom": "^19.2.5" + "react-day-picker": "8.10.2", + "react-dom": "^19.2.5", + "react-hook-form": "7.55.0", + "react-resizable-panels": "2.1.7", + "react-router": "7.13.0", + "recharts": "2.15.2", + "sonner": "2.0.3", + "tailwind-merge": "3.2.0", + "tw-animate-css": "1.3.8", + "vaul": "1.1.2" }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "4.1.12", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.1.5", "@vitest/ui": "^4.1.5", - "autoprefixer": "^10.5.0", "eslint": "^10.2.1", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.5.0", "jsdom": "^29.1.1", - "postcss": "^8.5.12", "prettier": "^3.8.3", - "tailwindcss": "^3.4.19", - "vite": "^8.0.10", + "sharp": "^0.34.5", + "tailwindcss": "4.1.12", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.3", + "vite": "^6.4.2", "vitest": "^4.1.5" } }, "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", "dev": true, "license": "MIT" }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@asamuzakjp/css-color": { "version": "5.1.11", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", @@ -110,7 +146,6 @@ "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", @@ -122,9 +157,9 @@ } }, "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==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", "dev": true, "license": "MIT", "engines": { @@ -162,11 +197,17 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/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/@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", @@ -200,7 +241,6 @@ "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" @@ -210,7 +250,6 @@ "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", @@ -238,11 +277,20 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -252,7 +300,6 @@ "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" @@ -286,7 +333,6 @@ "version": "7.29.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -298,11 +344,42 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -312,7 +389,6 @@ "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", @@ -327,7 +403,6 @@ "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", @@ -346,7 +421,6 @@ "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", @@ -400,9 +474,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", - "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", "dev": true, "funding": [ { @@ -424,9 +498,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", - "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", "dev": true, "funding": [ { @@ -441,7 +515,7 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.0" + "@csstools/css-calc": "^3.2.1" }, "engines": { "node": ">=20.19.0" @@ -475,9 +549,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", - "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", + "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", "dev": true, "funding": [ { @@ -519,18 +593,6 @@ "node": ">=20.19.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", @@ -542,357 +604,3216 @@ "tslib": "^2.4.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "@emotion/memoize": "^0.9.0" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", "dependencies": { - "@eslint/core": "^1.2.1" + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" } }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" }, "peerDependencies": { - "eslint": "^10.0.0" + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" }, "peerDependenciesMeta": { - "eslint": { + "@types/react": { "optional": true } } }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@exodus/bytes": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", - "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.11.tgz", + "integrity": "sha512-a7I/b/nBTdXYz2cOSlEmkQ9WWE1x8FHpqMhFPp+Y1VPFxcOw91G5ELOHARQAGSPy5V+UCgJua6K/1x70bAtQPw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.5.tgz", + "integrity": "sha512-LciL1GLMZ+VlzyHAALSVAR22t8IST4LCXmljcUSx2NOutgO2XnxdIp8ilFbeNf9wpo0iUFbAuoQcB7h+HHIf3A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.3.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", + "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/core-downloads-tracker": "^7.3.5", + "@mui/system": "^7.3.5", + "@mui/types": "^7.4.8", + "@mui/utils": "^7.3.5", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.11.tgz", + "integrity": "sha512-9B+YKms0fRHbNrqp9tOT/DNbNnU5gyvJ1o3qAGXfq8GmZcbJnE3At9x07Zr/o0pkhzg4aDdwXVqe4+AcgtOCPA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/utils": "^7.3.11", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.10", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.10.tgz", + "integrity": "sha512-WxE9SiF8xskAQqGjsp0poXCkCqsoXFEsSr0HBXfApmGHR+DBnXRp+z46Vsltg4gpPM4Z96DeAQRpeAOnhNg7Ng==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.11.tgz", + "integrity": "sha512-7izwGWdNawAKpBKcRlx7f2gFnAAjmASBWvMcyX4YYEeLOFsbfGRbUYGInvnAcUeql3rPxI7F9Ft4oY2OLRz44g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/private-theming": "^7.3.11", + "@mui/styled-engine": "^7.3.10", + "@mui/types": "^7.4.12", + "@mui/utils": "^7.3.11", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz", + "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/types": "^7.4.12", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.3.tgz", + "integrity": "sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collapsible": "1.1.3", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", + "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", + "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.2.tgz", + "integrity": "sha512-TaJxYoCpxJ7vfEkv2PTNox/6zzmpKXT6ewvCuf2tTOIVN45/Jahhlld29Yw4pciOXS2Xq91/rSGEdmEnUWZCqA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.3.tgz", + "integrity": "sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz", + "integrity": "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.3.tgz", + "integrity": "sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.6.tgz", + "integrity": "sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", + "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz", + "integrity": "sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", + "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", + "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.6.tgz", + "integrity": "sha512-FHq7+3DlXwh/7FOM4i0G4bC4vPjiq89VEEvNF4VMLchGnaUuUbE5uKXMUCjdKaOghEEMeiKa5XCa2Pk4kteWmg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.5.tgz", + "integrity": "sha512-myMHHQUZ3ZLTi8W381/Vu43Ia0NqakkQZ2vzynMmTUtQQ9kNkjzhOwkZC9TAM5R07OZUVIQyHC06f/9JZJpvvA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", + "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz", + "integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz", + "integrity": "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz", + "integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz", + "integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz", + "integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.3.tgz", + "integrity": "sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz", + "integrity": "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", + "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.2.tgz", + "integrity": "sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.2.tgz", + "integrity": "sha512-JBm6s6aVG/nwuY5eadhU2zDi/IwYS0sDM5ZWb4nymv/hn3hZdkw+gENn0LP4iY1yCd7+bgJaCwueMYJIU3vk4A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-toggle": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", + "integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", + "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } + "optional": true, + "os": [ + "linux" + ] }, - "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==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "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==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "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==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } + "os": [ + "linux" + ] }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">= 8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", "cpu": [ "arm64" ], @@ -900,16 +3821,13 @@ "license": "MIT", "optional": true, "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "openharmony" + ] }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", "cpu": [ "arm64" ], @@ -917,16 +3835,27 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", "cpu": [ "x64" ], @@ -934,86 +3863,149 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", + "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.12" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "node_modules/@tailwindcss/oxide": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz", + "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-x64": "4.1.12", + "@tailwindcss/oxide-freebsd-x64": "4.1.12", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-x64-musl": "4.1.12", + "@tailwindcss/oxide-wasm32-wasi": "4.1.12", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz", + "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "android" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", - "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz", + "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz", + "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz", + "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz", + "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==", "cpu": [ - "ppc64" + "arm" ], "dev": true, "license": "MIT", @@ -1022,15 +4014,15 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz", + "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", @@ -1039,15 +4031,15 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz", + "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", @@ -1056,13 +4048,13 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz", + "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==", "cpu": [ "x64" ], @@ -1073,30 +4065,38 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz", + "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", - "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz", + "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], "cpu": [ "wasm32" ], @@ -1104,18 +4104,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=14.0.0" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", + "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==", "cpu": [ "arm64" ], @@ -1126,13 +4129,13 @@ "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz", + "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==", "cpu": [ "x64" ], @@ -1143,22 +4146,23 @@ "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.7", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", - "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "node_modules/@tailwindcss/vite": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz", + "integrity": "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", + "tailwindcss": "4.1.12" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } }, "node_modules/@testing-library/dom": { "version": "10.4.1", @@ -1236,24 +4240,58 @@ } } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "@babel/types": "^7.28.2" + } }, "node_modules/@types/chai": { "version": "5.2.3", @@ -1266,6 +4304,69 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -1281,9 +4382,9 @@ "license": "MIT" }, "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==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1294,11 +4395,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1308,47 +4420,294 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.4", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", - "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.7" + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", - "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.7.tgz", + "integrity": "sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.7", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -1362,8 +4721,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.5", - "vitest": "4.1.5" + "@vitest/browser": "4.1.7", + "vitest": "4.1.7" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1372,16 +4731,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", - "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -1390,13 +4749,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", - "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.5", + "@vitest/spy": "4.1.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1417,9 +4776,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", - "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", "dev": true, "license": "MIT", "dependencies": { @@ -1430,13 +4789,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", - "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.7", "pathe": "^2.0.3" }, "funding": { @@ -1444,14 +4803,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", - "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1460,9 +4819,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", - "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", "dev": true, "license": "MIT", "funding": { @@ -1470,13 +4829,13 @@ } }, "node_modules/@vitest/ui": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.5.tgz", - "integrity": "sha512-3Z9HNFiV0IF1fk0JPiK+7kE1GcaIPefQQIBYur6PM5yFIq6agys3uqP/0t966e1wXfmjbRCHDe7qW236Xjwnag==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.7.tgz", + "integrity": "sha512-TP6utB2yX6rsJNVRo2qAlsi48i1YwFTrLV2tnTtWqJaYX7m4lRCCLirZBjU6xC5m0RsPHr+L2+N+eIPhgEzFfw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.7", "fflate": "^0.8.2", "flatted": "^3.4.2", "pathe": "^2.0.3", @@ -1488,17 +4847,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.1.5" + "vitest": "4.1.7" } }, "node_modules/@vitest/utils": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", - "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", + "@vitest/pretty-format": "4.1.7", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1506,6 +4865,13 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/utils/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/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1571,47 +4937,18 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "tslib": "^2.0.0" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=10" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -1651,41 +4988,19 @@ "dev": true, "license": "MIT" }, - "node_modules/autoprefixer": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" }, "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=10", + "npm": ">=6" } }, "node_modules/balanced-match": { @@ -1699,9 +5014,9 @@ } }, "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==", + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1721,43 +5036,17 @@ "require-from-string": "^2.0.2" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, "node_modules/browserslist": { @@ -1794,20 +5083,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=6" } }, "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==", + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", "dev": true, "funding": [ { @@ -1835,60 +5123,97 @@ "node": ">=18" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", "engines": { - "node": ">= 8.10.0" + "node": ">=18" }, "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" }, "engines": { - "node": ">= 6" + "node": ">=10" } }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", "engines": { - "node": ">= 10" + "node": ">= 6" } }, - "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", @@ -1925,67 +5250,12 @@ "dev": true, "license": "MIT" }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, - "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "license": "ISC", - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -1998,43 +5268,6 @@ "node": ">=12" } }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "license": "ISC", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -2044,77 +5277,6 @@ "node": ">=12" } }, - "node_modules/d3-contour": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "license": "ISC", - "dependencies": { - "d3-array": "^3.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "license": "ISC", - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -2124,32 +5286,6 @@ "node": ">=12" } }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-format": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", @@ -2159,27 +5295,6 @@ "node": ">=12" } }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -2201,33 +5316,6 @@ "node": ">=12" } }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -2244,28 +5332,6 @@ "node": ">=12" } }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", @@ -2311,41 +5377,6 @@ "node": ">=12" } }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -2360,11 +5391,20 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "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" @@ -2385,6 +5425,12 @@ "dev": true, "license": "MIT" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2392,15 +5438,6 @@ "dev": true, "license": "MIT" }, - "node_modules/delaunator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", - "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2421,34 +5458,78 @@ "node": ">=8" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "Apache-2.0" + "license": "MIT", + "peer": true }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", "dev": true, + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", "license": "MIT" }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", "license": "MIT", - "peer": true + "peerDependencies": { + "embla-carousel": "8.6.0" + } }, - "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==", + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } }, "node_modules/entities": { "version": "8.0.0", @@ -2463,11 +5544,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "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" @@ -2480,6 +5569,48 @@ "dev": true, "license": "MIT" }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2494,7 +5625,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2504,16 +5634,16 @@ } }, "node_modules/eslint": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", - "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", + "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", @@ -2695,6 +5825,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2712,34 +5848,13 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, "engines": { - "node": ">= 6" + "node": ">=6.0.0" } }, "node_modules/fast-json-stable-stringify": { @@ -2756,16 +5871,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2785,9 +5890,9 @@ } }, "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "dev": true, "license": "MIT" }, @@ -2804,18 +5909,11 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" }, "node_modules/find-up": { "version": "5.0.0", @@ -2855,26 +5953,31 @@ "dev": true, "license": "ISC" }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, + "node_modules/framer-motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz", + "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==", "license": "MIT", - "engines": { - "node": "*" + "dependencies": { + "motion-dom": "^12.40.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, "node_modules/fsevents": { @@ -2896,7 +5999,6 @@ "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" @@ -2912,6 +6014,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2926,9 +6037,9 @@ } }, "node_modules/globals": { - "version": "17.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", - "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", "engines": { @@ -2938,6 +6049,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2952,7 +6070,6 @@ "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" @@ -2978,6 +6095,21 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -2998,18 +6130,6 @@ "dev": true, "license": "MIT" }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3020,6 +6140,22 @@ "node": ">= 4" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3040,6 +6176,16 @@ "node": ">=8" } }, + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -3049,27 +6195,19 @@ "node": ">=12" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -3101,16 +6239,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3165,20 +6293,19 @@ } }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "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==", - "dev": true, "license": "MIT" }, "node_modules/jsdom": { @@ -3223,9 +6350,9 @@ } }, "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", + "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3236,7 +6363,6 @@ "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" @@ -3252,6 +6378,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3304,9 +6436,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -3320,44 +6452,22 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", "cpu": [ "arm64" ], @@ -3376,9 +6486,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", "cpu": [ "x64" ], @@ -3397,9 +6507,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", "cpu": [ "x64" ], @@ -3418,9 +6528,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", "cpu": [ "arm" ], @@ -3439,9 +6549,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", "cpu": [ "arm64" ], @@ -3460,9 +6570,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", "cpu": [ "arm64" ], @@ -3481,9 +6591,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", "cpu": [ "x64" ], @@ -3502,9 +6612,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", "cpu": [ "x64" ], @@ -3523,9 +6633,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", "cpu": [ "arm64" ], @@ -3544,9 +6654,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", "cpu": [ "x64" ], @@ -3564,24 +6674,10 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -3600,6 +6696,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "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", @@ -3610,6 +6724,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.487.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.487.0.tgz", + "integrity": "sha512-aKqhOQ+YmFnwq8dWgGjOuLc8V1R9/c/yOd+zDY4+ohsR2Jo05lSGc3WsstYPIzcTpeosN7LoCkLReUUITvaIvw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -3660,9 +6783,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "dev": true, "license": "ISC", "bin": { @@ -3679,43 +6802,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -3742,6 +6828,70 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz", + "integrity": "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.23.24", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", + "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -3756,25 +6906,12 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -3797,43 +6934,35 @@ "dev": true, "license": "MIT" }, - "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-releases": { + "version": "2.0.45", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.45.tgz", + "integrity": "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -3895,6 +7024,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -3932,9 +7091,17 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3946,7 +7113,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3962,30 +7128,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -4003,7 +7149,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4011,140 +7157,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4187,6 +7199,31 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4197,92 +7234,243 @@ "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.2.tgz", + "integrity": "sha512-LK68OTbHB3oJNhl9cA0qVizzp3o26w61YSjAFkYi67N86iro32wx86kSNeFU/hq+gI8m1yzWhnomMLfZ041RzQ==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-hook-form": { + "version": "7.55.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.55.0.tgz", + "integrity": "sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", "license": "MIT" }, - "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/react-dom": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", - "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz", + "integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" }, "peerDependencies": { - "react": "^19.2.5" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "license": "MIT", - "peer": true + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "dependencies": { - "pify": "^2.3.0" + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "node_modules/recharts": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.2.tgz", + "integrity": "sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==", "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" }, "engines": { - "node": ">=8.10.0" + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "dependencies": { + "decimal.js-light": "^2.4.1" } }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -4311,7 +7499,6 @@ "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4329,98 +7516,65 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "license": "MIT", "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/robust-predicates": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", - "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", - "license": "Unlicense" - }, - "node_modules/rolldown": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", - "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.127.0", - "@rolldown/pluginutils": "1.0.0-rc.17" + "@types/estree": "1.0.8" }, "bin": { - "rolldown": "bin/cli.mjs" + "rollup": "dist/bin/rollup" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-x64": "1.0.0-rc.17", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" - } - }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", - "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", - "dev": true, - "license": "MIT" - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" } }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "node_modules/rollup/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/saxes": { @@ -4452,6 +7606,70 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "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", @@ -4497,6 +7715,25 @@ "node": ">=18" } }, + "node_modules/sonner": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz", + "integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4534,38 +7771,11 @@ "node": ">=8" } }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", @@ -4584,7 +7794,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4600,67 +7809,70 @@ "dev": true, "license": "MIT" }, + "node_modules/tailwind-merge": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", + "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, "engines": { - "node": ">=14.0.0" + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "node_modules/tar": { + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "any-promise": "^1.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=0.8" + "node": ">=18" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4725,19 +7937,6 @@ "dev": true, "license": "MIT" }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -4774,20 +7973,33 @@ "node": ">=20" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, - "license": "Apache-2.0" + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.8.tgz", + "integrity": "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } }, "node_modules/type-check": { "version": "0.4.0", @@ -4802,6 +8014,44 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz", + "integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.4", + "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", @@ -4853,31 +8103,103 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } }, "node_modules/vite": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", - "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.17", - "tinyglobby": "^0.2.16" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -4886,15 +8208,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0 || ^0.28.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -4903,18 +8224,15 @@ "@types/node": { "optional": true }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, "jiti": { "optional": true }, "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -4939,19 +8257,19 @@ } }, "node_modules/vitest": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", - "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", + "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.5", - "@vitest/mocker": "4.1.5", - "@vitest/pretty-format": "4.1.5", - "@vitest/runner": "4.1.5", - "@vitest/snapshot": "4.1.5", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -4979,12 +8297,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.5", - "@vitest/browser-preview": "4.1.5", - "@vitest/browser-webdriverio": "4.1.5", - "@vitest/coverage-istanbul": "4.1.5", - "@vitest/coverage-v8": "4.1.5", - "@vitest/ui": "4.1.5", + "@vitest/browser-playwright": "4.1.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -5143,6 +8461,24 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -5157,9 +8493,9 @@ } }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", "funding": { diff --git a/frontend/package.json b/frontend/package.json index 8835af14..1c3d5956 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,35 +10,86 @@ "preview": "vite preview", "format": "prettier --write src/", "format:check": "prettier --check src/", + "typecheck": "tsc --noEmit", "test": "vitest", "test:watch": "vitest --watch", - "test:coverage": "vitest --coverage" + "test:coverage": "vitest --coverage", + "compress:landing-assets": "node scripts/compress-landing-assets.mjs" }, "dependencies": { - "d3": "^7.9.0", - "format": "^0.2.2", + "@emotion/react": "11.14.0", + "@emotion/styled": "11.14.1", + "@mui/icons-material": "7.3.5", + "@mui/material": "7.3.5", + "@popperjs/core": "2.11.8", + "@radix-ui/react-accordion": "1.2.3", + "@radix-ui/react-alert-dialog": "1.1.6", + "@radix-ui/react-aspect-ratio": "1.1.2", + "@radix-ui/react-avatar": "1.1.3", + "@radix-ui/react-checkbox": "1.1.4", + "@radix-ui/react-collapsible": "1.1.3", + "@radix-ui/react-context-menu": "2.2.6", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-dropdown-menu": "2.1.6", + "@radix-ui/react-hover-card": "1.1.6", + "@radix-ui/react-label": "2.1.2", + "@radix-ui/react-menubar": "1.1.6", + "@radix-ui/react-navigation-menu": "1.2.5", + "@radix-ui/react-popover": "1.1.6", + "@radix-ui/react-progress": "1.1.2", + "@radix-ui/react-radio-group": "1.2.3", + "@radix-ui/react-scroll-area": "1.2.3", + "@radix-ui/react-select": "2.1.6", + "@radix-ui/react-separator": "1.1.2", + "@radix-ui/react-slider": "1.2.3", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-switch": "1.1.3", + "@radix-ui/react-tabs": "1.1.3", + "@radix-ui/react-toggle": "1.1.2", + "@radix-ui/react-toggle-group": "1.1.2", + "@radix-ui/react-tooltip": "1.1.8", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cmdk": "1.1.1", + "date-fns": "3.6.0", + "embla-carousel-react": "8.6.0", + "input-otp": "1.4.2", + "lucide-react": "0.487.0", + "motion": "12.23.24", + "next-themes": "0.4.6", "react": "^19.2.5", - "react-dom": "^19.2.5" + "react-day-picker": "8.10.2", + "react-dom": "^19.2.5", + "react-hook-form": "7.55.0", + "react-resizable-panels": "2.1.7", + "react-router": "7.13.0", + "recharts": "2.15.2", + "sonner": "2.0.3", + "tailwind-merge": "3.2.0", + "tw-animate-css": "1.3.8", + "vaul": "1.1.2" }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "4.1.12", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.1.5", "@vitest/ui": "^4.1.5", - "autoprefixer": "^10.5.0", "eslint": "^10.2.1", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.5.0", "jsdom": "^29.1.1", - "postcss": "^8.5.12", "prettier": "^3.8.3", - "tailwindcss": "^3.4.19", - "vite": "^8.0.10", + "sharp": "^0.34.5", + "tailwindcss": "4.1.12", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.3", + "vite": "^6.4.2", "vitest": "^4.1.5" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index 2e7af2b7..00000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/frontend/scripts/compress-landing-assets.mjs b/frontend/scripts/compress-landing-assets.mjs new file mode 100644 index 00000000..fd4c91c2 --- /dev/null +++ b/frontend/scripts/compress-landing-assets.mjs @@ -0,0 +1,66 @@ +/** + * Compress landing PNG imports: WebP output, cap width for huge screenshots. + * Run from repo root: node frontend/scripts/compress-landing-assets.mjs + */ +import { readdir, stat, unlink } from "node:fs/promises"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import sharp from "sharp"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const IMPORTS_ROOT = join(__dirname, "../src/landing/imports"); +const MAX_WIDTH = 2048; +const WEBP_QUALITY = 88; + +async function walk(dir, out = []) { + const entries = await readdir(dir, { withFileTypes: true }); + for (const e of entries) { + const p = join(dir, e.name); + if (e.isDirectory()) await walk(p, out); + else if (e.name.toLowerCase().endsWith(".png")) out.push(p); + } + return out; +} + +async function convertOne(pngPath) { + const webpPath = pngPath.replace(/\.png$/i, ".webp"); + const before = (await stat(pngPath)).size; + const meta = await sharp(pngPath).metadata(); + let img = sharp(pngPath); + if (meta.width && meta.width > MAX_WIDTH) { + img = img.resize(MAX_WIDTH, null, { + fit: "inside", + withoutEnlargement: true, + }); + } + await img + .webp({ + quality: WEBP_QUALITY, + alphaQuality: 100, + effort: 6, + }) + .toFile(webpPath); + await unlink(pngPath); + const after = (await stat(webpPath)).size; + return { pngPath, webpPath, before, after }; +} + +async function main() { + const pngs = await walk(IMPORTS_ROOT); + let saved = 0; + for (const p of pngs) { + const { before, after } = await convertOne(p); + saved += Math.max(0, before - after); + console.log( + `${p.replace(IMPORTS_ROOT, "")}: ${(before / 1024).toFixed(0)}KB → ${(after / 1024).toFixed(0)}KB`, + ); + } + console.log(`Done. ${pngs.length} files. ~${(saved / 1024 / 1024).toFixed(2)} MB smaller.`); +} + +try { + await main(); +} catch (e) { + console.error(e); + process.exit(1); +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx deleted file mode 100644 index d710a965..00000000 --- a/frontend/src/App.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useState } from "react"; - -function App() { - const [count, setCount] = useState(0); - - return ( -
-
-

🎮 Vantage Point

-

- Spatial Intelligence Platform for Competitive Gamers -

-
- -
-
-

Welcome to Vantage Point

-

- Transform your gameplay through advanced positioning analysis. -

- -
- -
-
-

📍 Spatial Tracking

-

- Process coordinate data for deaths and kills -

-
-
-

🤖 AI Positioning

-

- ML predictions for optimal positioning -

-
-
-

👻 Ghost Player

-

Real-time overlay recommendations

-
-
-

📊 Analytics

-

Identify patterns and mistakes

-
-
-
- -
-

- Backend API:{" "} - - http://localhost:8000 - -

-
-
- ); -} - -export default App; diff --git a/frontend/src/__tests__/App.test.jsx b/frontend/src/__tests__/App.test.tsx similarity index 88% rename from frontend/src/__tests__/App.test.jsx rename to frontend/src/__tests__/App.test.tsx index 2c16a106..a395cceb 100644 --- a/frontend/src/__tests__/App.test.jsx +++ b/frontend/src/__tests__/App.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { render } from "@testing-library/react"; -import App from "../App"; +import App from "../landing/app/App"; describe("App Component", () => { it("should render without crashing", () => { diff --git a/frontend/src/index.css b/frontend/src/index.css deleted file mode 100644 index b5c61c95..00000000 --- a/frontend/src/index.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/frontend/src/landing/app/App.tsx b/frontend/src/landing/app/App.tsx new file mode 100644 index 00000000..0b7d8ae0 --- /dev/null +++ b/frontend/src/landing/app/App.tsx @@ -0,0 +1,11 @@ +import { RouterProvider } from "react-router"; +import { AuthProvider } from "./context/AuthContext"; +import { router } from "./routes"; + +export default function App() { + return ( + + + + ); +} diff --git a/frontend/src/landing/app/api/auth.ts b/frontend/src/landing/app/api/auth.ts new file mode 100644 index 00000000..30418797 --- /dev/null +++ b/frontend/src/landing/app/api/auth.ts @@ -0,0 +1,34 @@ +import { apiFetchPublic } from "./client"; +import { setStoredTokens } from "../lib/tokens"; +import type { TokenResponse } from "../types/auth"; + +export interface RegisterPayload { + readonly email: string; + readonly display_name: string; + readonly password: string; +} + +export interface LoginPayload { + readonly email: string; + readonly password: string; +} + +async function storeTokensFromResponse(tokens: TokenResponse): Promise { + setStoredTokens(tokens.access_token, tokens.refresh_token); +} + +export async function registerUser(payload: RegisterPayload): Promise { + const tokens = await apiFetchPublic("/api/v1/auth/register", { + method: "POST", + body: JSON.stringify(payload), + }); + await storeTokensFromResponse(tokens); +} + +export async function loginUser(payload: LoginPayload): Promise { + const tokens = await apiFetchPublic("/api/v1/auth/login", { + method: "POST", + body: JSON.stringify(payload), + }); + await storeTokensFromResponse(tokens); +} diff --git a/frontend/src/landing/app/api/client.ts b/frontend/src/landing/app/api/client.ts new file mode 100644 index 00000000..db8dcf24 --- /dev/null +++ b/frontend/src/landing/app/api/client.ts @@ -0,0 +1,168 @@ +import { + clearStoredTokens, + getStoredTokens, + setStoredTokens, +} from "../lib/tokens"; +import type { ApiErrorBody, TokenResponse } from "../types/auth"; + +const API_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; + +export class ApiError extends Error { + readonly status: number; + + constructor(status: number, message: string) { + super(message); + this.name = "ApiError"; + this.status = status; + } +} + +async function parseErrorMessage(response: Response): Promise { + try { + const body = (await response.json()) as ApiErrorBody; + if (typeof body.detail === "string") { + return body.detail; + } + if (Array.isArray(body.detail) && body.detail[0]?.msg) { + return body.detail[0].msg; + } + } catch { + // ignore JSON parse errors + } + return response.statusText || "Request failed"; +} + +let refreshInFlight: Promise | null = null; + +async function refreshAccessToken(): Promise { + const { refreshToken } = getStoredTokens(); + if (!refreshToken) { + return false; + } + + const response = await fetch(`${API_URL}/api/v1/auth/refresh`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refresh_token: refreshToken }), + }); + + if (!response.ok) { + clearStoredTokens(); + return false; + } + + const tokens = (await response.json()) as TokenResponse; + setStoredTokens(tokens.access_token, tokens.refresh_token); + return true; +} + +export async function apiFetch( + path: string, + options: RequestInit = {}, + retryOnUnauthorized = true, +): Promise { + const { accessToken } = getStoredTokens(); + const headers = new Headers(options.headers); + if (!headers.has("Content-Type") && options.body) { + headers.set("Content-Type", "application/json"); + } + if (accessToken) { + headers.set("Authorization", `Bearer ${accessToken}`); + } + + let response = await fetch(`${API_URL}${path}`, { ...options, headers }); + + if (response.status === 401 && retryOnUnauthorized) { + if (!refreshInFlight) { + refreshInFlight = refreshAccessToken().finally(() => { + refreshInFlight = null; + }); + } + const refreshed = await refreshInFlight; + if (refreshed) { + const retryHeaders = new Headers(options.headers); + if (!retryHeaders.has("Content-Type") && options.body) { + retryHeaders.set("Content-Type", "application/json"); + } + const { accessToken: newAccess } = getStoredTokens(); + if (newAccess) { + retryHeaders.set("Authorization", `Bearer ${newAccess}`); + } + response = await fetch(`${API_URL}${path}`, { + ...options, + headers: retryHeaders, + }); + } + } + + if (!response.ok) { + throw new ApiError(response.status, await parseErrorMessage(response)); + } + + if (response.status === 204) { + return undefined as T; + } + + return (await response.json()) as T; +} + +export async function apiFetchFormData( + path: string, + formData: FormData, + retryOnUnauthorized = true, +): Promise { + const { accessToken } = getStoredTokens(); + const headers = new Headers(); + if (accessToken) { + headers.set("Authorization", `Bearer ${accessToken}`); + } + + let response = await fetch(`${API_URL}${path}`, { + method: "POST", + headers, + body: formData, + }); + + if (response.status === 401 && retryOnUnauthorized) { + if (!refreshInFlight) { + refreshInFlight = refreshAccessToken().finally(() => { + refreshInFlight = null; + }); + } + const refreshed = await refreshInFlight; + if (refreshed) { + const retryHeaders = new Headers(); + const { accessToken: newAccess } = getStoredTokens(); + if (newAccess) { + retryHeaders.set("Authorization", `Bearer ${newAccess}`); + } + response = await fetch(`${API_URL}${path}`, { + method: "POST", + headers: retryHeaders, + body: formData, + }); + } + } + + if (!response.ok) { + throw new ApiError(response.status, await parseErrorMessage(response)); + } + + return (await response.json()) as T; +} + +export async function apiFetchPublic( + path: string, + options: RequestInit = {}, +): Promise { + const headers = new Headers(options.headers); + if (!headers.has("Content-Type") && options.body) { + headers.set("Content-Type", "application/json"); + } + + const response = await fetch(`${API_URL}${path}`, { ...options, headers }); + if (!response.ok) { + throw new ApiError(response.status, await parseErrorMessage(response)); + } + return (await response.json()) as T; +} diff --git a/frontend/src/landing/app/api/match.ts b/frontend/src/landing/app/api/match.ts new file mode 100644 index 00000000..e72e650c --- /dev/null +++ b/frontend/src/landing/app/api/match.ts @@ -0,0 +1,113 @@ +import { apiFetch } from "./client"; +import type { + MatchDetail, + ParticipantDetail, + TeamDetail, +} from "../types/match"; + +interface ObjectivesSummaryApi { + readonly baron: number; + readonly dragon: number; + readonly rift_herald: number; + readonly tower: number; + readonly inhibitor: number; +} + +interface ParticipantDetailApi { + readonly puuid: string; + readonly riot_id: string | null; + readonly champion_id: number; + readonly champion_name: string; + readonly position: string; + readonly win: boolean; + readonly kills: number; + readonly deaths: number; + readonly assists: number; + readonly cs: number; + readonly gold_earned: number; + readonly damage_to_champions: number; + readonly vision_score: number; + readonly items: number[]; + readonly summoner_spells: number[]; + readonly is_viewer: boolean; +} + +interface ChampionBanApi { + readonly champion_id: number; + readonly champion_name: string; +} + +interface TeamDetailApi { + readonly team_id: number; + readonly win: boolean; + readonly bans: ChampionBanApi[]; + readonly objectives: ObjectivesSummaryApi; + readonly participants: ParticipantDetailApi[]; +} + +interface MatchDetailApi { + readonly match_id: string; + readonly game_creation: number; + readonly game_duration: number; + readonly game_version: string; + readonly queue_id: number; + readonly queue_label: string; + readonly map_id: number; + readonly map_label: string; + readonly teams: TeamDetailApi[]; +} + +function mapParticipant(p: ParticipantDetailApi): ParticipantDetail { + return { + puuid: p.puuid, + riot_id: p.riot_id, + champion_id: p.champion_id, + champion_name: p.champion_name, + position: p.position, + win: p.win, + kills: p.kills, + deaths: p.deaths, + assists: p.assists, + cs: p.cs, + gold_earned: p.gold_earned, + damage_to_champions: p.damage_to_champions, + vision_score: p.vision_score, + items: p.items, + summoner_spells: p.summoner_spells, + is_viewer: p.is_viewer, + }; +} + +function mapTeam(t: TeamDetailApi): TeamDetail { + return { + team_id: t.team_id, + win: t.win, + bans: t.bans.map((ban) => ({ + champion_id: ban.champion_id, + champion_name: ban.champion_name, + })), + objectives: t.objectives, + participants: t.participants.map(mapParticipant), + }; +} + +function mapMatchDetail(body: MatchDetailApi): MatchDetail { + return { + match_id: body.match_id, + game_creation: body.game_creation, + game_duration: body.game_duration, + game_version: body.game_version, + queue_id: body.queue_id, + queue_label: body.queue_label, + map_id: body.map_id, + map_label: body.map_label, + teams: body.teams.map(mapTeam), + }; +} + +export async function fetchMatchDetail(matchId: string): Promise { + const body = await apiFetch( + `/api/v1/matches/${encodeURIComponent(matchId)}`, + ); + return mapMatchDetail(body); +} diff --git a/frontend/src/landing/app/api/matches.ts b/frontend/src/landing/app/api/matches.ts new file mode 100644 index 00000000..b95187a4 --- /dev/null +++ b/frontend/src/landing/app/api/matches.ts @@ -0,0 +1,37 @@ +import { apiFetch } from "./client"; +import type { MatchHistorySummary } from "../types/match"; + +interface MatchHistorySummaryApi { + readonly match_id: string; + readonly champion_name: string; + readonly outcome: "Victory" | "Defeat"; + readonly duration_minutes: number; + readonly map_label: string; + readonly played_on: string; + readonly kills: number; + readonly deaths: number; + readonly assists: number; + readonly cs: number; + readonly position: string; +} + +function mapHistoryRow(row: MatchHistorySummaryApi): MatchHistorySummary { + return { + matchId: row.match_id, + champion_name: row.champion_name, + outcome: row.outcome, + duration_minutes: row.duration_minutes, + map_label: row.map_label, + played_on: row.played_on, + kills: row.kills, + deaths: row.deaths, + assists: row.assists, + cs: row.cs, + position: row.position, + }; +} + +export async function fetchMatchHistory(): Promise { + const rows = await apiFetch("/api/v1/matches"); + return rows.map(mapHistoryRow); +} diff --git a/frontend/src/landing/app/api/profile.ts b/frontend/src/landing/app/api/profile.ts new file mode 100644 index 00000000..7fc942e0 --- /dev/null +++ b/frontend/src/landing/app/api/profile.ts @@ -0,0 +1,119 @@ +import { leagueWildRiftCard, leagueWildRiftCover } from "../assets/profile"; +import type { + FeaturedGameSlide, + PlayerAchievement, + PlayerProfile, + RadarMetric, + RecentChampion, +} from "../types/profile"; +import { apiFetch } from "./client"; + +const PROFILE_IMAGE_KEYS: Record = { + league_wild_rift_cover: leagueWildRiftCover, + league_wild_rift_card: leagueWildRiftCard, +}; + +function resolveImageUrl(key: string): string { + return PROFILE_IMAGE_KEYS[key] ?? leagueWildRiftCover; +} + +interface RadarMetricApi { + readonly key: string; + readonly label: string; + readonly value: number; + readonly raw_label: string; +} + +interface RecentChampionApi { + readonly champion_id: number; + readonly champion_name: string; + readonly games_played: number; +} + +interface PlayerAchievementApi { + readonly id: string; + readonly label: string; + readonly description: string; + readonly source_field: string; + readonly count: number; +} + +interface FeaturedGameSlideApi { + readonly game_name: string; + readonly cover_image_key: string; + readonly card_image_key?: string; + readonly efficiency_score: number; + readonly time_spent_label: string; + readonly win_rate_label: string; + readonly kda_label: string; +} + +interface PlayerProfileApi { + readonly display_name: string; + readonly riot_id_tag: string; + readonly avatar_initials: string; + readonly avatar_url: string | null; + readonly matches_sampled: number; + readonly radar_metrics: RadarMetricApi[]; + readonly recent_champions: RecentChampionApi[]; + readonly achievements: PlayerAchievementApi[]; + readonly featured_games: FeaturedGameSlideApi[]; +} + +function mapRadar(m: RadarMetricApi): RadarMetric { + return { + key: m.key, + label: m.label, + value: m.value, + rawLabel: m.raw_label, + }; +} + +function mapFeatured(slide: FeaturedGameSlideApi): FeaturedGameSlide { + const cover = resolveImageUrl(slide.cover_image_key); + const card = slide.card_image_key + ? resolveImageUrl(slide.card_image_key) + : cover; + return { + game_name: slide.game_name, + cover_image_url: cover, + card_image_url: card, + efficiency_score: slide.efficiency_score, + time_spent_label: slide.time_spent_label, + win_rate_label: slide.win_rate_label, + kda_label: slide.kda_label, + }; +} + +function mapProfile(body: PlayerProfileApi): PlayerProfile { + return { + display_name: body.display_name, + riot_id_tag: body.riot_id_tag, + avatar_initials: body.avatar_initials, + avatar_url: body.avatar_url, + matches_sampled: body.matches_sampled, + radar_metrics: body.radar_metrics.map(mapRadar), + recent_champions: body.recent_champions.map( + (c): RecentChampion => ({ + champion_id: c.champion_id, + champion_name: c.champion_name, + games_played: c.games_played, + }), + ), + achievements: body.achievements.map( + (a): PlayerAchievement => ({ + id: a.id, + label: a.label, + description: a.description, + source_field: a.source_field, + count: a.count, + }), + ), + featured_games: body.featured_games.map(mapFeatured), + }; +} + +export async function fetchPlayerProfile(): Promise { + const body = await apiFetch("/api/v1/users/me/profile"); + return mapProfile(body); +} diff --git a/frontend/src/landing/app/api/user.ts b/frontend/src/landing/app/api/user.ts new file mode 100644 index 00000000..837bca8a --- /dev/null +++ b/frontend/src/landing/app/api/user.ts @@ -0,0 +1,52 @@ +import { apiFetch, apiFetchFormData } from "./client"; +import type { + AvatarUploadResponse, + LinkGameAccountResponse, + UserMe, +} from "../types/auth"; + +export async function getMe(): Promise { + return apiFetch("/api/v1/users/me"); +} + +export async function updateMe(payload: { + display_name: string; +}): Promise { + return apiFetch("/api/v1/users/me", { + method: "PATCH", + body: JSON.stringify(payload), + }); +} + +export async function uploadAvatar(file: File): Promise { + const formData = new FormData(); + formData.append("file", file); + return apiFetchFormData( + "/api/v1/users/me/avatar", + formData, + ); +} + +export async function deleteAvatar(): Promise { + return apiFetch("/api/v1/users/me/avatar", { + method: "DELETE", + }); +} + +export async function linkGameAccount( + riotId: string, +): Promise { + return apiFetch("/api/v1/users/me/game-accounts", { + method: "POST", + body: JSON.stringify({ riot_id: riotId }), + }); +} + +export async function updateRiotId( + riotId: string, +): Promise { + return apiFetch("/api/v1/users/me/game-accounts", { + method: "PUT", + body: JSON.stringify({ riot_id: riotId }), + }); +} diff --git a/frontend/src/landing/app/assets/profile/icon-lightning-bolt.png b/frontend/src/landing/app/assets/profile/icon-lightning-bolt.png new file mode 100644 index 00000000..52dcb96f Binary files /dev/null and b/frontend/src/landing/app/assets/profile/icon-lightning-bolt.png differ diff --git a/frontend/src/landing/app/assets/profile/icon-time-machine.png b/frontend/src/landing/app/assets/profile/icon-time-machine.png new file mode 100644 index 00000000..ceab976a Binary files /dev/null and b/frontend/src/landing/app/assets/profile/icon-time-machine.png differ diff --git a/frontend/src/landing/app/assets/profile/index.ts b/frontend/src/landing/app/assets/profile/index.ts new file mode 100644 index 00000000..0e41c1a7 --- /dev/null +++ b/frontend/src/landing/app/assets/profile/index.ts @@ -0,0 +1,5 @@ +/** Profile featured-game assets exported from Figma (vantage-point file). */ +export { default as leagueWildRiftCover } from "./league-wild-rift-cover.png"; +export { default as leagueWildRiftCard } from "./league-wild-rift-card.png"; +export { default as iconLightningBolt } from "./icon-lightning-bolt.png"; +export { default as iconTimeMachine } from "./icon-time-machine.png"; diff --git a/frontend/src/landing/app/assets/profile/league-wild-rift-card.png b/frontend/src/landing/app/assets/profile/league-wild-rift-card.png new file mode 100644 index 00000000..eab271c0 Binary files /dev/null and b/frontend/src/landing/app/assets/profile/league-wild-rift-card.png differ diff --git a/frontend/src/landing/app/assets/profile/league-wild-rift-cover.png b/frontend/src/landing/app/assets/profile/league-wild-rift-cover.png new file mode 100644 index 00000000..eab271c0 Binary files /dev/null and b/frontend/src/landing/app/assets/profile/league-wild-rift-cover.png differ diff --git a/frontend/src/landing/app/components/AuthOnlyRoute.tsx b/frontend/src/landing/app/components/AuthOnlyRoute.tsx new file mode 100644 index 00000000..293e7a5e --- /dev/null +++ b/frontend/src/landing/app/components/AuthOnlyRoute.tsx @@ -0,0 +1,23 @@ +import { Navigate, Outlet } from "react-router"; +import { useAuth } from "../context/AuthContext"; + +/** Requires a logged-in session but does not require a linked Riot ID. */ +export default function AuthOnlyRoute() { + const { user, loading } = useAuth(); + + if (loading) { + return ( +
+

+ Loading… +

+
+ ); + } + + if (!user) { + return ; + } + + return ; +} diff --git a/frontend/src/landing/app/components/DashboardPage.tsx b/frontend/src/landing/app/components/DashboardPage.tsx new file mode 100644 index 00000000..f6acb69d --- /dev/null +++ b/frontend/src/landing/app/components/DashboardPage.tsx @@ -0,0 +1,106 @@ +import { useCallback, useEffect, useState } from "react"; +import { + Outlet, + useLocation, + useNavigate, + useSearchParams, +} from "react-router"; +import DashboardShell, { + type DashboardSection, +} from "../../imports/Group14/Group14"; +import type { DashboardOutletContext } from "../context/dashboardLayoutContext"; +import { fetchPlayerProfile } from "../api/profile"; +import { useAuth } from "../context/AuthContext"; +import type { PlayerProfile } from "../types/profile"; + +function sectionFromPathname(pathname: string): DashboardSection { + return pathname.includes("/dashboard/profile") ? "profile" : "matches"; +} + +export default function DashboardPage() { + const navigate = useNavigate(); + const location = useLocation(); + const [searchParams] = useSearchParams(); + const { user, logout, refreshUser } = useAuth(); + const [sidebarOpen, setSidebarOpen] = useState(true); + const [profile, setProfile] = useState(undefined); + + const loadProfile = useCallback(async () => { + if (!user) { + setProfile(undefined); + return; + } + const data = await fetchPlayerProfile(); + setProfile(data); + }, [user]); + + const activeSection = sectionFromPathname(location.pathname); + + useEffect(() => { + const legacyMatch = searchParams.get("match"); + const legacyView = searchParams.get("view"); + if (legacyMatch) { + navigate(`/dashboard/matches/${encodeURIComponent(legacyMatch)}`, { + replace: true, + }); + return; + } + if (legacyView === "profile") { + navigate("/dashboard/profile", { replace: true }); + return; + } + if (legacyView === "matches") { + navigate("/dashboard/matches", { replace: true }); + } + }, [navigate, searchParams]); + + useEffect(() => { + let cancelled = false; + loadProfile().catch(() => { + if (!cancelled) { + setProfile(undefined); + } + }); + return () => { + cancelled = true; + }; + }, [loadProfile]); + + const refreshProfile = useCallback(async () => { + await refreshUser(); + await loadProfile(); + }, [refreshUser, loadProfile]); + + const accountAvatarUrl = profile?.avatar_url ?? user?.avatar_url ?? null; + const accountInitials = profile?.avatar_initials ?? "VP"; + + const handleLogout = () => { + logout(); + navigate("/login", { replace: true }); + }; + + const outletContext: DashboardOutletContext = { + sidebarOpen, + profile, + refreshProfile, + }; + + return ( +
+
+ setSidebarOpen((open) => !open)} + activeSection={activeSection} + onMatchesClick={() => navigate("/dashboard/matches")} + onProfileClick={() => navigate("/dashboard/profile")} + onLogout={handleLogout} + accountInitials={accountInitials} + accountAvatarUrl={accountAvatarUrl} + > + + +
+
+ ); +} diff --git a/frontend/src/landing/app/components/FeaturedGameCard.tsx b/frontend/src/landing/app/components/FeaturedGameCard.tsx new file mode 100644 index 00000000..39a6fd4c --- /dev/null +++ b/frontend/src/landing/app/components/FeaturedGameCard.tsx @@ -0,0 +1,129 @@ +import type { ReactNode } from "react"; +import { History, Swords, Trophy, Zap } from "lucide-react"; +import type { FeaturedGameSlide } from "../types/profile"; + +interface FeaturedGameCardProps { + readonly slide: FeaturedGameSlide; + readonly expanded: boolean; + readonly onToggle?: () => void; +} + +function StatRow({ + icon, + label, + value, +}: Readonly<{ + icon: ReactNode; + label: string; + value: string | number; +}>) { + return ( +
+ + {icon} + +
+ + {label} + + + {value} + +
+
+ ); +} + +function StatIconLucide({ Icon }: Readonly<{ Icon: typeof Trophy }>) { + return ; +} + +/** Closed state — Figma node 139:837 (Product Info Card). */ +function FeaturedGameCardClosed({ + slide, + onToggle, +}: Readonly<{ slide: FeaturedGameSlide; onToggle?: () => void }>) { + return ( + + ); +} + +/** Open state — Figma node 179:1051 (Group 16). */ +function FeaturedGameCardOpen({ + slide, + onToggle, +}: Readonly<{ slide: FeaturedGameSlide; onToggle?: () => void }>) { + return ( + + ); +} + +export default function FeaturedGameCard({ + slide, + expanded, + onToggle, +}: Readonly) { + if (expanded) { + return ; + } + return ; +} diff --git a/frontend/src/landing/app/components/LandingPage.tsx b/frontend/src/landing/app/components/LandingPage.tsx new file mode 100644 index 00000000..662af638 --- /dev/null +++ b/frontend/src/landing/app/components/LandingPage.tsx @@ -0,0 +1,9 @@ +import Group12 from "../../imports/Group12/Group12"; + +export default function LandingPage() { + return ( +
+ +
+ ); +} diff --git a/frontend/src/landing/app/components/LinkRiotPage.tsx b/frontend/src/landing/app/components/LinkRiotPage.tsx new file mode 100644 index 00000000..e1359a82 --- /dev/null +++ b/frontend/src/landing/app/components/LinkRiotPage.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import { useNavigate } from "react-router"; +import { ApiError } from "../api/client"; +import { useAuth } from "../context/AuthContext"; +import RiotIdComponent, { + type RiotIdFormProps, +} from "../../imports/RiotId/RiotId"; + +export default function LinkRiotPage() { + const navigate = useNavigate(); + const { linkRiot } = useAuth(); + const [riotId, setRiotId] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleSubmit = async () => { + setError(null); + const trimmed = riotId.trim(); + if (!trimmed.includes("#")) { + setError("Enter your Riot ID as Name#TAG (e.g. Player#EUW)."); + return; + } + + setLoading(true); + try { + await linkRiot(trimmed); + navigate("/loading", { replace: true }); + } catch (err) { + const message = + err instanceof ApiError + ? err.message + : "Could not link Riot ID. Please try again."; + setError(message); + } finally { + setLoading(false); + } + }; + + const formProps: RiotIdFormProps = { + riotId, + error, + loading, + onRiotIdChange: setRiotId, + onSubmit: () => void handleSubmit(), + }; + + return ( +
+ +
+ ); +} diff --git a/frontend/src/landing/app/components/LoadingPage.tsx b/frontend/src/landing/app/components/LoadingPage.tsx new file mode 100644 index 00000000..758a3765 --- /dev/null +++ b/frontend/src/landing/app/components/LoadingPage.tsx @@ -0,0 +1,57 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router"; +import { useAuth } from "../context/AuthContext"; +import imgLogo from "../../imports/Login/798001aef0b2686ac929f8c349135d3326ab65bb.webp"; + +const MIN_DISPLAY_MS = 1500; +const DOT_DELAYS_S = [0, 0.9, 1.8] as const; + +export default function LoadingPage() { + const navigate = useNavigate(); + const { refreshUser } = useAuth(); + + useEffect(() => { + const started = Date.now(); + + void (async () => { + await refreshUser(); + const elapsed = Date.now() - started; + const remaining = Math.max(0, MIN_DISPLAY_MS - elapsed); + globalThis.setTimeout(() => { + navigate("/dashboard/matches", { replace: true }); + }, remaining); + })(); + }, [navigate, refreshUser]); + + return ( +
+
+
+ +
+

+ Vantage Point +

+ +
+
+ ); +} diff --git a/frontend/src/landing/app/components/LoginPage.tsx b/frontend/src/landing/app/components/LoginPage.tsx new file mode 100644 index 00000000..93460cde --- /dev/null +++ b/frontend/src/landing/app/components/LoginPage.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import { useNavigate } from "react-router"; +import { ApiError } from "../api/client"; +import { useAuth } from "../context/AuthContext"; +import LoginComponent, { type LoginFormProps } from "../../imports/Login/Login"; + +export default function LoginPage() { + const navigate = useNavigate(); + const { login } = useAuth(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleSubmit = async () => { + setError(null); + setLoading(true); + try { + const me = await login({ email: email.trim(), password }); + if (me.has_linked_riot) { + navigate("/loading", { replace: true }); + } else { + navigate("/link-riot", { replace: true }); + } + } catch (err) { + const message = + err instanceof ApiError + ? err.message + : "Sign in failed. Please try again."; + setError(message); + } finally { + setLoading(false); + } + }; + + const formProps: LoginFormProps = { + email, + password, + error, + loading, + onEmailChange: setEmail, + onPasswordChange: setPassword, + onSubmit: () => void handleSubmit(), + onSocialClick: () => setError("Social sign-in is coming soon."), + }; + + return ( +
+ +
+ ); +} diff --git a/frontend/src/landing/app/components/MatchDetailView.tsx b/frontend/src/landing/app/components/MatchDetailView.tsx new file mode 100644 index 00000000..b8827b01 --- /dev/null +++ b/frontend/src/landing/app/components/MatchDetailView.tsx @@ -0,0 +1,456 @@ +import { useEffect, useState } from "react"; +import { ArrowLeft } from "lucide-react"; +import { useNavigate, useOutletContext, useParams } from "react-router"; +import type { DashboardOutletContext } from "../context/dashboardLayoutContext"; +import { fetchMatchDetail } from "../api/match"; +import { + DASHBOARD_CONTENT_HEIGHT, + getDashboardContentStyle, +} from "../lib/dashboardLayout"; +import { + championIconUrl, + itemIconUrl, + summonerSpellIconUrl, +} from "../lib/ddragon"; +import type { + MatchDetail, + ParticipantDetail, + TeamDetail, +} from "../types/match"; + +interface MatchDetailViewProps { + readonly matchId?: string; + readonly sidebarOpen?: boolean; + readonly onBack?: () => void; + readonly viewerPuuid?: string; +} + +function formatDuration(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, "0")}`; +} + +function formatGameDate(epochMs: number): string { + return new Date(epochMs).toLocaleString(undefined, { + dateStyle: "medium", + timeStyle: "short", + }); +} + +function formatNumber(n: number): string { + if (n >= 1000) return `${(n / 1000).toFixed(1)}k`; + return String(n); +} + +function viewerParticipant( + match: MatchDetail, + viewerPuuid?: string, +): ParticipantDetail | undefined { + for (const team of match.teams) { + const found = team.participants.find( + (p) => p.is_viewer || (viewerPuuid && p.puuid === viewerPuuid), + ); + if (found) return found; + } + return undefined; +} + +function LoadingSkeleton() { + return ( +
+
+
+
+
+
+
+
+ ); +} + +function ParticipantRow({ player }: Readonly<{ player: ParticipantDetail }>) { + const isViewer = player.is_viewer; + const rowBg = isViewer ? "bg-[#dce8fc]" : ""; + const cellBase = `py-2 ${rowBg}`; + + return ( + + +
+ +
+

+ {player.riot_id ?? player.champion_name} +

+

{player.position}

+
+
+ + + {player.kills}/{player.deaths}/{player.assists} + + + {player.cs} + + + {formatNumber(player.gold_earned)} + + + {formatNumber(player.damage_to_champions)} + + + {player.vision_score} + + +
+ {player.summoner_spells.map((spellId) => { + const url = summonerSpellIconUrl(spellId); + return url ? ( + + ) : ( + + {spellId} + + ); + })} + {player.items.map((itemId, i) => { + const url = itemIconUrl(itemId); + if (!url) { + return ( + + ); + } + return ( + + ); + })} +
+ + + ); +} + +function TeamScoreboard({ + team, + sideLabel, +}: Readonly<{ team: TeamDetail; sideLabel: string }>) { + const sideColor = team.team_id === 100 ? "text-[#4a7fd4]" : "text-[#c44a4a]"; + + return ( +
+
+

{sideLabel}

+ + {team.win ? "Victory" : "Defeat"} + +
+
+ + + + + + + + + + + + + + {team.participants.map((p) => ( + + ))} + +
PlayerKDACS + Gold + + DMG + + Vis + Build
+
+
+ ); +} + +function ObjectivesRow({ teams }: Readonly<{ teams: readonly TeamDetail[] }>) { + const labels: { key: keyof TeamDetail["objectives"]; label: string }[] = [ + { key: "dragon", label: "Dragons" }, + { key: "baron", label: "Baron" }, + { key: "rift_herald", label: "Herald" }, + { key: "tower", label: "Towers" }, + { key: "inhibitor", label: "Inhibitors" }, + ]; + + return ( +
+ {teams.map((team) => ( +
+

+ {team.team_id === 100 ? "Blue" : "Red"} objectives +

+
    + {labels.map(({ key, label }) => ( +
  • + {label} + + {team.objectives[key]} + +
  • + ))} +
+
+ ))} +
+ ); +} + +function BansRow({ teams }: Readonly<{ teams: readonly TeamDetail[] }>) { + return ( +
+ {teams.map((team) => ( +
+

+ {team.team_id === 100 ? "Blue" : "Red"} bans +

+
+ {team.bans.map((ban) => ( + {ban.champion_name} + ))} +
+
+ ))} +
+ ); +} + +export default function MatchDetailView({ + matchId: matchIdProp, + sidebarOpen: sidebarOpenProp, + onBack: onBackProp, + viewerPuuid, +}: Readonly = {}) { + const navigate = useNavigate(); + const { matchId: matchIdParam } = useParams<{ matchId: string }>(); + const outlet = useOutletContext(); + const matchId = matchIdProp ?? matchIdParam ?? ""; + const sidebarOpen = sidebarOpenProp ?? outlet?.sidebarOpen ?? true; + const onBack = + onBackProp ?? (() => navigate("/dashboard/matches", { replace: false })); + + const [match, setMatch] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const contentStyle = getDashboardContentStyle(sidebarOpen); + + useEffect(() => { + if (!matchId) { + setMatch(null); + setError(null); + return; + } + + let cancelled = false; + setLoading(true); + setError(null); + setMatch(null); + + fetchMatchDetail(matchId) + .then((data) => { + if (!cancelled) { + if (!data.teams?.length) { + setError("Could not load match"); + } else { + setMatch(data); + } + } + }) + .catch(() => { + if (!cancelled) setError("Could not load match"); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [matchId, viewerPuuid]); + + const viewer = match ? viewerParticipant(match, viewerPuuid) : undefined; + const resultLabel = viewer ? (viewer.win ? "Victory" : "Defeat") : null; + const resultClass = viewer?.win ? "text-[#1e7e34]" : "text-[#c44a4a]"; + + const blueTeam = match?.teams.find((t) => t.team_id === 100); + const redTeam = match?.teams.find((t) => t.team_id === 200); + + return ( +
+
+ + +
+ {loading && ( +

+ Loading match… +

+ )} + {error && ( +

{error}

+ )} + {match && viewer && ( + <> +
+ +
+

+ {resultLabel} +

+

+ {viewer.champion_name} · {viewer.kills}/{viewer.deaths}/ + {viewer.assists} KDA +

+
+
+

+ {formatDuration(match.game_duration)} + · + {match.queue_label} + · + {match.map_label} + · + v{match.game_version} + · + {formatGameDate(match.game_creation)} +

+ + )} + {match && !viewer && ( +

+ Match details +

+ )} +
+ +
+ {loading && } + {error && !loading && ( +

+ Try again later or pick another match from your matches. +

+ )} + {match && blueTeam && redTeam && !loading && ( + <> +
+ + +
+
+

+ Objectives +

+ +
+
+

+ Bans +

+ +
+ + )} +
+
+
+ ); +} diff --git a/frontend/src/landing/app/components/MatchesListToolbar.tsx b/frontend/src/landing/app/components/MatchesListToolbar.tsx new file mode 100644 index 00000000..b9754051 --- /dev/null +++ b/frontend/src/landing/app/components/MatchesListToolbar.tsx @@ -0,0 +1,65 @@ +import { Search } from "lucide-react"; +import type { MatchFilterId, MatchSortId } from "../lib/matchListControls"; +import MatchesListToolbarMenus from "./MatchesListToolbarMenus"; + +interface MatchesListToolbarProps { + readonly searchQuery: string; + readonly onSearchQueryChange: (query: string) => void; + readonly filterId: MatchFilterId; + readonly onFilterIdChange: (filterId: MatchFilterId) => void; + readonly sortId: MatchSortId; + readonly onSortIdChange: (sortId: MatchSortId) => void; +} + +export default function MatchesListToolbar({ + searchQuery, + onSearchQueryChange, + filterId, + onFilterIdChange, + sortId, + onSortIdChange, +}: Readonly) { + return ( +
+
+ + + +
+ ); +} diff --git a/frontend/src/landing/app/components/MatchesListToolbarMenus.tsx b/frontend/src/landing/app/components/MatchesListToolbarMenus.tsx new file mode 100644 index 00000000..39eb4cf6 --- /dev/null +++ b/frontend/src/landing/app/components/MatchesListToolbarMenus.tsx @@ -0,0 +1,96 @@ +import { ArrowUpDown, Filter } from "lucide-react"; +import { + MATCH_FILTER_OPTIONS, + MATCH_SORT_OPTIONS, + matchFilterLabel, + matchSortLabel, + type MatchFilterId, + type MatchSortId, +} from "../lib/matchListControls"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; + +const TOOLBAR_ICON_BUTTON_CLASS = + "flex size-[40px] shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 text-[#0a0a0a] hover:bg-neutral-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4a7fd4]"; + +interface MatchesListToolbarMenusProps { + readonly filterId: MatchFilterId; + readonly onFilterIdChange: (filterId: MatchFilterId) => void; + readonly sortId: MatchSortId; + readonly onSortIdChange: (sortId: MatchSortId) => void; +} + +export default function MatchesListToolbarMenus({ + filterId, + onFilterIdChange, + sortId, + onSortIdChange, +}: Readonly) { + return ( +
+ + + + + + Filter + onFilterIdChange(value as MatchFilterId)} + > + {MATCH_FILTER_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + + + + + + + + Sort + onSortIdChange(value as MatchSortId)} + > + {MATCH_SORT_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + +
+ ); +} diff --git a/frontend/src/landing/app/components/MatchesListView.tsx b/frontend/src/landing/app/components/MatchesListView.tsx new file mode 100644 index 00000000..8d2f36b2 --- /dev/null +++ b/frontend/src/landing/app/components/MatchesListView.tsx @@ -0,0 +1,299 @@ +import { useEffect, useMemo, useState, type ReactNode } from "react"; +import { ChevronRight } from "lucide-react"; +import { useNavigate, useOutletContext } from "react-router"; +import type { DashboardOutletContext } from "../context/dashboardLayoutContext"; +import { + DASHBOARD_CONTENT_HEIGHT, + getDashboardContentStyle, +} from "../lib/dashboardLayout"; +import { fetchMatchHistory } from "../api/matches"; +import { + groupDashboardMatchesByDay, + type DashboardMatchListItem, + type MatchHistoryDayRow, +} from "../lib/matchHistoryGroup"; +import { + applyMatchListControls, + matchListDaySortAscending, +} from "../lib/matchListQuery"; +import { + DEFAULT_MATCH_FILTER_ID, + DEFAULT_MATCH_SORT_ID, + type MatchFilterId, + type MatchSortId, +} from "../lib/matchListControls"; +import type { MatchHistorySummary } from "../types/match"; +import MatchesListToolbar from "./MatchesListToolbar"; + +interface MatchesListViewProps { + readonly sidebarOpen?: boolean; +} + +function outcomeClass(outcome: "Victory" | "Defeat"): string { + return outcome === "Victory" ? "text-[#1e7e34]" : "text-[#c44a4a]"; +} + +interface MatchHistoryListRowProps { + readonly item: DashboardMatchListItem; + readonly onOpenMatch: (matchId: string) => void; +} + +const MATCH_ROW_GRID = + "grid w-full grid-cols-[88px_minmax(0,1fr)_72px_80px_20px] items-center gap-x-4 gap-y-1 px-4 py-3 sm:grid-cols-[88px_minmax(0,1fr)_52px_72px_72px_80px_20px]"; + +const STAT_LABEL_CLASS = + "font-['Inter:Regular',sans-serif] text-[11px] font-medium uppercase tracking-wide text-[#757575]"; + +const STAT_VALUE_CLASS = + "font-['Inter:Regular',sans-serif] text-[14px] text-[#1e1e1e] tabular-nums"; + +function matchRowAriaLabel(item: DashboardMatchListItem): string { + return `View match as ${item.champion_name}, ${item.outcome}, role ${item.roleLabel}, KDA ${item.kdaLabel}, ${item.cs} creep score, ${item.duration_minutes} minutes`; +} + +function MatchStatCell({ + children, + className = "", + hideOnMobile = false, + align = "start", +}: Readonly<{ + children: ReactNode; + className?: string; + hideOnMobile?: boolean; + align?: "start" | "end"; +}>) { + const visibility = hideOnMobile ? "hidden sm:block" : "block"; + const alignment = align === "end" ? "text-right" : "text-left"; + return ( +
+ {children} +
+ ); +} + +function MatchHistoryListHeader() { + return ( +
+ + Result + + + Champion + + + + KDA + + + + Duration + + +
+ ); +} + +function MatchHistoryListRow({ + item, + onOpenMatch, +}: Readonly) { + return ( + + ); +} + +function MatchHistoryDaySection({ + dayRow, + onOpenMatch, +}: Readonly<{ + dayRow: MatchHistoryDayRow; + onOpenMatch: (matchId: string) => void; +}>) { + return ( +
+

+ {dayRow.dateLabel} +

+
+ +
    + {dayRow.matches.map((item) => ( +
  • + +
  • + ))} +
+
+
+ ); +} + +export default function MatchesListView( + props: Readonly = {}, +) { + const { sidebarOpen: sidebarOpenProp } = props; + const navigate = useNavigate(); + const outlet = useOutletContext(); + const sidebarOpen = sidebarOpenProp ?? outlet?.sidebarOpen ?? true; + + const handleOpenMatch = (matchId: string) => { + navigate(`/dashboard/matches/${encodeURIComponent(matchId)}`); + }; + + const [allMatches, setAllMatches] = useState( + [], + ); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [filterId, setFilterId] = useState( + DEFAULT_MATCH_FILTER_ID, + ); + const [sortId, setSortId] = useState(DEFAULT_MATCH_SORT_ID); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + fetchMatchHistory() + .then((matches) => { + if (!cancelled) { + setAllMatches(matches); + } + }) + .catch((err: unknown) => { + if (!cancelled) { + setError( + err instanceof Error ? err.message : "Failed to load matches", + ); + setAllMatches([]); + } + }) + .finally(() => { + if (!cancelled) { + setLoading(false); + } + }); + return () => { + cancelled = true; + }; + }, []); + + const dayRows = useMemo( + () => + groupDashboardMatchesByDay( + applyMatchListControls(allMatches, { filterId, sortId, searchQuery }), + { oldestDaysFirst: matchListDaySortAscending(sortId) }, + ), + [allMatches, filterId, searchQuery, sortId], + ); + + const hasNoMatches = !loading && !error && allMatches.length === 0; + const hasNoVisibleMatches = + !loading && !error && allMatches.length > 0 && dayRows.length === 0; + + const contentStyle = getDashboardContentStyle(sidebarOpen); + + return ( +
+
+ {!loading && !error ? ( + + ) : null} + {loading ? ( +

+ Loading matches… +

+ ) : null} + {error ? ( +

+ {error} +

+ ) : null} + {hasNoMatches ? ( +

+ No matches yet. Link your Riot ID or sign in with the seeded test + account. +

+ ) : null} + {hasNoVisibleMatches ? ( +

+ No matches match your search or filters. +

+ ) : null} +
+ {dayRows.map((dayRow) => ( + + ))} +
+
+
+ ); +} diff --git a/frontend/src/landing/app/components/ProfileHeaderEditor.tsx b/frontend/src/landing/app/components/ProfileHeaderEditor.tsx new file mode 100644 index 00000000..9c602d0a --- /dev/null +++ b/frontend/src/landing/app/components/ProfileHeaderEditor.tsx @@ -0,0 +1,297 @@ +import { useEffect, useRef, useState } from "react"; +import { Camera, Pencil } from "lucide-react"; +import { ApiError } from "../api/client"; +import { + deleteAvatar, + updateMe, + updateRiotId, + uploadAvatar, +} from "../api/user"; +import { resolveAvatarUrl } from "../lib/avatarUrl"; +import { parseRiotId } from "../lib/riotId"; +import type { PlayerProfile } from "../types/profile"; +import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; + +interface ProfileHeaderEditorProps { + readonly profile: PlayerProfile; + readonly onSaved: () => Promise; +} + +export default function ProfileHeaderEditor({ + profile, + onSaved, +}: Readonly) { + const [editing, setEditing] = useState(false); + const [displayName, setDisplayName] = useState(profile.display_name); + const [riotId, setRiotId] = useState( + profile.riot_id_tag === "Not linked" ? "" : profile.riot_id_tag, + ); + const [avatarPreview, setAvatarPreview] = useState( + resolveAvatarUrl(profile.avatar_url), + ); + const [pendingFile, setPendingFile] = useState(null); + const [removeAvatar, setRemoveAvatar] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + + useEffect(() => { + if (!editing) { + setDisplayName(profile.display_name); + setRiotId( + profile.riot_id_tag === "Not linked" ? "" : profile.riot_id_tag, + ); + setAvatarPreview(resolveAvatarUrl(profile.avatar_url)); + setPendingFile(null); + setRemoveAvatar(false); + } + }, [profile, editing]); + + const resetForm = () => { + setDisplayName(profile.display_name); + setRiotId(profile.riot_id_tag === "Not linked" ? "" : profile.riot_id_tag); + setAvatarPreview(resolveAvatarUrl(profile.avatar_url)); + setPendingFile(null); + setRemoveAvatar(false); + setError(null); + }; + + const handleCancel = () => { + resetForm(); + setEditing(false); + }; + + const handleFileChange = (file: File | undefined) => { + if (!file) { + return; + } + setPendingFile(file); + setRemoveAvatar(false); + setAvatarPreview(URL.createObjectURL(file)); + }; + + const handleSave = async () => { + const trimmedName = displayName.trim(); + if (!trimmedName) { + setError("Display name is required."); + return; + } + + setSaving(true); + setError(null); + + try { + await updateMe({ display_name: trimmedName }); + + if (removeAvatar && profile.avatar_url) { + await deleteAvatar(); + } else if (pendingFile) { + const result = await uploadAvatar(pendingFile); + setAvatarPreview(resolveAvatarUrl(result.avatar_url)); + } + + const trimmedRiot = riotId.trim(); + const currentRiot = + profile.riot_id_tag === "Not linked" ? "" : profile.riot_id_tag; + if (trimmedRiot && trimmedRiot !== currentRiot) { + parseRiotId(trimmedRiot); + await updateRiotId(trimmedRiot); + } + + await onSaved(); + setEditing(false); + setPendingFile(null); + setRemoveAvatar(false); + } catch (err) { + const message = + err instanceof ApiError + ? err.message + : err instanceof Error + ? err.message + : "Could not save profile."; + setError(message); + } finally { + setSaving(false); + } + }; + + const viewAvatarSrc = resolveAvatarUrl(profile.avatar_url); + + if (!editing) { + return ( +
+
+ + {viewAvatarSrc ? ( + + ) : null} + + {profile.avatar_initials} + + +
+

+ {profile.display_name} +

+

+ {profile.riot_id_tag} +

+
+
+ +
+ ); + } + + return ( +
+
+
+ + handleFileChange(e.target.files?.[0])} + /> + + {profile.avatar_url || avatarPreview ? ( + + ) : null} +
+ +
+ + +
+
+ + {error ? ( +

+ {error} +

+ ) : null} + +
+ + +
+
+ ); +} diff --git a/frontend/src/landing/app/components/ProfileRadarChart.tsx b/frontend/src/landing/app/components/ProfileRadarChart.tsx new file mode 100644 index 00000000..01fdef15 --- /dev/null +++ b/frontend/src/landing/app/components/ProfileRadarChart.tsx @@ -0,0 +1,52 @@ +import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"; +import type { RadarMetric } from "../types/profile"; +import { ChartContainer, type ChartConfig } from "./ui/chart"; + +interface ProfileRadarChartProps { + readonly metrics: readonly RadarMetric[]; + readonly className?: string; +} + +const chartConfig = { + performance: { + label: "Performance", + color: "#22c55e", + }, +} satisfies ChartConfig; + +export default function ProfileRadarChart({ + metrics, + className, +}: Readonly) { + const data = metrics.map((m) => ({ + metric: m.label, + value: m.value, + raw: m.rawLabel, + })); + + return ( + + + + + + + + ); +} diff --git a/frontend/src/landing/app/components/ProfileView.tsx b/frontend/src/landing/app/components/ProfileView.tsx new file mode 100644 index 00000000..05b68986 --- /dev/null +++ b/frontend/src/landing/app/components/ProfileView.tsx @@ -0,0 +1,163 @@ +import { useState } from "react"; +import { useOutletContext } from "react-router"; +import type { DashboardOutletContext } from "../context/dashboardLayoutContext"; +import { getAchievementIcon } from "../lib/achievementIcons"; +import { championIconUrl } from "../lib/ddragon"; +import type { PlayerProfile } from "../types/profile"; +import { + DASHBOARD_CONTENT_HEIGHT, + getDashboardContentStyle, +} from "../lib/dashboardLayout"; +import FeaturedGameCard from "./FeaturedGameCard"; +import ProfileHeaderEditor from "./ProfileHeaderEditor"; +import ProfileRadarChart from "./ProfileRadarChart"; +import { useAuth } from "../context/AuthContext"; + +interface ProfileViewProps { + readonly profile?: PlayerProfile; + readonly sidebarOpen?: boolean; +} + +export default function ProfileView({ + profile: profileProp, + sidebarOpen: sidebarOpenProp, +}: Readonly = {}) { + const outlet = useOutletContext(); + const { refreshUser } = useAuth(); + const profile = profileProp ?? outlet?.profile; + const sidebarOpen = sidebarOpenProp ?? outlet?.sidebarOpen ?? true; + const refreshProfile = outlet?.refreshProfile; + const [cardExpanded, setCardExpanded] = useState(true); + + const handleProfileSaved = async () => { + if (refreshProfile) { + await refreshProfile(); + } else { + await refreshUser(); + } + }; + + const contentStyle = getDashboardContentStyle(sidebarOpen); + + if (!profile) { + return ( +
+ Loading profile… +
+ ); + } + + const featured = profile.featured_games[0]; + + return ( +
+
+ + +
+

+ Last {profile.matches_sampled} matches +

+ +
+
+ +
    + {profile.radar_metrics.map((m) => ( +
  • + {m.label}:{" "} + {m.rawLabel} +
  • + ))} +
+
+ + {featured ? ( +
+
+ setCardExpanded((open) => !open)} + /> +
+
+ ) : null} +
+
+ +
+

+ Last played champions +

+
+ {profile.recent_champions.map((champ) => ( +
+ {champ.champion_name} + + {champ.games_played} + +
+ ))} +
+
+ +
+

+ Achievements +

+
+ {profile.achievements.map((a) => { + const Icon = getAchievementIcon(a.id); + return ( +
+
+ {Icon ? ( + + ) : null} + + {a.count > 99 ? "99+" : a.count} + +
+ + {a.label} + +
+ ); + })} +
+
+
+
+ ); +} diff --git a/frontend/src/landing/app/components/ProtectedRoute.tsx b/frontend/src/landing/app/components/ProtectedRoute.tsx new file mode 100644 index 00000000..47cb60ec --- /dev/null +++ b/frontend/src/landing/app/components/ProtectedRoute.tsx @@ -0,0 +1,32 @@ +import { Navigate, Outlet } from "react-router"; +import { useAuth } from "../context/AuthContext"; + +interface ProtectedRouteProps { + readonly requireRiot?: boolean; +} + +export default function ProtectedRoute({ + requireRiot = false, +}: Readonly) { + const { user, loading } = useAuth(); + + if (loading) { + return ( +
+

+ Loading… +

+
+ ); + } + + if (!user) { + return ; + } + + if (requireRiot && !user.has_linked_riot) { + return ; + } + + return ; +} diff --git a/frontend/src/landing/app/components/RegisterPage.tsx b/frontend/src/landing/app/components/RegisterPage.tsx new file mode 100644 index 00000000..1929e7e0 --- /dev/null +++ b/frontend/src/landing/app/components/RegisterPage.tsx @@ -0,0 +1,69 @@ +import { useState } from "react"; +import { useNavigate } from "react-router"; +import { ApiError } from "../api/client"; +import { useAuth } from "../context/AuthContext"; +import RegisterComponent, { + type RegisterFormProps, +} from "../../imports/Register/Register"; + +export default function RegisterPage() { + const navigate = useNavigate(); + const { register } = useAuth(); + const [email, setEmail] = useState(""); + const [displayName, setDisplayName] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleSubmit = async () => { + setError(null); + if (password !== confirmPassword) { + setError("Passwords do not match."); + return; + } + if (password.length < 8) { + setError("Password must be at least 8 characters."); + return; + } + + setLoading(true); + try { + await register({ + email: email.trim(), + display_name: displayName.trim(), + password, + }); + navigate("/link-riot", { replace: true }); + } catch (err) { + const message = + err instanceof ApiError + ? err.message + : "Registration failed. Please try again."; + setError(message); + } finally { + setLoading(false); + } + }; + + const formProps: RegisterFormProps = { + email, + displayName, + password, + confirmPassword, + error, + loading, + onEmailChange: setEmail, + onDisplayNameChange: setDisplayName, + onPasswordChange: setPassword, + onConfirmPasswordChange: setConfirmPassword, + onSubmit: () => void handleSubmit(), + onSocialClick: () => setError("Social sign-in is coming soon."), + }; + + return ( +
+ +
+ ); +} diff --git a/frontend/src/landing/app/components/UserAccountMenu.tsx b/frontend/src/landing/app/components/UserAccountMenu.tsx new file mode 100644 index 00000000..c50ffeda --- /dev/null +++ b/frontend/src/landing/app/components/UserAccountMenu.tsx @@ -0,0 +1,90 @@ +import { LogOut, User } from "lucide-react"; +import { resolveAvatarUrl } from "../lib/avatarUrl"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; +import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; + +interface UserAccountMenuProps { + readonly onProfileClick?: () => void; + readonly onLogout?: () => void; + readonly initials?: string; + readonly avatarUrl?: string | null; +} + +function ProfileAvatar({ + initials, + avatarUrl, +}: Readonly<{ initials: string; avatarUrl?: string | null }>) { + const src = resolveAvatarUrl(avatarUrl ?? undefined); + + if (src) { + return ( + + + + {initials} + + + ); + } + + return ( +
+ + + + + {initials} + +
+ ); +} + +export default function UserAccountMenu({ + onProfileClick, + onLogout, + initials = "UN", + avatarUrl = null, +}: Readonly) { + return ( + + + + + + onProfileClick?.()} + className="font-['Inter:Regular',sans-serif] cursor-pointer" + > + + Profile + + + onLogout?.()} + className="font-['Inter:Regular',sans-serif] cursor-pointer" + > + + Log out + + + + ); +} diff --git a/frontend/src/landing/app/components/figma/ImageWithFallback.tsx b/frontend/src/landing/app/components/figma/ImageWithFallback.tsx new file mode 100644 index 00000000..889d6c40 --- /dev/null +++ b/frontend/src/landing/app/components/figma/ImageWithFallback.tsx @@ -0,0 +1,41 @@ +import React, { useState } from "react"; + +const ERROR_IMG_SRC = + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=="; + +export function ImageWithFallback( + props: React.ImgHTMLAttributes, +) { + const [didError, setDidError] = useState(false); + + const handleError = () => { + setDidError(true); + }; + + const { src, alt, style, className, ...rest } = props; + + return didError ? ( +
+
+ Unavailable +
+
+ ) : ( + {alt} + ); +} diff --git a/frontend/src/landing/app/components/ui/accordion.tsx b/frontend/src/landing/app/components/ui/accordion.tsx new file mode 100644 index 00000000..bd6b1e33 --- /dev/null +++ b/frontend/src/landing/app/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDownIcon } from "lucide-react"; + +import { cn } from "./utils"; + +function Accordion({ + ...props +}: React.ComponentProps) { + return ; +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + + ); +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ); +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/frontend/src/landing/app/components/ui/alert-dialog.tsx b/frontend/src/landing/app/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..875b8df4 --- /dev/null +++ b/frontend/src/landing/app/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client"; + +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "./utils"; +import { buttonVariants } from "./button"; + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/frontend/src/landing/app/components/ui/alert.tsx b/frontend/src/landing/app/components/ui/alert.tsx new file mode 100644 index 00000000..9c35976c --- /dev/null +++ b/frontend/src/landing/app/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "./utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Alert, AlertTitle, AlertDescription }; diff --git a/frontend/src/landing/app/components/ui/aspect-ratio.tsx b/frontend/src/landing/app/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000..c16d6bcb --- /dev/null +++ b/frontend/src/landing/app/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +"use client"; + +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return ; +} + +export { AspectRatio }; diff --git a/frontend/src/landing/app/components/ui/avatar.tsx b/frontend/src/landing/app/components/ui/avatar.tsx new file mode 100644 index 00000000..c9904518 --- /dev/null +++ b/frontend/src/landing/app/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client"; + +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "./utils"; + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/frontend/src/landing/app/components/ui/badge.tsx b/frontend/src/landing/app/components/ui/badge.tsx new file mode 100644 index 00000000..2ccc2c44 --- /dev/null +++ b/frontend/src/landing/app/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "./utils"; + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span"; + + return ( + + ); +} + +export { Badge, badgeVariants }; diff --git a/frontend/src/landing/app/components/ui/breadcrumb.tsx b/frontend/src/landing/app/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..8b535e47 --- /dev/null +++ b/frontend/src/landing/app/components/ui/breadcrumb.tsx @@ -0,0 +1,107 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "./utils"; + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return