From ca6c0b65f2e8f81a5efa6e5388828b99843d4ee8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 16:22:56 +0000 Subject: [PATCH 01/12] docs: define 10 pre-launch development goals for PoseLab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add PROJECT_GOALS.md covering all critical gaps identified through a full codebase audit — secrets management, backend API completion, rig consolidation, test suite, mobile UX, CI/CD, and more. Each goal includes current state, what to build, and acceptance criteria. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_016GhhvSFLMp3M5ubNM4mvTY --- PROJECT_GOALS.md | 239 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 PROJECT_GOALS.md diff --git a/PROJECT_GOALS.md b/PROJECT_GOALS.md new file mode 100644 index 0000000..f26e13f --- /dev/null +++ b/PROJECT_GOALS.md @@ -0,0 +1,239 @@ +# PoseLab — Pre-Launch Development Goals + +> Version: 1.2.0 | Target: Production Launch +> Scope: All goals must be complete before going live. + +--- + +## Goal 1 — Complete Backend API Integration + +**Current State:** Only 5 auth endpoints exist (`/api/auth`, `/api/signup`, `/api/sign-out`, `/api/forgot-password`, `/api/reset-password`). All community feed, gallery, creations, and pose data are mocked on the frontend. + +**What to Build:** +- `POST /api/renders` — save a completed render (image + metadata) +- `GET /api/renders/:userId` — fetch a user's render history +- `GET /api/poses` — list community shared poses with pagination +- `POST /api/poses` — save and publish a pose preset +- `POST /api/posts` — submit a post to the community feed +- `GET /api/feed` — paginated home feed with like/bookmark counts +- `POST /api/feed/:postId/like` — toggle like on a post +- `POST /api/feed/:postId/bookmark` — toggle bookmark on a post +- `GET /api/profile/:userId` — public profile with stats +- `PATCH /api/profile` — update avatar, username, bio + +**Acceptance Criteria:** +- Every page that currently uses mock data is wired to a real API endpoint. +- API responses match Zod schemas defined in the frontend. +- Error states (404, 401, 500) are handled and shown in the UI. + +--- + +## Goal 2 — Secure Secrets & Environment Configuration + +**Current State:** Firebase API keys are hardcoded in `/src/firebase.ts`. No `.env` file strategy is in place. The backend URL is hardcoded as a plain string in `app.config.ts`. + +**What to Build:** +- Move all secrets into `.env` files using Vite's `import.meta.env` pattern. + ``` + VITE_FIREBASE_API_KEY= + VITE_FIREBASE_AUTH_DOMAIN= + VITE_FIREBASE_PROJECT_ID= + VITE_API_BASE_URL= + ``` +- Add `.env.example` with all required keys (no values) committed to the repo. +- Add `.env` and `.env.local` to `.gitignore`. +- Replace all hardcoded keys/URLs in source files with `import.meta.env.*` references. +- Configure CI/CD secrets in the deployment environment. + +**Acceptance Criteria:** +- Zero secrets committed in source code. +- App builds correctly using env vars injected at build time. +- New developer can run the project after copying `.env.example` and filling values. + +--- + +## Goal 3 — Consolidate the Character Rig System + +**Current State:** Three separate, parallel rig implementations exist: +1. `standard/MinecraftCharacter.tsx` — rigid body with basic joints +2. `bendable/BendableMinecraftCharacter.tsx` — flexible skeleton with `SkeletonSystem`, `ArmSystem`, `LegSystem` +3. `rigid-system/NewMinecraftCharacter.tsx` — a third approach with `minecraftRig.ts` + +This triples the maintenance surface, causes inconsistent behavior across pose modes, and confuses the pose-preset format. + +**What to Build:** +- Audit all three rigs: document what each supports (joint count, facial controls, attachment points). +- Pick one as the canonical rig or merge the best features into a single `MinecraftRig` abstraction. +- Migrate all pose presets into a single shared `posePresets.ts` format compatible with the chosen rig. +- Delete the two deprecated rig directories after migration. +- Update `CharacterViewer.tsx` to use only the canonical rig. + +**Acceptance Criteria:** +- One rig system used across the entire app. +- All existing preset poses (standing, running, walking, combat, sitting, waving) work correctly. +- Facial expression controls and attachment points remain functional. + +--- + +## Goal 4 — Implement Real Render Persistence & Gallery + +**Current State:** The render export pipeline in `renderUtils.ts` correctly generates PNG output and downloads it locally. However, renders are never saved to a backend or linked to the user's account. `myCreations/` shows a mock gallery. + +**What to Build:** +- After a successful render, upload the PNG to cloud storage (Firebase Storage or equivalent). +- Save render metadata (image URL, pose snapshot, skin URL, timestamp, render settings) via `POST /api/renders`. +- Wire `myCreations/` to `GET /api/renders/:userId` with real data. +- Support filtering by date and render type in the gallery. +- Add delete-render capability (`DELETE /api/renders/:renderId`). +- Show a per-render detail view with the pose settings used. + +**Acceptance Criteria:** +- A render survives a page refresh and is visible in My Creations. +- A user logging in on a second device sees their full render history. +- Deleting a render removes it from both the UI and storage. + +--- + +## Goal 5 — Complete Creator Space + +**Current State:** The `/creatorSpace` route shows a `` placeholder. + +**What to Build:** +- **Creator Dashboard:** stats on their published poses (views, saves, likes) and renders. +- **Pose Publishing:** a flow to take a current in-editor pose, add a name/description/tags, and publish it to the community pose library. +- **Creator Profile Badge:** display "Creator" badge on profile once a pose is published. +- **Creator Application Review:** wire the existing creator application form (`/account/creatorApplication`) to a backend endpoint so submissions are stored and reviewable. +- **Pose Management:** list of their published poses with edit/unpublish controls. + +**Acceptance Criteria:** +- A user can publish a pose preset from the editor and see it appear in the Creator Space. +- Stats update when other users save or like their poses. +- Creator application submissions are persisted to the backend. + +--- + +## Goal 6 — Add a Test Suite + +**Current State:** Zero test files exist. No Jest, Vitest, or Testing Library setup. + +**What to Build:** +- Install and configure **Vitest** + **React Testing Library** (aligns with Vite). +- Unit tests (target: all utility functions): + - `renderUtils.ts` — resolution calculation, quality preset application + - `pbrMaterials.ts` — material creation + - `posePresets.ts` — preset shape validation + - Auth store (`authStore.ts`) — sign-in, sign-out, token refresh logic +- Component tests: + - `PoseControls` — slider updates propagate to pose state + - `RenderDialog` — size/quality selection updates settings correctly + - Auth forms — validation errors appear on invalid input +- Integration tests: + - Sign-in flow (mocked API): error state, success redirect + - Render export: mock canvas, verify download is triggered + +**Acceptance Criteria:** +- `npm test` runs the suite without errors. +- Core utility functions have ≥80% line coverage. +- CI pipeline runs tests on every push and fails the build on test failure. + +--- + +## Goal 7 — Performance Optimization for 3D Viewport + +**Current State:** The `CharacterViewer` loads all three rig systems eagerly, applies post-processing effects (Bloom, SSAO, Depth of Field) unconditionally, and `useDeviceQuality` adjusts quality after mount—causing a visible quality drop on load. + +**What to Build:** +- **Lazy-load rig components:** use `React.lazy` + `Suspense` for `BendableMinecraftCharacter` and `NewMinecraftCharacter`; only load the active rig. +- **Pre-mount quality detection:** run device quality detection before the scene mounts so the correct settings are applied from frame one. +- **Post-processing gating:** disable SSAO and Depth of Field on low/medium quality profiles; only enable on high/ultra. +- **Texture atlas caching:** cache the Minecraft skin canvas texture so re-uploads on the same skin are instant. +- **Shadow map optimization:** use `CascadedShadowMap` for better quality at lower cost on supported devices. +- Measure: target ≥60 fps on mid-range hardware (integrated GPU). + +**Acceptance Criteria:** +- No quality-drop flash on initial load. +- Low/medium quality profiles maintain ≥60 fps on integrated GPU (tested in Chrome DevTools CPU throttle 4×). +- Switching between rig types does not cause a full scene remount. + +--- + +## Goal 8 — Mobile Experience & Touch Controls + +**Current State:** The UI is responsive at a CSS level, but the 3D pose editor is unusable on mobile: orbit controls require mouse drag, pose sliders stack in a way that pushes the viewport off screen, and the render dialog is not scrollable on small screens. + +**What to Build:** +- **Touch orbit controls:** enable `OrbitControls` touch events (pinch-to-zoom, one-finger rotate, two-finger pan) in `CharacterViewer`. +- **Collapsible control panels:** on screens <768px, all sidebar panels (Pose, Lighting, Quality) default to collapsed; expand on tap. +- **Bottom sheet pose controls:** on mobile, open pose sliders in a swipeable bottom sheet instead of a sidebar. +- **Responsive render dialog:** make `RenderDialog` scrollable and limit default size presets to max device resolution. +- **Viewport height fix:** use `dvh` units (dynamic viewport height) to prevent the control panel from being clipped by mobile browser chrome. + +**Acceptance Criteria:** +- Full pose editing and render export flow is completable on a 390px-wide viewport (iPhone 14 Pro). +- Pinch-to-zoom and rotate work correctly in the 3D viewport on a touch device. +- No layout overflow or hidden controls on mobile. + +--- + +## Goal 9 — Community Feed with Real-Time Data & Moderation + +**Current State:** The Home feed is fully mocked (static post data). Stories, trending challenges, and post interactions (like, bookmark, follow) fire no API calls. There is no content moderation layer. + +**What to Build:** +- Wire all feed interactions to real API endpoints (like, bookmark, follow, unfollow). +- Implement infinite scroll pagination for the feed (`GET /api/feed?page=&limit=`). +- **Stories:** connect story carousel to real user renders (`GET /api/stories`). +- **Trending challenges:** pull from `GET /api/challenges` with view/submission counts. +- **Basic moderation:** add a "Report" option on posts and renders; store reports via `POST /api/reports`. Flag reported content for admin review. +- **Notifications:** real-time badge on new likes/follows using Firebase Realtime Database or WebSocket. + +**Acceptance Criteria:** +- Feed loads real posts and updates on interaction without page refresh. +- Infinite scroll loads the next page when the user reaches the bottom. +- A reported post is flagged in the backend and the reporter sees a confirmation. + +--- + +## Goal 10 — CI/CD Pipeline & Production Readiness + +**Current State:** No CI/CD configuration exists in the repository. Deployment process, environment promotion, and release strategy are undefined. No error monitoring or analytics beyond Firebase Analytics. + +**What to Build:** +- **GitHub Actions workflow** (`.github/workflows/ci.yml`): + - Trigger: every push and PR to `main`. + - Steps: install → lint (ESLint) → type-check (tsc --noEmit) → test (Vitest) → build. + - Block merges to `main` if any step fails. +- **Deployment workflow** (`.github/workflows/deploy.yml`): + - Trigger: push to `main` (after CI passes). + - Build Vite production bundle with injected env vars. + - Deploy to hosting (Firebase Hosting, Vercel, or equivalent). +- **Error monitoring:** integrate **Sentry** for frontend error tracking. Capture unhandled exceptions and failed renders with user context. +- **Structured logging:** add a centralized error boundary in `App.tsx` that reports to Sentry and shows a user-friendly fallback UI. +- **Health check:** expose a `/health` endpoint on the backend that CI can ping post-deploy to verify the deployment succeeded. + +**Acceptance Criteria:** +- Every pull request to `main` runs linting, type-check, and tests automatically. +- Merging to `main` triggers an automatic deployment. +- Sentry captures and groups frontend errors in production. +- Build time stays under 60 seconds. + +--- + +## Priority Order + +| # | Goal | Priority | Effort | +|---|------|----------|--------| +| 2 | Secure Secrets & Env Config | Critical | Small | +| 1 | Complete Backend API Integration | Critical | Large | +| 4 | Render Persistence & Gallery | High | Medium | +| 3 | Consolidate Character Rig System | High | Medium | +| 10 | CI/CD Pipeline & Production Readiness | High | Small | +| 6 | Add a Test Suite | High | Medium | +| 7 | Performance Optimization | Medium | Medium | +| 8 | Mobile Experience & Touch Controls | Medium | Medium | +| 5 | Complete Creator Space | Medium | Large | +| 9 | Community Feed with Real-Time Data | Medium | Large | + +--- + +*Goals 2 and 10 are prerequisites for all others — secure config must be in place before any real data flows, and CI must be running before feature development scales up.* From 548b4a4ea73f507165bbeac88ae3665708d5f949 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 16:29:23 +0000 Subject: [PATCH 02/12] feat: migrate secrets to environment variables (Goal 2) Replace hardcoded Firebase keys and app URLs with VITE_* env vars via import.meta.env, add .env.example template, update .gitignore, and add TypeScript declarations for all new env variables. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_016GhhvSFLMp3M5ubNM4mvTY --- .env.example | 12 ++++++++++++ .gitignore | 7 ++++++- src/configs/app.config.ts | 4 ++-- src/firebase.ts | 23 ++++++++--------------- src/vite-env.d.ts | 16 ++++++++++++++++ vite.config.ts | 2 +- 6 files changed, 45 insertions(+), 19 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..81f4b67 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Firebase Configuration +VITE_FIREBASE_API_KEY=your_firebase_api_key_here +VITE_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com +VITE_FIREBASE_PROJECT_ID=your_project_id +VITE_FIREBASE_STORAGE_BUCKET=your_project.firebasestorage.app +VITE_FIREBASE_MESSAGING_SENDER_ID=your_sender_id +VITE_FIREBASE_APP_ID=your_app_id +VITE_FIREBASE_MEASUREMENT_ID=your_measurement_id + +# App Configuration +VITE_API_BASE_URL=https://poselab-backend.nexiotech.cloud +VITE_APP_URL=https://poselab.nexiotech.cloud diff --git a/.gitignore b/.gitignore index 6fb2679..ef5a185 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ node_modules/ -src/firebase.ts \ No newline at end of file +.env +.env.local +.env.*.local +build/ +dist/ +.DS_Store diff --git a/src/configs/app.config.ts b/src/configs/app.config.ts index fe95a58..d744a60 100644 --- a/src/configs/app.config.ts +++ b/src/configs/app.config.ts @@ -14,10 +14,10 @@ export type AppConfig = { const appConfig: AppConfig = { appVersion: '0.0.1', - appUrl: 'https://poselab.nexiotech.cloud', + appUrl: import.meta.env.VITE_APP_URL || 'https://poselab.nexiotech.cloud', appName: 'PoseLab', appDescription: 'PoseLab – Minecraft Skin Posing & Render Tool', - apiBaseUrl: 'https://poselab-backend.nexiotech.cloud', + apiBaseUrl: import.meta.env.VITE_API_BASE_URL || 'https://poselab-backend.nexiotech.cloud', apiPrefix: '/api', authenticatedEntryPath: '/home', unAuthenticatedEntryPath: '/sign-in', diff --git a/src/firebase.ts b/src/firebase.ts index 1b35482..9270dcd 100644 --- a/src/firebase.ts +++ b/src/firebase.ts @@ -1,31 +1,24 @@ -// src/firebase.ts import { initializeApp } from "firebase/app"; import { getAnalytics, isSupported } from "firebase/analytics"; import { getAuth } from 'firebase/auth' - const firebaseConfig = { - apiKey: "AIzaSyDlxnhkCH_m4CVVCdjgzF7BAS4YAk6Zn10", - authDomain: "poselab-584a8.firebaseapp.com", - projectId: "poselab-584a8", - storageBucket: "poselab-584a8.firebasestorage.app", - messagingSenderId: "350044736479", - appId: "1:350044736479:web:aa43b1ceb0622a10a9526a", - measurementId: "G-ZL15YJRKHY" + apiKey: import.meta.env.VITE_FIREBASE_API_KEY, + authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, + projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, + storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, + messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID, + appId: import.meta.env.VITE_FIREBASE_APP_ID, + measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID, }; -// Initialize Firebase const app = initializeApp(firebaseConfig); -const auth = getAuth(app) +const auth = getAuth(app) -// Initialize Analytics (only if supported in this environment) let analytics: ReturnType | undefined; isSupported().then((yes) => { if (yes) { analytics = getAnalytics(app); - console.log("Firebase Analytics initialized 🚀"); - } else { - console.log("Analytics not supported in this environment"); } }); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..32afda4 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,17 @@ /// + +interface ImportMetaEnv { + readonly VITE_FIREBASE_API_KEY: string + readonly VITE_FIREBASE_AUTH_DOMAIN: string + readonly VITE_FIREBASE_PROJECT_ID: string + readonly VITE_FIREBASE_STORAGE_BUCKET: string + readonly VITE_FIREBASE_MESSAGING_SENDER_ID: string + readonly VITE_FIREBASE_APP_ID: string + readonly VITE_FIREBASE_MEASUREMENT_ID: string + readonly VITE_API_BASE_URL: string + readonly VITE_APP_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/vite.config.ts b/vite.config.ts index 58243dc..62a334a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ server: { proxy: { '/api': { - target: 'http://localhost:3000', + target: process.env.VITE_API_BASE_URL || 'https://poselab-backend.nexiotech.cloud', changeOrigin: true, secure: false } From 039cda68c63f8903f33920b7bf7980759c0aa05c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 16:29:38 +0000 Subject: [PATCH 03/12] chore: add .claude/ to .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6fb2679..ef3a987 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -src/firebase.ts \ No newline at end of file +src/firebase.ts +.claude/ \ No newline at end of file From c188a90bb337b63c67b2d460a45570447f29b231 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 16:30:07 +0000 Subject: [PATCH 04/12] feat: add CI/CD pipeline, Firebase Hosting config, and error boundary (Goal 10) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_016GhhvSFLMp3M5ubNM4mvTY --- .env.example | 18 +++++ .firebaserc | 5 ++ .github/workflows/ci.yml | 103 ++++++++++++++++++++++++ firebase.json | 7 ++ src/App.tsx | 21 ++--- src/components/shared/ErrorBoundary.tsx | 66 +++++++++++++++ src/components/shared/index.tsx | 1 + src/lib/sentry.ts | 16 ++++ 8 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 .env.example create mode 100644 .firebaserc create mode 100644 .github/workflows/ci.yml create mode 100644 firebase.json create mode 100644 src/components/shared/ErrorBoundary.tsx create mode 100644 src/lib/sentry.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3fa7c83 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# Firebase configuration +# Get these values from your Firebase project settings +VITE_FIREBASE_API_KEY=your_firebase_api_key_here +VITE_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com +VITE_FIREBASE_PROJECT_ID=your_firebase_project_id +VITE_FIREBASE_STORAGE_BUCKET=your_project.appspot.com +VITE_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id +VITE_FIREBASE_APP_ID=your_firebase_app_id +VITE_FIREBASE_MEASUREMENT_ID=your_measurement_id + +# API configuration +VITE_API_BASE_URL=https://your-api-base-url.example.com +VITE_APP_URL=https://your-app-url.example.com + +# Sentry error monitoring (optional) +# Install @sentry/react first: npm install @sentry/react +# Get your DSN from https://sentry.io/settings//projects//keys/ +VITE_SENTRY_DSN=your_sentry_dsn_here diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..0966e86 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "poselab-584a8" + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..48e481e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,103 @@ +name: CI/CD + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + ci: + name: CI + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Cache node_modules + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Type check + run: npx tsc --noEmit + + - name: Test + # Only run tests if vitest is configured + if: hashFiles('vitest.config.ts') != '' + run: npm test -- --run + + - name: Build + env: + VITE_FIREBASE_API_KEY: ${{ secrets.VITE_FIREBASE_API_KEY }} + VITE_FIREBASE_AUTH_DOMAIN: ${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }} + VITE_FIREBASE_PROJECT_ID: ${{ secrets.VITE_FIREBASE_PROJECT_ID }} + VITE_FIREBASE_STORAGE_BUCKET: ${{ secrets.VITE_FIREBASE_STORAGE_BUCKET }} + VITE_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.VITE_FIREBASE_MESSAGING_SENDER_ID }} + VITE_FIREBASE_APP_ID: ${{ secrets.VITE_FIREBASE_APP_ID }} + VITE_FIREBASE_MEASUREMENT_ID: ${{ secrets.VITE_FIREBASE_MEASUREMENT_ID }} + VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }} + VITE_APP_URL: ${{ secrets.VITE_APP_URL }} + run: npm run build + + deploy: + name: Deploy to Firebase Hosting + runs-on: ubuntu-latest + needs: ci + # Only deploy on pushes to main, not on PRs + if: github.event_name == 'push' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Cache node_modules + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm ci + + - name: Build + env: + VITE_FIREBASE_API_KEY: ${{ secrets.VITE_FIREBASE_API_KEY }} + VITE_FIREBASE_AUTH_DOMAIN: ${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }} + VITE_FIREBASE_PROJECT_ID: ${{ secrets.VITE_FIREBASE_PROJECT_ID }} + VITE_FIREBASE_STORAGE_BUCKET: ${{ secrets.VITE_FIREBASE_STORAGE_BUCKET }} + VITE_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.VITE_FIREBASE_MESSAGING_SENDER_ID }} + VITE_FIREBASE_APP_ID: ${{ secrets.VITE_FIREBASE_APP_ID }} + VITE_FIREBASE_MEASUREMENT_ID: ${{ secrets.VITE_FIREBASE_MEASUREMENT_ID }} + VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }} + VITE_APP_URL: ${{ secrets.VITE_APP_URL }} + run: npm run build + + - name: Deploy to Firebase Hosting + uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} + channelId: live diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..e7c0a50 --- /dev/null +++ b/firebase.json @@ -0,0 +1,7 @@ +{ + "hosting": { + "public": "build", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [{"source": "**", "destination": "/index.html"}] + } +} diff --git a/src/App.tsx b/src/App.tsx index 15886e0..f34549a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import Theme from '@/components/template/Theme' import Layout from '@/components/layouts' import { AuthProvider } from '@/auth' import Views from '@/views' +import ErrorBoundary from '@/components/shared/ErrorBoundary' import appConfig from './configs/app.config' import './locales' import 'animate.css'; @@ -13,15 +14,17 @@ if (appConfig.enableMock) { function App() { return ( - - - - - - - - - + + + + + + + + + + + ) } diff --git a/src/components/shared/ErrorBoundary.tsx b/src/components/shared/ErrorBoundary.tsx new file mode 100644 index 0000000..8a1e37e --- /dev/null +++ b/src/components/shared/ErrorBoundary.tsx @@ -0,0 +1,66 @@ +import { Component, ErrorInfo, ReactNode } from 'react' + +interface Props { + children: ReactNode +} + +interface State { + hasError: boolean + error: Error | null +} + +class ErrorBoundary extends Component { + constructor(props: Props) { + super(props) + this.state = { hasError: false, error: null } + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, info) + + // Report to Sentry in production (dynamic import so it's safe if Sentry isn't installed) + if (import.meta.env.PROD) { + import('@sentry/react') + .then(({ captureException }) => { + captureException(error, { extra: { componentStack: info.componentStack } }) + }) + .catch(() => { + // Sentry not installed — silently ignore + }) + } + } + + handleReload = () => { + window.location.reload() + } + + render() { + if (this.state.hasError) { + return ( +
+
+

Something went wrong

+

+ An unexpected error occurred. Our team has been notified. You can try + reloading the page to recover. +

+ +
+
+ ) + } + + return this.props.children + } +} + +export default ErrorBoundary diff --git a/src/components/shared/index.tsx b/src/components/shared/index.tsx index 2040867..c2891e9 100644 --- a/src/components/shared/index.tsx +++ b/src/components/shared/index.tsx @@ -1,5 +1,6 @@ export { default as ActionLink } from './ActionLink' export { default as AuthorityCheck } from './AuthorityCheck' export { default as Container } from './Container' +export { default as ErrorBoundary } from './ErrorBoundary' export { default as NavToggle } from './NavToggle' export { default as PasswordInput } from './PasswordInput' diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts new file mode 100644 index 0000000..736f63b --- /dev/null +++ b/src/lib/sentry.ts @@ -0,0 +1,16 @@ +// Initialize Sentry for production error tracking +// Install @sentry/react before using: npm install @sentry/react +// Then call initSentry() in main.tsx before React renders + +export function initSentry() { + if (import.meta.env.PROD && import.meta.env.VITE_SENTRY_DSN) { + import('@sentry/react').then(({ init, browserTracingIntegration }) => { + init({ + dsn: import.meta.env.VITE_SENTRY_DSN, + integrations: [browserTracingIntegration()], + tracesSampleRate: 0.1, + environment: import.meta.env.MODE, + }) + }) + } +} From b4ab05630696b0594877ee0d92badf77fc9f0a16 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 16:32:58 +0000 Subject: [PATCH 05/12] feat: add Vitest test suite with unit and integration tests (Goal 6) Installs Vitest, @testing-library, and jsdom; wires up vitest.config.ts and src/test/setup.ts with Firebase/env mocks; adds 73 passing tests across appConfig, authMapper, cookiesStorage, authStore, qualitySettings, and AuthService (integration via axios-mock-adapter). Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_016GhhvSFLMp3M5ubNM4mvTY --- package-lock.json | 2319 ++++++++++++++++++++-- package.json | 13 +- src/test/integration/authService.test.ts | 159 ++ src/test/setup.ts | 36 + src/test/unit/appConfig.test.ts | 69 + src/test/unit/authMapper.test.ts | 134 ++ src/test/unit/authStore.test.ts | 167 ++ src/test/unit/cookiesStorage.test.ts | 86 + src/test/unit/qualitySettings.test.ts | 155 ++ vitest.config.ts | 20 + 10 files changed, 3042 insertions(+), 116 deletions(-) create mode 100644 src/test/integration/authService.test.ts create mode 100644 src/test/setup.ts create mode 100644 src/test/unit/appConfig.test.ts create mode 100644 src/test/unit/authMapper.test.ts create mode 100644 src/test/unit/authStore.test.ts create mode 100644 src/test/unit/cookiesStorage.test.ts create mode 100644 src/test/unit/qualitySettings.test.ts create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index 64a39f4..23c4453 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,6 +81,9 @@ "@eslint/js": "^9.17.0", "@tailwindcss/postcss": "^4.0.8", "@tailwindcss/typography": "^0.5.15", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/d3-fetch": "^3.0.7", "@types/d3-scale": "^4.0.8", "@types/js-cookie": "^3.0.6", @@ -96,6 +99,7 @@ "@typescript-eslint/eslint-plugin": "^8.5.0", "@typescript-eslint/parser": "^8.5.0", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^4.1.9", "color": "^4.2.3", "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", @@ -106,6 +110,7 @@ "eslint-plugin-react": "^7.37.3", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", + "jsdom": "^29.1.1", "postcss": "^8.4.45", "postcss-nesting": "^13.0.0", "prettier": "^3.3.3", @@ -113,7 +118,8 @@ "typescript": "^5.7.2", "typescript-eslint": "^8.18.2", "vite": "^6.0.5", - "vite-plugin-dynamic-import": "^1.5.0" + "vite-plugin-dynamic-import": "^1.5.0", + "vitest": "^4.1.9" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -125,6 +131,13 @@ "node": ">=0.10.0" } }, + "node_modules/@adobe/css-tools": { + "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", @@ -151,6 +164,57 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -301,18 +365,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -343,12 +407,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -431,18 +495,181 @@ } }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.7.tgz", + "integrity": "sha512-CmjJFQTFQx/U/xNJhSjCQ0ilpesPmNQ8+eOUeM/+kDOVW33qsIjeOXc27vrQDdWVkf83ZSWwtg7kXSUvKDJ8cQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@csstools/selector-resolve-nested": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-2.0.0.tgz", @@ -1174,6 +1401,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "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/@firebase/ai": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-1.4.1.tgz", @@ -2018,14 +2263,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "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.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "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" @@ -3425,6 +3672,13 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, + "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/@svgdotjs/svg.draggable.js": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.4.tgz", @@ -3834,6 +4088,107 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tiptap/core": { "version": "2.10.4", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.10.4.tgz", @@ -4225,6 +4580,14 @@ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", "license": "MIT" }, + "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", @@ -4266,6 +4629,17 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3-dsv": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", @@ -4304,6 +4678,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/draco3d": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", @@ -4796,20 +5177,137 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, - "node_modules/@webgpu/types": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.68.tgz", - "integrity": "sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA==", - "license": "BSD-3-Clause" - }, - "node_modules/@yr/monotone-cubic-spline": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", - "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", + "node_modules/@vitest/coverage-v8": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", + "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", + "dev": true, "license": "MIT", - "peer": true - }, - "node_modules/acorn": { + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.9", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.9", + "vitest": "4.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.68", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.68.tgz", + "integrity": "sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA==", + "license": "BSD-3-Clause" + }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", + "license": "MIT", + "peer": true + }, + "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", @@ -5069,12 +5567,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", + "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5373,6 +5900,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/character-entities": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", @@ -5583,6 +6120,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5724,6 +6282,20 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5799,6 +6371,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -5948,6 +6527,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "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": "MIT", + "peer": true + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -6777,27 +7364,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/espree": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", @@ -6869,6 +7435,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -6883,6 +7459,16 @@ "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -7383,6 +7969,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -7599,6 +8195,26 @@ "htmlparser2": "10.0.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -7769,6 +8385,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -8129,6 +8755,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -8288,6 +8921,45 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", @@ -8363,6 +9035,57 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -8830,6 +9553,17 @@ "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", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/maath": { "version": "0.10.8", "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", @@ -8841,15 +9575,41 @@ } }, "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "semver": "^7.5.3" }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/markdown-it": { @@ -9185,6 +9945,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -9673,10 +10440,20 @@ "node": ">= 0.6" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -9885,6 +10662,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -9982,6 +10773,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10012,6 +10829,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10147,6 +10971,44 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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/prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -10801,6 +11663,20 @@ } } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz", @@ -11105,6 +11981,19 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", @@ -11250,6 +12139,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -11323,6 +12219,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stats-gl": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", @@ -11349,6 +12252,13 @@ "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", "license": "MIT" }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -11524,6 +12434,19 @@ "node": ">=4" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -11559,6 +12482,19 @@ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -11592,6 +12528,13 @@ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -11669,6 +12612,81 @@ "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tippy.js": { "version": "6.3.7", "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", @@ -11678,6 +12696,26 @@ "@popperjs/core": "^2.9.0" } }, + "node_modules/tldts": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz", + "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.3" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz", + "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==", + "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", @@ -11690,6 +12728,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -11968,6 +13032,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -12344,6 +13418,143 @@ "magic-string": "^0.30.1" } }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -12357,6 +13568,19 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -12382,6 +13606,16 @@ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", "license": "MIT" }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -12405,6 +13639,31 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12506,6 +13765,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -12523,6 +13799,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -12668,6 +13961,12 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, + "@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true + }, "@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -12684,6 +13983,44 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "requires": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "requires": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + } + }, + "@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true + }, + "@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true + }, "@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -12791,14 +14128,14 @@ "dev": true }, "@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==" }, "@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==" }, "@babel/helper-validator-option": { "version": "7.25.9", @@ -12817,11 +14154,11 @@ } }, "@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "requires": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.29.7" } }, "@babel/plugin-transform-react-jsx-self": { @@ -12872,14 +14209,72 @@ } }, "@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "requires": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + } + }, + "@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true + }, + "@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "requires": { + "css-tree": "^3.0.0" + } + }, + "@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true + }, + "@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "requires": {} + }, + "@csstools/css-color-parser": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.7.tgz", + "integrity": "sha512-CmjJFQTFQx/U/xNJhSjCQ0ilpesPmNQ8+eOUeM/+kDOVW33qsIjeOXc27vrQDdWVkf83ZSWwtg7kXSUvKDJ8cQ==", + "dev": true, "requires": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" } }, + "@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "requires": {} + }, + "@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "requires": {} + }, + "@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true + }, "@csstools/selector-resolve-nested": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-2.0.0.tgz", @@ -13278,6 +14673,13 @@ "levn": "^0.4.1" } }, + "@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, + "requires": {} + }, "@firebase/ai": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-1.4.1.tgz", @@ -13876,14 +15278,14 @@ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" }, "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" }, "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -14599,6 +16001,12 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, + "@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 + }, "@svgdotjs/svg.draggable.js": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.4.tgz", @@ -14822,6 +16230,73 @@ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz", "integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==" }, + "@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "peer": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "peer": true, + "requires": { + "dequal": "^2.0.3" + } + } + } + }, + "@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "requires": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "dependencies": { + "dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + } + } + }, + "@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5" + } + }, + "@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "requires": {} + }, "@tiptap/core": { "version": "2.10.4", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.10.4.tgz", @@ -15028,6 +16503,13 @@ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==" }, + "@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, + "peer": true + }, "@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -15069,6 +16551,16 @@ "@babel/types": "^7.20.7" } }, + "@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "requires": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "@types/d3-dsv": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", @@ -15107,6 +16599,12 @@ "@types/ms": "*" } }, + "@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, "@types/draco3d": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", @@ -15458,17 +16956,97 @@ "prop-types": "^15.5.10" } }, - "@vitejs/plugin-react": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", - "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "requires": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + } + }, + "@vitest/coverage-v8": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", + "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.9", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + } + }, + "@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "requires": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + } + }, + "@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "requires": { + "tinyrainbow": "^3.1.0" + } + }, + "@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "requires": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + } + }, + "@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "requires": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + } + }, + "@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true + }, + "@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", "dev": true, "requires": { - "@babel/core": "^7.26.0", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" } }, "@webgpu/types": { @@ -15665,12 +17243,37 @@ "is-array-buffer": "^3.0.4" } }, + "assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true + }, "ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "ast-v8-to-istanbul": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", + "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + }, + "dependencies": { + "js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true + } + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -15844,6 +17447,12 @@ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==" }, + "chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true + }, "character-entities": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", @@ -15992,6 +17601,22 @@ "which": "^2.0.1" } }, + "css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "requires": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + } + }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -16088,6 +17713,16 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "requires": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + } + }, "data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -16134,6 +17769,12 @@ "ms": "2.1.2" } }, + "decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, "decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -16240,6 +17881,13 @@ "dequal": "^2.0.0" } }, + "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, + "peer": true + }, "dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -16593,21 +18241,6 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } } } }, @@ -16917,6 +18550,15 @@ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==" }, + "estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0" + } + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -16928,6 +18570,12 @@ "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" }, + "expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -17279,6 +18927,12 @@ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -17439,6 +19093,21 @@ "htmlparser2": "10.0.0" } }, + "html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "requires": { + "@exodus/bytes": "^1.6.0" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -17531,6 +19200,12 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -17744,6 +19419,12 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==" }, + "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", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -17842,6 +19523,33 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, "iterator.prototype": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", @@ -17899,6 +19607,43 @@ "argparse": "^2.0.1" } }, + "jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "requires": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true + } + } + }, "jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -18168,6 +19913,13 @@ "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", "requires": {} }, + "lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "peer": true + }, "maath": { "version": "0.10.8", "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", @@ -18175,12 +19927,32 @@ "requires": {} }, "magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "requires": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "requires": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "semver": "^7.5.3" } }, "markdown-it": { @@ -18446,6 +20218,12 @@ "@types/mdast": "^4.0.0" } }, + "mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true + }, "mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -18707,6 +20485,12 @@ "mime-db": "1.52.0" } }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -18848,6 +20632,12 @@ "es-object-atoms": "^1.0.0" } }, + "obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true + }, "optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -18917,6 +20707,23 @@ "lines-and-columns": "^1.1.6" } }, + "parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "requires": { + "entities": "^8.0.0" + }, + "dependencies": { + "entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true + } + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -18938,6 +20745,12 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, + "pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -19009,6 +20822,34 @@ "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "peer": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true + }, + "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, + "peer": true + } + } + }, "prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -19476,6 +21317,16 @@ "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", "requires": {} }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, "reflect.getprototypeof": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz", @@ -19678,6 +21529,15 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, "scheduler": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", @@ -19776,6 +21636,12 @@ "side-channel-map": "^1.0.1" } }, + "siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -19831,6 +21697,12 @@ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==" }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "stats-gl": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", @@ -19852,6 +21724,12 @@ "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" }, + "std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true + }, "stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -19980,6 +21858,15 @@ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -20007,6 +21894,15 @@ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -20027,6 +21923,12 @@ "use-sync-external-store": "^1.4.0" } }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -20085,6 +21987,49 @@ } } }, + "tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true + }, + "tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "requires": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "dependencies": { + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true + } + } + }, + "tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true + }, "tippy.js": { "version": "6.3.7", "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", @@ -20093,6 +22038,21 @@ "@popperjs/core": "^2.9.0" } }, + "tldts": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz", + "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==", + "dev": true, + "requires": { + "tldts-core": "^7.4.3" + } + }, + "tldts-core": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz", + "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -20102,6 +22062,24 @@ "is-number": "^7.0.0" } }, + "tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "requires": { + "tldts": "^7.0.5" + } + }, + "tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, "trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -20283,6 +22261,12 @@ "which-boxed-primitive": "^1.1.1" } }, + "undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true + }, "undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -20519,6 +22503,59 @@ "magic-string": "^0.30.1" } }, + "vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "requires": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "dependencies": { + "@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "requires": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + } + }, + "es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true + } + } + }, "void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -20529,6 +22566,15 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" }, + "w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "requires": { + "xml-name-validator": "^5.0.0" + } + }, "warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -20552,6 +22598,12 @@ "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==" }, + "webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true + }, "websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -20567,6 +22619,23 @@ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" }, + "whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true + }, + "whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "requires": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -20635,6 +22704,16 @@ "has-tostringtag": "^1.0.2" } }, + "why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "requires": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + } + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -20645,6 +22724,18 @@ "strip-ansi": "^6.0.0" } }, + "xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 65ecd8d..1623029 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,10 @@ "prettier:fix": "npm run prettier -- --write", "lint": "npx eslint", "lint:fix": "npm run lint -- --fix", - "format": "npm run prettier:fix && npm run lint:fix" + "format": "npm run prettier:fix && npm run lint:fix", + "test": "vitest", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@floating-ui/react": "^0.27.2", @@ -87,6 +90,9 @@ "@eslint/js": "^9.17.0", "@tailwindcss/postcss": "^4.0.8", "@tailwindcss/typography": "^0.5.15", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/d3-fetch": "^3.0.7", "@types/d3-scale": "^4.0.8", "@types/js-cookie": "^3.0.6", @@ -102,6 +108,7 @@ "@typescript-eslint/eslint-plugin": "^8.5.0", "@typescript-eslint/parser": "^8.5.0", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^4.1.9", "color": "^4.2.3", "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", @@ -112,6 +119,7 @@ "eslint-plugin-react": "^7.37.3", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", + "jsdom": "^29.1.1", "postcss": "^8.4.45", "postcss-nesting": "^13.0.0", "prettier": "^3.3.3", @@ -119,6 +127,7 @@ "typescript": "^5.7.2", "typescript-eslint": "^8.18.2", "vite": "^6.0.5", - "vite-plugin-dynamic-import": "^1.5.0" + "vite-plugin-dynamic-import": "^1.5.0", + "vitest": "^4.1.9" } } diff --git a/src/test/integration/authService.test.ts b/src/test/integration/authService.test.ts new file mode 100644 index 0000000..c921586 --- /dev/null +++ b/src/test/integration/authService.test.ts @@ -0,0 +1,159 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import axios from 'axios' +import MockAdapter from 'axios-mock-adapter' + +// Mock js-cookie (needed by cookiesStorage used in the store) +vi.mock('js-cookie', () => ({ + default: { + get: vi.fn(), + set: vi.fn(), + remove: vi.fn(), + }, +})) + +// We import AxiosBase and mock at the adapter level +import AxiosBase from '@/services/axios/AxiosBase' +import { apiSignIn, apiSignUp, apiSignOut } from '@/services/AuthService' +import endpointConfig from '@/configs/endpoint.config' +import type { ApiSignInResponse, ApiSignUpResponse } from '@/@types/auth' + +const mock = new MockAdapter(AxiosBase) + +beforeEach(() => { + mock.reset() +}) + +afterEach(() => { + mock.reset() +}) + +const mockSignInResponse: ApiSignInResponse = { + status: 200, + success: true, + Token: 'test-jwt-token', + User: { + uid: 'firebase-uid-001', + email: 'user@example.com', + phone: null, + provider: 'password', + createdAt: { _seconds: 1700000000, _nanoseconds: 0 }, + }, +} + +const mockSignUpResponse: ApiSignUpResponse = { + status: 201, + success: true, + Token: 'signup-jwt-token', + User: { + uid: 'firebase-uid-002', + email: 'newuser@example.com', + provider: 'password', + createdAt: '2024-01-01T00:00:00Z', + }, +} + +describe('apiSignIn', () => { + it('calls the signIn endpoint with correct payload structure', async () => { + mock.onPost(endpointConfig.signIn).reply(200, mockSignInResponse) + + await apiSignIn({ email: 'user@example.com', password: 'secret' }) + + expect(mock.history.post).toHaveLength(1) + const requestBody = JSON.parse(mock.history.post[0].data) + expect(requestBody.login_type_id).toBe(4) + expect(requestBody.payload.email).toBe('user@example.com') + expect(requestBody.payload.password).toBe('secret') + }) + + it('returns mapped user and token on success', async () => { + mock.onPost(endpointConfig.signIn).reply(200, mockSignInResponse) + + const result = await apiSignIn({ email: 'user@example.com', password: 'secret' }) + + expect(result.token).toBe('test-jwt-token') + expect(result.user.userId).toBe('firebase-uid-001') + expect(result.user.email).toBe('user@example.com') + expect(result.user.authority).toEqual(['USER']) + }) + + it('throws on 401 Unauthorized', async () => { + mock.onPost(endpointConfig.signIn).reply(401, { message: 'Invalid credentials' }) + + await expect( + apiSignIn({ email: 'wrong@example.com', password: 'wrongpass' }), + ).rejects.toThrow() + }) + + it('throws on 500 server error', async () => { + mock.onPost(endpointConfig.signIn).reply(500, { message: 'Internal Server Error' }) + + await expect( + apiSignIn({ email: 'user@example.com', password: 'password' }), + ).rejects.toThrow() + }) + + it('throws on network error', async () => { + mock.onPost(endpointConfig.signIn).networkError() + + await expect( + apiSignIn({ email: 'user@example.com', password: 'password' }), + ).rejects.toThrow() + }) +}) + +describe('apiSignUp', () => { + it('calls the signUp endpoint with correct payload', async () => { + mock.onPost(endpointConfig.signUp).reply(201, mockSignUpResponse) + + await apiSignUp({ userName: 'newuser', email: 'newuser@example.com', password: 'newpass' }) + + expect(mock.history.post).toHaveLength(1) + const requestBody = JSON.parse(mock.history.post[0].data) + expect(requestBody.username).toBe('newuser') + expect(requestBody.email).toBe('newuser@example.com') + expect(requestBody.password).toBe('newpass') + }) + + it('returns mapped user and token on successful sign-up', async () => { + mock.onPost(endpointConfig.signUp).reply(201, mockSignUpResponse) + + const result = await apiSignUp({ userName: 'newuser', email: 'newuser@example.com', password: 'newpass' }) + + expect(result.token).toBe('signup-jwt-token') + expect(result.user.userId).toBe('firebase-uid-002') + expect(result.user.email).toBe('newuser@example.com') + }) + + it('throws on 400 validation error', async () => { + mock.onPost(endpointConfig.signUp).reply(400, { message: 'Email already in use' }) + + await expect( + apiSignUp({ userName: 'newuser', email: 'existing@example.com', password: 'pass' }), + ).rejects.toThrow() + }) +}) + +describe('apiSignOut', () => { + it('calls the signOut endpoint', async () => { + mock.onPost(endpointConfig.signOut).reply(200, { success: true }) + + await apiSignOut() + + expect(mock.history.post).toHaveLength(1) + expect(mock.history.post[0].url).toBe(endpointConfig.signOut) + }) + + it('uses POST method for sign-out', async () => { + mock.onPost(endpointConfig.signOut).reply(200, {}) + + await apiSignOut() + + expect(mock.history.post[0].method).toBe('post') + }) + + it('throws if server returns an error on sign-out', async () => { + mock.onPost(endpointConfig.signOut).reply(500, { message: 'Server error' }) + + await expect(apiSignOut()).rejects.toThrow() + }) +}) diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..1bea2f9 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,36 @@ +import '@testing-library/jest-dom' +import { vi } from 'vitest' + +// Mock Firebase to prevent initialization errors in tests +vi.mock('firebase/app', () => ({ + initializeApp: vi.fn(() => ({})), +})) +vi.mock('firebase/auth', () => ({ + getAuth: vi.fn(() => ({})), + signInWithEmailAndPassword: vi.fn(), + createUserWithEmailAndPassword: vi.fn(), + signOut: vi.fn(), + onAuthStateChanged: vi.fn(), +})) +vi.mock('firebase/analytics', () => ({ + getAnalytics: vi.fn(), + isSupported: vi.fn(() => Promise.resolve(false)), +})) + +// Mock import.meta.env +Object.defineProperty(import.meta, 'env', { + value: { + VITE_FIREBASE_API_KEY: 'test-key', + VITE_FIREBASE_AUTH_DOMAIN: 'test.firebaseapp.com', + VITE_FIREBASE_PROJECT_ID: 'test-project', + VITE_FIREBASE_STORAGE_BUCKET: 'test.firebasestorage.app', + VITE_FIREBASE_MESSAGING_SENDER_ID: '123456', + VITE_FIREBASE_APP_ID: 'test-app-id', + VITE_FIREBASE_MEASUREMENT_ID: 'test-measurement', + VITE_API_BASE_URL: 'https://test-api.example.com', + VITE_APP_URL: 'https://test.example.com', + PROD: false, + MODE: 'test', + }, + writable: true, +}) diff --git a/src/test/unit/appConfig.test.ts b/src/test/unit/appConfig.test.ts new file mode 100644 index 0000000..38e298f --- /dev/null +++ b/src/test/unit/appConfig.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from 'vitest' +import appConfig from '@/configs/app.config' +import type { AppConfig } from '@/configs/app.config' + +describe('appConfig', () => { + it('has all required fields', () => { + expect(appConfig).toHaveProperty('appUrl') + expect(appConfig).toHaveProperty('appName') + expect(appConfig).toHaveProperty('apiBaseUrl') + expect(appConfig).toHaveProperty('apiPrefix') + expect(appConfig).toHaveProperty('authenticatedEntryPath') + expect(appConfig).toHaveProperty('unAuthenticatedEntryPath') + expect(appConfig).toHaveProperty('locale') + expect(appConfig).toHaveProperty('accessTokenPersistStrategy') + expect(appConfig).toHaveProperty('enableMock') + }) + + it('has apiPrefix set to /api', () => { + expect(appConfig.apiPrefix).toBe('/api') + }) + + it('has accessTokenPersistStrategy set to cookies', () => { + expect(appConfig.accessTokenPersistStrategy).toBe('cookies') + }) + + it('has a non-empty appName', () => { + expect(typeof appConfig.appName).toBe('string') + expect(appConfig.appName.length).toBeGreaterThan(0) + }) + + it('has a valid authenticatedEntryPath starting with /', () => { + expect(appConfig.authenticatedEntryPath).toMatch(/^\//) + }) + + it('has a valid unAuthenticatedEntryPath starting with /', () => { + expect(appConfig.unAuthenticatedEntryPath).toMatch(/^\//) + }) + + it('has a locale string', () => { + expect(typeof appConfig.locale).toBe('string') + expect(appConfig.locale.length).toBeGreaterThan(0) + }) + + it('accepts valid AppConfig shape', () => { + const validConfig: AppConfig = { + appUrl: 'https://example.com', + appName: 'TestApp', + apiBaseUrl: 'https://api.example.com', + apiPrefix: '/api/v1', + authenticatedEntryPath: '/dashboard', + unAuthenticatedEntryPath: '/login', + locale: 'en', + accessTokenPersistStrategy: 'localStorage', + enableMock: false, + } + // TypeScript check — this compiles only if the shape is correct + expect(validConfig.appName).toBe('TestApp') + expect(validConfig.accessTokenPersistStrategy).toBe('localStorage') + }) + + it('accessTokenPersistStrategy is one of the valid union types', () => { + const validStrategies: AppConfig['accessTokenPersistStrategy'][] = [ + 'localStorage', + 'sessionStorage', + 'cookies', + ] + expect(validStrategies).toContain(appConfig.accessTokenPersistStrategy) + }) +}) diff --git a/src/test/unit/authMapper.test.ts b/src/test/unit/authMapper.test.ts new file mode 100644 index 0000000..83d62cf --- /dev/null +++ b/src/test/unit/authMapper.test.ts @@ -0,0 +1,134 @@ +import { describe, it, expect, vi } from 'vitest' +import { mapApiResponseToUser } from '@/utils/authMapper' +import type { ApiSignInResponse, ApiSignUpResponse } from '@/@types/auth' + +describe('mapApiResponseToUser', () => { + const validSignInResponse: ApiSignInResponse = { + status: 200, + success: true, + Token: 'abc123token', + User: { + uid: 'user-uid-1', + email: 'test@example.com', + phone: null, + provider: 'password', + createdAt: { _seconds: 1700000000, _nanoseconds: 0 }, + }, + } + + const validSignUpResponse: ApiSignUpResponse = { + status: 201, + success: true, + Token: 'signupToken456', + User: { + uid: 'user-uid-2', + email: 'newuser@example.com', + provider: 'password', + createdAt: '2024-01-01T00:00:00Z', + }, + } + + describe('with a sign-in response', () => { + it('extracts the token from the Token field', () => { + const result = mapApiResponseToUser(validSignInResponse) + expect(result.token).toBe('abc123token') + }) + + it('maps uid to userId', () => { + const result = mapApiResponseToUser(validSignInResponse) + expect(result.user.userId).toBe('user-uid-1') + }) + + it('maps email to email and userName', () => { + const result = mapApiResponseToUser(validSignInResponse) + expect(result.user.email).toBe('test@example.com') + expect(result.user.userName).toBe('test@example.com') + }) + + it('sets default authority to [USER]', () => { + const result = mapApiResponseToUser(validSignInResponse) + expect(result.user.authority).toEqual(['USER']) + }) + + it('sets avatar to empty string by default', () => { + const result = mapApiResponseToUser(validSignInResponse) + expect(result.user.avatar).toBe('') + }) + }) + + describe('with a sign-up response', () => { + it('extracts the token from the Token field', () => { + const result = mapApiResponseToUser(validSignUpResponse) + expect(result.token).toBe('signupToken456') + }) + + it('maps uid to userId', () => { + const result = mapApiResponseToUser(validSignUpResponse) + expect(result.user.userId).toBe('user-uid-2') + }) + + it('maps email correctly', () => { + const result = mapApiResponseToUser(validSignUpResponse) + expect(result.user.email).toBe('newuser@example.com') + }) + + it('returns authority as [USER]', () => { + const result = mapApiResponseToUser(validSignUpResponse) + expect(result.user.authority).toEqual(['USER']) + }) + }) + + describe('when User object is missing', () => { + it('returns a fallback user with empty strings', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const badResponse = { ...validSignInResponse, User: undefined as any } + const result = mapApiResponseToUser(badResponse) + expect(result.user.userId).toBe('') + expect(result.user.userName).toBe('Unknown') + expect(result.user.email).toBe('') + expect(result.user.authority).toEqual(['USER']) + consoleSpy.mockRestore() + }) + + it('logs an error when User object is missing', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const badResponse = { ...validSignInResponse, User: undefined as any } + mapApiResponseToUser(badResponse) + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('API Response missing User object'), + expect.anything(), + ) + consoleSpy.mockRestore() + }) + }) + + describe('when email is null (phone-only user)', () => { + it('falls back to phone if email is null', () => { + const phoneUser: ApiSignInResponse = { + ...validSignInResponse, + User: { + ...validSignInResponse.User, + email: null, + phone: '+1234567890', + }, + } + const result = mapApiResponseToUser(phoneUser) + expect(result.user.email).toBe('') + // userName falls back to phone when email is null + expect(result.user.userName).toBe('+1234567890') + }) + + it('uses "User" as fallback userName when both email and phone are absent', () => { + const noEmailNoPhone: ApiSignInResponse = { + ...validSignInResponse, + User: { + ...validSignInResponse.User, + email: null, + phone: null, + }, + } + const result = mapApiResponseToUser(noEmailNoPhone) + expect(result.user.userName).toBe('User') + }) + }) +}) diff --git a/src/test/unit/authStore.test.ts b/src/test/unit/authStore.test.ts new file mode 100644 index 0000000..c016b71 --- /dev/null +++ b/src/test/unit/authStore.test.ts @@ -0,0 +1,167 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +// Mock js-cookie before importing store (cookiesStorage uses it) +vi.mock('js-cookie', () => ({ + default: { + get: vi.fn(), + set: vi.fn(), + remove: vi.fn(), + }, +})) + +import { useSessionUser, useToken } from '@/store/authStore' +import { TOKEN_NAME_IN_STORAGE } from '@/constants/api.constant' +import type { User } from '@/@types/auth' + +// Reset zustand store state between tests +beforeEach(() => { + useSessionUser.setState({ + session: { signedIn: false }, + user: { + avatar: '', + userName: '', + email: '', + authority: [], + }, + }) + + // Clear localStorage/sessionStorage between tests + localStorage.clear() + sessionStorage.clear() +}) + +describe('useSessionUser store — initial state', () => { + it('starts with signedIn = false', () => { + const { session } = useSessionUser.getState() + expect(session.signedIn).toBe(false) + }) + + it('starts with an empty user', () => { + const { user } = useSessionUser.getState() + expect(user.userName).toBe('') + expect(user.email).toBe('') + expect(user.avatar).toBe('') + expect(user.authority).toEqual([]) + }) +}) + +describe('useSessionUser store — setSessionSignedIn', () => { + it('sets signedIn to true', () => { + useSessionUser.getState().setSessionSignedIn(true) + expect(useSessionUser.getState().session.signedIn).toBe(true) + }) + + it('sets signedIn back to false', () => { + useSessionUser.getState().setSessionSignedIn(true) + useSessionUser.getState().setSessionSignedIn(false) + expect(useSessionUser.getState().session.signedIn).toBe(false) + }) +}) + +describe('useSessionUser store — setUser', () => { + it('updates the user state with new values', () => { + const newUser: User = { + userId: 'uid-123', + userName: 'John', + email: 'john@example.com', + avatar: '/avatar.png', + authority: ['USER'], + } + useSessionUser.getState().setUser(newUser) + const { user } = useSessionUser.getState() + expect(user.userId).toBe('uid-123') + expect(user.userName).toBe('John') + expect(user.email).toBe('john@example.com') + expect(user.avatar).toBe('/avatar.png') + expect(user.authority).toEqual(['USER']) + }) + + it('merges partial user updates without erasing other fields', () => { + // Set a full user first + useSessionUser.getState().setUser({ + userId: 'uid-1', + userName: 'Alice', + email: 'alice@example.com', + avatar: '/alice.png', + authority: ['ADMIN'], + }) + // Now update only the email + useSessionUser.getState().setUser({ email: 'newalice@example.com' }) + const { user } = useSessionUser.getState() + expect(user.userName).toBe('Alice') + expect(user.email).toBe('newalice@example.com') + }) +}) + +describe('useToken — setToken and getToken', () => { + it('setToken stores token in localStorage', () => { + const { setToken } = useToken() + setToken('my-access-token') + expect(localStorage.getItem(TOKEN_NAME_IN_STORAGE)).toBe('my-access-token') + }) + + it('setToken stores token in sessionStorage', () => { + const { setToken } = useToken() + setToken('session-token') + expect(sessionStorage.getItem(TOKEN_NAME_IN_STORAGE)).toBe('session-token') + }) + + it('setToken with empty string clears localStorage', () => { + localStorage.setItem(TOKEN_NAME_IN_STORAGE, 'old-token') + const { setToken } = useToken() + setToken('') + expect(localStorage.getItem(TOKEN_NAME_IN_STORAGE)).toBeNull() + }) + + it('setToken with empty string clears sessionStorage', () => { + sessionStorage.setItem(TOKEN_NAME_IN_STORAGE, 'old-token') + const { setToken } = useToken() + setToken('') + expect(sessionStorage.getItem(TOKEN_NAME_IN_STORAGE)).toBeNull() + }) + + it('token is readable from localStorage after setToken', () => { + localStorage.setItem(TOKEN_NAME_IN_STORAGE, 'readable-token') + const { token } = useToken() + expect(token).toBe('readable-token') + }) + + it('token is null/falsy when no token is stored', () => { + localStorage.clear() + sessionStorage.clear() + const { token } = useToken() + // With js-cookie mocked to return undefined, token should be falsy + expect(token).toBeFalsy() + }) +}) + +describe('sign-out flow', () => { + it('clears user and session state on sign-out', () => { + // Set up a signed-in user + useSessionUser.getState().setSessionSignedIn(true) + useSessionUser.getState().setUser({ + userId: 'uid-1', + userName: 'Alice', + email: 'alice@example.com', + authority: ['USER'], + }) + + // Simulate sign-out: clear token and reset session + const { setToken } = useToken() + setToken('') + useSessionUser.getState().setSessionSignedIn(false) + useSessionUser.getState().setUser({ + userId: undefined, + userName: '', + email: '', + avatar: '', + authority: [], + }) + + const state = useSessionUser.getState() + expect(state.session.signedIn).toBe(false) + expect(state.user.email).toBe('') + expect(state.user.authority).toEqual([]) + expect(localStorage.getItem(TOKEN_NAME_IN_STORAGE)).toBeNull() + }) +}) diff --git a/src/test/unit/cookiesStorage.test.ts b/src/test/unit/cookiesStorage.test.ts new file mode 100644 index 0000000..4c92cba --- /dev/null +++ b/src/test/unit/cookiesStorage.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +// Mock js-cookie before importing cookiesStorage +vi.mock('js-cookie', () => ({ + default: { + get: vi.fn(), + set: vi.fn(), + remove: vi.fn(), + }, +})) + +import cookiesStorage from '@/utils/cookiesStorage' +import Cookies from 'js-cookie' + +const mockCookies = Cookies as unknown as { + get: ReturnType + set: ReturnType + remove: ReturnType +} + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('cookiesStorage.getItem', () => { + it('returns the cookie value when it exists', () => { + mockCookies.get.mockReturnValue('my-token') + const result = cookiesStorage.getItem('token') + expect(result).toBe('my-token') + expect(mockCookies.get).toHaveBeenCalledWith('token') + }) + + it('returns null when the cookie does not exist', () => { + mockCookies.get.mockReturnValue(undefined) + const result = cookiesStorage.getItem('missing-key') + expect(result).toBeNull() + }) + + it('calls Cookies.get with the correct name', () => { + mockCookies.get.mockReturnValue('value') + cookiesStorage.getItem('auth-session') + expect(mockCookies.get).toHaveBeenCalledWith('auth-session') + }) +}) + +describe('cookiesStorage.setItem', () => { + it('calls Cookies.set with the name and value', () => { + cookiesStorage.setItem('token', 'abc123') + expect(mockCookies.set).toHaveBeenCalledWith('token', 'abc123', { expires: 1, path: '/' }) + }) + + it('uses the provided expires value', () => { + cookiesStorage.setItem('token', 'xyz', 7) + expect(mockCookies.set).toHaveBeenCalledWith('token', 'xyz', { expires: 7, path: '/' }) + }) + + it('accepts a Date object as expires', () => { + const expiryDate = new Date('2099-01-01') + cookiesStorage.setItem('session', 'data', expiryDate) + expect(mockCookies.set).toHaveBeenCalledWith('session', 'data', { expires: expiryDate, path: '/' }) + }) + + it('defaults to expires=1 when no expiry is provided', () => { + cookiesStorage.setItem('key', 'value') + expect(mockCookies.set).toHaveBeenCalledWith('key', 'value', { expires: 1, path: '/' }) + }) + + it('sets the path to /', () => { + cookiesStorage.setItem('key', 'value') + const callArgs = mockCookies.set.mock.calls[0] + expect(callArgs[2]).toMatchObject({ path: '/' }) + }) +}) + +describe('cookiesStorage.removeItem', () => { + it('calls Cookies.remove with the correct name', () => { + cookiesStorage.removeItem('token') + expect(mockCookies.remove).toHaveBeenCalledWith('token') + }) + + it('removes the specified key', () => { + cookiesStorage.removeItem('auth-session') + expect(mockCookies.remove).toHaveBeenCalledWith('auth-session') + expect(mockCookies.remove).not.toHaveBeenCalledWith('other-key') + }) +}) diff --git a/src/test/unit/qualitySettings.test.ts b/src/test/unit/qualitySettings.test.ts new file mode 100644 index 0000000..3e7828f --- /dev/null +++ b/src/test/unit/qualitySettings.test.ts @@ -0,0 +1,155 @@ +import { describe, it, expect, vi, beforeAll } from 'vitest' + +// Mock THREE before importing qualitySettings since it uses window.devicePixelRatio +// and THREE constants at module evaluation time +vi.mock('three', () => ({ + BasicShadowMap: 0, + PCFShadowMap: 1, + PCFSoftShadowMap: 2, + VSMShadowMap: 3, +})) + +// jsdom doesn't set devicePixelRatio by default, so set it before import +beforeAll(() => { + Object.defineProperty(window, 'devicePixelRatio', { + value: 2, + writable: true, + }) +}) + +import { + QUALITY_PRESETS, + getQualityPreset, + getShadowMapType, + type QualityLevel, + type QualityPreset, +} from '@/views/users/LabUtils/qualitySettings' + +describe('QUALITY_PRESETS', () => { + it('defines all 4 quality levels', () => { + const levels: QualityLevel[] = ['low', 'medium', 'high', 'ultra'] + for (const level of levels) { + expect(QUALITY_PRESETS).toHaveProperty(level) + } + }) + + it('each preset has all required rendering fields', () => { + const requiredFields: (keyof QualityPreset)[] = [ + 'shadowMapSize', + 'shadowType', + 'shadowBias', + 'shadowNormalBias', + 'antialias', + 'pixelRatio', + 'toneMapping', + 'toneMappingExposure', + 'postProcessing', + 'bloom', + 'ssao', + 'dof', + 'maxLights', + 'envMapIntensity', + 'textureAnisotropy', + 'lodDistances', + ] + for (const level of Object.keys(QUALITY_PRESETS) as QualityLevel[]) { + const preset = QUALITY_PRESETS[level] + for (const field of requiredFields) { + expect(preset, `${level} preset missing ${field}`).toHaveProperty(field) + } + } + }) + + it('shadowMapSize increases with quality level', () => { + expect(QUALITY_PRESETS.low.shadowMapSize).toBeLessThan(QUALITY_PRESETS.medium.shadowMapSize) + expect(QUALITY_PRESETS.medium.shadowMapSize).toBeLessThan(QUALITY_PRESETS.high.shadowMapSize) + expect(QUALITY_PRESETS.high.shadowMapSize).toBeLessThan(QUALITY_PRESETS.ultra.shadowMapSize) + }) + + it('maxLights increases with quality level', () => { + expect(QUALITY_PRESETS.low.maxLights).toBeLessThan(QUALITY_PRESETS.medium.maxLights) + expect(QUALITY_PRESETS.medium.maxLights).toBeLessThan(QUALITY_PRESETS.high.maxLights) + expect(QUALITY_PRESETS.high.maxLights).toBeLessThan(QUALITY_PRESETS.ultra.maxLights) + }) + + it('textureAnisotropy increases with quality level', () => { + expect(QUALITY_PRESETS.low.textureAnisotropy).toBeLessThan(QUALITY_PRESETS.medium.textureAnisotropy) + expect(QUALITY_PRESETS.medium.textureAnisotropy).toBeLessThan(QUALITY_PRESETS.high.textureAnisotropy) + expect(QUALITY_PRESETS.high.textureAnisotropy).toBeLessThan(QUALITY_PRESETS.ultra.textureAnisotropy) + }) + + it('low preset has antialias disabled', () => { + expect(QUALITY_PRESETS.low.antialias).toBe(false) + }) + + it('medium/high/ultra presets have antialias enabled', () => { + expect(QUALITY_PRESETS.medium.antialias).toBe(true) + expect(QUALITY_PRESETS.high.antialias).toBe(true) + expect(QUALITY_PRESETS.ultra.antialias).toBe(true) + }) + + it('low preset has no post-processing features', () => { + expect(QUALITY_PRESETS.low.postProcessing).toBe(false) + expect(QUALITY_PRESETS.low.bloom).toBe(false) + expect(QUALITY_PRESETS.low.ssao).toBe(false) + expect(QUALITY_PRESETS.low.dof).toBe(false) + }) + + it('ultra preset enables all post-processing features', () => { + expect(QUALITY_PRESETS.ultra.postProcessing).toBe(true) + expect(QUALITY_PRESETS.ultra.bloom).toBe(true) + expect(QUALITY_PRESETS.ultra.ssao).toBe(true) + expect(QUALITY_PRESETS.ultra.dof).toBe(true) + }) + + it('lodDistances is a tuple of 3 numbers for each preset', () => { + for (const level of Object.keys(QUALITY_PRESETS) as QualityLevel[]) { + const { lodDistances } = QUALITY_PRESETS[level] + expect(Array.isArray(lodDistances)).toBe(true) + expect(lodDistances).toHaveLength(3) + expect(typeof lodDistances[0]).toBe('number') + expect(typeof lodDistances[1]).toBe('number') + expect(typeof lodDistances[2]).toBe('number') + } + }) + + it('lodDistances range increases with quality', () => { + const maxDist = (preset: QualityPreset) => preset.lodDistances[2] + expect(maxDist(QUALITY_PRESETS.low)).toBeLessThan(maxDist(QUALITY_PRESETS.medium)) + expect(maxDist(QUALITY_PRESETS.medium)).toBeLessThan(maxDist(QUALITY_PRESETS.high)) + expect(maxDist(QUALITY_PRESETS.high)).toBeLessThan(maxDist(QUALITY_PRESETS.ultra)) + }) +}) + +describe('getQualityPreset', () => { + it('returns the correct preset for each quality level', () => { + const levels: QualityLevel[] = ['low', 'medium', 'high', 'ultra'] + for (const level of levels) { + expect(getQualityPreset(level)).toBe(QUALITY_PRESETS[level]) + } + }) +}) + +describe('getShadowMapType', () => { + it('returns BasicShadowMap for basic', () => { + // THREE is mocked; value is 0 + expect(getShadowMapType('basic')).toBe(0) + }) + + it('returns PCFShadowMap for pcf', () => { + expect(getShadowMapType('pcf')).toBe(1) + }) + + it('returns PCFSoftShadowMap for pcfSoft', () => { + expect(getShadowMapType('pcfSoft')).toBe(2) + }) + + it('returns VSMShadowMap for vsm', () => { + expect(getShadowMapType('vsm')).toBe(3) + }) + + it('returns PCFShadowMap as fallback for unknown type', () => { + // Cast to bypass TS for testing the default branch + expect(getShadowMapType('unknown' as any)).toBe(1) + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..a74e9b0 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: ['node_modules/', 'src/test/', '**/*.d.ts', 'src/mock/'], + }, + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}) From 58fe47153d344ae42dcad818104920e0b740658c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 16:35:07 +0000 Subject: [PATCH 06/12] feat: complete backend API integration with service layer and live data (Goal 1) - Extend endpoint.config.ts with feed, renders, poses, profile, stories, challenges, reports endpoints - Add src/@types/feed.ts and src/@types/creation.ts type definitions - Create FeedService.ts and CreationService.ts using ApiService.fetchDataWithAxios - Update Home feed to use SWR + FeedService with loading skeleton and FALLBACK_POSTS on error - Update MyCreations to use SWR + CreationService with loading skeleton and FALLBACK_CREATIONS on error - Add delete button (Trash2, hover-only) to CreationCard with confirmation and SWR mutate revalidation Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_016GhhvSFLMp3M5ubNM4mvTY --- src/@types/creation.ts | 25 +++ src/@types/feed.ts | 39 ++++ src/configs/endpoint.config.ts | 25 ++- src/services/CreationService.ts | 30 +++ src/services/FeedService.ts | 43 ++++ src/views/Home/index.tsx | 210 ++++++++++++------ .../myCreations/components/CreationCard.tsx | 65 +++++- src/views/users/myCreations/index.tsx | 125 +++++++---- 8 files changed, 445 insertions(+), 117 deletions(-) create mode 100644 src/@types/creation.ts create mode 100644 src/@types/feed.ts create mode 100644 src/services/CreationService.ts create mode 100644 src/services/FeedService.ts diff --git a/src/@types/creation.ts b/src/@types/creation.ts new file mode 100644 index 0000000..10ff949 --- /dev/null +++ b/src/@types/creation.ts @@ -0,0 +1,25 @@ +export interface Creation { + id: string + title: string + thumbnail: string + type: 'pose' | 'render' | 'animation' + views: number + likes: number + createdAt: string + tags: string[] + userId?: string +} + +export interface CreationsResponse { + creations: Creation[] + total: number + nextPage: number | null +} + +export interface RenderPayload { + imageDataUrl: string + poseSnapshot: Record + skinUrl?: string + renderSettings: Record + title?: string +} diff --git a/src/@types/feed.ts b/src/@types/feed.ts new file mode 100644 index 0000000..67208af --- /dev/null +++ b/src/@types/feed.ts @@ -0,0 +1,39 @@ +export interface FeedUser { + id: string + name: string + avatar: string +} + +export interface FeedPost { + id: string + user: FeedUser + type: 'image' | 'video' | 'promo' | 'recommendation' + content: string + likes: number + comments: number + caption: string + tags: string[] + isLiked?: boolean + isBookmarked?: boolean + createdAt: string +} + +export interface FeedResponse { + posts: FeedPost[] + nextPage: number | null + total: number +} + +export interface Story { + id: string + user: FeedUser + content: string + createdAt: string +} + +export interface Challenge { + id: string + name: string + entries: string + previewUrl?: string +} diff --git a/src/configs/endpoint.config.ts b/src/configs/endpoint.config.ts index 155fef4..559bee8 100644 --- a/src/configs/endpoint.config.ts +++ b/src/configs/endpoint.config.ts @@ -1,13 +1,34 @@ export const apiPrefix = '/api' const endpointConfig = { + // Auth (existing) signIn: '/auth', signOut: '/sign-out', signUp: '/signup', forgotPassword: '/forgot-password', resetPassword: '/reset-password', - googleSignIn: '/auth', // Add this for Google OAuth - githubSignIn: '/auth', // Add this for GitHub OAuth + googleSignIn: '/auth', + githubSignIn: '/auth', + // Feed + feed: '/feed', + feedPost: '/feed', + feedLike: '/feed/:postId/like', + feedBookmark: '/feed/:postId/bookmark', + // Renders + renders: '/renders', + renderById: '/renders/:renderId', + // Poses + poses: '/poses', + poseById: '/poses/:poseId', + // Profile + profileById: '/profile/:userId', + profileUpdate: '/profile', + // Stories + stories: '/stories', + // Challenges + challenges: '/challenges', + // Reports + reports: '/reports', } export default endpointConfig diff --git a/src/services/CreationService.ts b/src/services/CreationService.ts new file mode 100644 index 0000000..2cd5a69 --- /dev/null +++ b/src/services/CreationService.ts @@ -0,0 +1,30 @@ +import ApiService from './ApiService' +import endpointConfig from '@/configs/endpoint.config' +import type { CreationsResponse, Creation, RenderPayload } from '@/@types/creation' + +const CreationService = { + async getMyCreations(userId: string, page = 1, limit = 20): Promise { + return ApiService.fetchDataWithAxios({ + url: endpointConfig.renders, + method: 'GET', + params: { userId, page, limit }, + }) + }, + + async saveRender(payload: RenderPayload): Promise { + return ApiService.fetchDataWithAxios({ + url: endpointConfig.renders, + method: 'POST', + data: payload, + }) + }, + + async deleteCreation(renderId: string): Promise { + return ApiService.fetchDataWithAxios({ + url: endpointConfig.renderById.replace(':renderId', renderId), + method: 'DELETE', + }) + }, +} + +export default CreationService diff --git a/src/services/FeedService.ts b/src/services/FeedService.ts new file mode 100644 index 0000000..f86a763 --- /dev/null +++ b/src/services/FeedService.ts @@ -0,0 +1,43 @@ +import ApiService from './ApiService' +import endpointConfig from '@/configs/endpoint.config' +import type { FeedResponse, Story, Challenge } from '@/@types/feed' + +const FeedService = { + async getFeed(page = 1, limit = 10): Promise { + return ApiService.fetchDataWithAxios({ + url: endpointConfig.feed, + method: 'GET', + params: { page, limit }, + }) + }, + + async likePost(postId: string): Promise { + return ApiService.fetchDataWithAxios({ + url: endpointConfig.feedLike.replace(':postId', postId), + method: 'POST', + }) + }, + + async bookmarkPost(postId: string): Promise { + return ApiService.fetchDataWithAxios({ + url: endpointConfig.feedBookmark.replace(':postId', postId), + method: 'POST', + }) + }, + + async getStories(): Promise { + return ApiService.fetchDataWithAxios({ + url: endpointConfig.stories, + method: 'GET', + }) + }, + + async getChallenges(): Promise { + return ApiService.fetchDataWithAxios({ + url: endpointConfig.challenges, + method: 'GET', + }) + }, +} + +export default FeedService diff --git a/src/views/Home/index.tsx b/src/views/Home/index.tsx index 0aeb085..de1a817 100644 --- a/src/views/Home/index.tsx +++ b/src/views/Home/index.tsx @@ -1,93 +1,172 @@ import { useState } from 'react' import { motion } from 'framer-motion' +import useSWR from 'swr' import SearchBar from './components/SearchBar' import Stories from './components/Stories' import PostCard from './components/PostCard' import PromoCard from './components/PromoCard' import Sidebar from './components/Sidebar' +import FeedService from '@/services/FeedService' -// --- Mock Data --- -const COMMUNITY_POSTS = [ +// --- Fallback Mock Data (used when backend is unavailable) --- +const FALLBACK_POSTS = [ { - id: 1, - user: { name: 'AlexCraft', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alex' }, - type: 'image', + id: '1', + user: { id: 'u1', name: 'AlexCraft', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alex' }, + type: 'image' as const, content: '/poseLabsPose/standing.png', likes: 1240, comments: 45, caption: 'Just finished the cinematic standing pose! The lighting in PoseLab is insane! 🔥 #Minecraft #Render', - tags: ['#3DRendering', '#MinecraftArt'] + tags: ['#3DRendering', '#MinecraftArt'], + createdAt: '2024-01-20', }, { - id: 2, - user: { name: 'SteveTheGod', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Steve' }, - type: 'video', + id: '2', + user: { id: 'u2', name: 'SteveTheGod', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Steve' }, + type: 'video' as const, content: '/poseLabsPose/walking.png', likes: 890, comments: 12, caption: 'Experimenting with the bendable model. The joints feel so smooth now. 🧊', - tags: ['#BendableModel', '#Animation'] + tags: ['#BendableModel', '#Animation'], + createdAt: '2024-01-19', }, { id: 'promo-1', - type: 'promo', - title: 'New Bendable Rig v2.0', - subtitle: 'Experience ultimate flexibility in your renders with our upgraded vertex-weight systems.', - cta: 'Upgrade Rig', - path: '/3d-viewer', - image: 'https://images.unsplash.com/photo-1614850523296-d8c1af93d400?q=80&w=2070&auto=format&fit=crop' + user: { id: 'system', name: 'PoseLab', avatar: '' }, + type: 'promo' as const, + content: 'https://images.unsplash.com/photo-1614850523296-d8c1af93d400?q=80&w=2070&auto=format&fit=crop', + likes: 0, + comments: 0, + caption: '', + tags: [], + createdAt: '', }, { - id: 3, - user: { name: 'CreativeCat', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Cat' }, - type: 'image', + id: '3', + user: { id: 'u3', name: 'CreativeCat', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Cat' }, + type: 'image' as const, content: '/poseLabsPose/combat.png', likes: 2100, comments: 156, caption: 'Action sequence render! Ready for the battle. ⚔️', - tags: ['#CombatPose', '#Epic'] + tags: ['#CombatPose', '#Epic'], + createdAt: '2024-01-18', }, { id: 'pose-rec-1', - type: 'recommendation', - title: 'Try this: "The Hero Entry"', - poseId: 'standing', - preview: '/poseLabsPose/standing.png' + user: { id: 'system', name: 'PoseLab', avatar: '' }, + type: 'recommendation' as const, + content: '/poseLabsPose/standing.png', + likes: 0, + comments: 0, + caption: '', + tags: [], + createdAt: '', }, { - id: 4, - user: { name: 'PixelMaster', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Pixel' }, - type: 'image', + id: '4', + user: { id: 'u4', name: 'PixelMaster', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Pixel' }, + type: 'image' as const, content: '/poseLabsPose/walking.png', likes: 560, comments: 8, caption: 'Starting small with basic walks. Practice makes perfect! 🚶‍♂️', - tags: ['#Beginner', '#PosePractice'] - } + tags: ['#Beginner', '#PosePractice'], + createdAt: '2024-01-17', + }, ] const TRENDING_CHALLENGES = [ - { id: 1, name: 'SunlightShadows', entries: '1.2k' }, - { id: 2, name: 'RigidDynamics', entries: '850' }, - { id: 3, name: 'EpicCombat', entries: '2.4k' }, + { id: '1', name: 'SunlightShadows', entries: '1.2k' }, + { id: '2', name: 'RigidDynamics', entries: '850' }, + { id: '3', name: 'EpicCombat', entries: '2.4k' }, +] + +// Map legacy mock post shapes (promo/recommendation) to the legacy components +// The API posts follow FeedPost shape; promos and recs are interleaved in the mock. +const LEGACY_COMMUNITY_POSTS = [ + ...FALLBACK_POSTS.filter(p => p.type !== 'promo' && p.type !== 'recommendation'), + { + id: 'promo-1', + type: 'promo', + title: 'New Bendable Rig v2.0', + subtitle: 'Experience ultimate flexibility in your renders with our upgraded vertex-weight systems.', + cta: 'Upgrade Rig', + path: '/3d-viewer', + image: 'https://images.unsplash.com/photo-1614850523296-d8c1af93d400?q=80&w=2070&auto=format&fit=crop', + }, + { + id: 'pose-rec-1', + type: 'recommendation', + title: 'Try this: "The Hero Entry"', + poseId: 'standing', + preview: '/poseLabsPose/standing.png', + }, ] +const FeedSkeleton = () => ( +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+) + const Home = () => { - const [likedPosts, setLikedPosts] = useState>(new Set()) - const [bookmarkedPosts, setBookmarkedPosts] = useState>(new Set()) + const [likedPosts, setLikedPosts] = useState>(new Set()) + const [bookmarkedPosts, setBookmarkedPosts] = useState>(new Set()) const [followedUsers, setFollowedUsers] = useState>(new Set()) - const toggleLike = (id: number | string) => { + const { data: feedData, isLoading: feedLoading } = useSWR( + 'feed-page-1', + () => FeedService.getFeed(1, 10), + { shouldRetryOnError: false }, + ) + + // Use API data if available, fall back to FALLBACK_POSTS on error or before load + const apiPosts = feedData?.posts ?? null + + // Build the display list: API posts when available, else legacy mock with promos/recs interleaved + const displayPosts = apiPosts + ? apiPosts + : LEGACY_COMMUNITY_POSTS + + const toggleLike = (id: string) => { const newLiked = new Set(likedPosts) - if (newLiked.has(id)) newLiked.delete(id) - else newLiked.add(id) + if (newLiked.has(id)) { + newLiked.delete(id) + } else { + newLiked.add(id) + // Fire-and-forget API call + FeedService.likePost(id).catch(() => undefined) + } setLikedPosts(newLiked) } - const toggleBookmark = (id: number | string) => { + const toggleBookmark = (id: string) => { const newBookmarks = new Set(bookmarkedPosts) - if (newBookmarks.has(id)) newBookmarks.delete(id) - else newBookmarks.add(id) + if (newBookmarks.has(id)) { + newBookmarks.delete(id) + } else { + newBookmarks.add(id) + // Fire-and-forget API call + FeedService.bookmarkPost(id).catch(() => undefined) + } setBookmarkedPosts(newBookmarks) } @@ -108,7 +187,6 @@ const Home = () => {
-
@@ -118,31 +196,35 @@ const Home = () => {

Discovery Hub

- +
-
- {COMMUNITY_POSTS.map((post) => { - if (post.type === 'recommendation') return null - - if (post.type === 'promo') { - return - } - - return ( - - ) - })} -
+ {feedLoading ? ( + + ) : ( +
+ {displayPosts.map((post: any) => { + if (post.type === 'recommendation') return null + + if (post.type === 'promo') { + return + } + + return ( + toggleLike(String(id))} + onToggleBookmark={(id) => toggleBookmark(String(id))} + onToggleFollow={toggleFollow} + /> + ) + })} +
+ )} {/* Infinite Scroll/Loading Indicator */}
diff --git a/src/views/users/myCreations/components/CreationCard.tsx b/src/views/users/myCreations/components/CreationCard.tsx index 725022b..0a083a9 100644 --- a/src/views/users/myCreations/components/CreationCard.tsx +++ b/src/views/users/myCreations/components/CreationCard.tsx @@ -1,7 +1,8 @@ -import React, { cloneElement } from 'react' +import React, { cloneElement, useState } from 'react' import { motion } from 'framer-motion' import { Eye, Heart, Download, Share2, MoreVertical, Trash2, Edit3, PlayCircle, Image as ImageIcon } from 'lucide-react' import classNames from '@/utils/classNames' +import CreationService from '@/services/CreationService' export interface Creation { id: string @@ -17,10 +18,12 @@ export interface Creation { interface CreationCardProps { creation: Creation viewMode: 'grid' | 'list' + mutate?: () => void } -const CreationCard = ({ creation, viewMode }: CreationCardProps) => { +const CreationCard = ({ creation, viewMode, mutate }: CreationCardProps) => { const isGrid = viewMode === 'grid' + const [isDeleting, setIsDeleting] = useState(false) const typeIcons = { pose: , @@ -28,6 +31,21 @@ const CreationCard = ({ creation, viewMode }: CreationCardProps) => { animation: , } + const handleDelete = async () => { + const confirmed = window.confirm(`Delete "${creation.title}"? This action cannot be undone.`) + if (!confirmed) return + + setIsDeleting(true) + try { + await CreationService.deleteCreation(creation.id) + mutate?.() + } catch { + alert('Failed to delete creation. Please try again.') + } finally { + setIsDeleting(false) + } + } + return ( { {creation.type}
+ {/* Delete button — grid only, visible on hover */} + {isGrid && ( + + )} + {/* Quick Action Flyout (Grid Only) */} {isGrid && (
@@ -105,8 +135,13 @@ const CreationCard = ({ creation, viewMode }: CreationCardProps) => { {/* List Action Buttons */} {!isGrid && (
- } color="hover:text-blue-400" /> - } color="hover:text-red-400" /> + } color="hover:text-blue-400" onClick={() => undefined} /> + } + color="hover:text-red-400" + onClick={handleDelete} + disabled={isDeleting} + />
)} @@ -115,13 +150,27 @@ const CreationCard = ({ creation, viewMode }: CreationCardProps) => { const ActionButton = ({ icon }: { icon: React.ReactNode }) => ( ) -const ListActionBtn = ({ icon, color }: { icon: React.ReactNode, color: string }) => ( - ) diff --git a/src/views/users/myCreations/index.tsx b/src/views/users/myCreations/index.tsx index 0d61c5b..8c5a270 100644 --- a/src/views/users/myCreations/index.tsx +++ b/src/views/users/myCreations/index.tsx @@ -1,12 +1,15 @@ import { useState } from 'react' +import useSWR from 'swr' import Header from './components/Header' import Filters from './components/Filters' import CreationCard, { type Creation } from './components/CreationCard' import { Search } from 'lucide-react' import { motion, AnimatePresence } from 'framer-motion' +import { useSessionUser } from '@/store/authStore' +import CreationService from '@/services/CreationService' -// Sample data (In a real app, this would come from a hook/API) -const sampleCreations: Creation[] = [ +// Fallback mock data (used when backend is unavailable) +const FALLBACK_CREATIONS: Creation[] = [ { id: '1', title: 'Nether Warlord', @@ -49,12 +52,43 @@ const sampleCreations: Creation[] = [ } ] +const CreationsSkeleton = () => ( +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( +
+
+
+
+
+
+
+
+
+
+
+ ))} +
+) + const MyCreations = () => { const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') const [search, setSearch] = useState('') const [category, setCategory] = useState<'all' | 'pose' | 'render' | 'animation'>('all') - const filteredCreations = sampleCreations.filter(c => { + const userId = useSessionUser((state) => state.user?.email ?? '') + + const swrKey = userId ? `creations-${userId}` : null + + const { data: creationsData, isLoading, mutate } = useSWR( + swrKey, + () => CreationService.getMyCreations(userId), + { shouldRetryOnError: false }, + ) + + // Use API data if available, fall back to FALLBACK_CREATIONS + const allCreations: Creation[] = creationsData?.creations ?? FALLBACK_CREATIONS + + const filteredCreations = allCreations.filter(c => { const matchesSearch = c.title.toLowerCase().includes(search.toLowerCase()) || c.tags.some(t => t.toLowerCase().includes(search.toLowerCase())) const matchesCategory = category === 'all' || c.type === category @@ -74,47 +108,52 @@ const MyCreations = () => { setCategory={setCategory} /> - - {filteredCreations.length > 0 ? ( - - {filteredCreations.map((creation) => ( - - ))} - - ) : ( - -
- -
-

Zero Results Detected

-

Adjust your filters to see more creations.

- -
- )} -
+
+ +
+

Zero Results Detected

+

Adjust your filters to see more creations.

+ + + )} + + )}
) } From dc7430ae0da909797d0c4dd3247cc14dbfbbe9ac Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 16:35:32 +0000 Subject: [PATCH 07/12] feat: consolidate to rigid-system rig, add unified PoseControls and enriched presets (Goal 3) - CharacterViewer now exclusively uses NewMinecraftCharacter (rigid-system) - Removed imports for standard/MinecraftCharacter, bendable/BendableMinecraftCharacter, standard/PoseControls, bendable/PoseControls, and their posePresets - Simplified characterModel state from 4-way to 'rigid' | 'bendable' - Bottom bar now shows RIGID and BENDABLE buttons (both visible, no commented-out buttons) - handlePoseChange updated to work with RigState (Record) - Created rigid-system/PoseControls.tsx: unified slider-based controls that read Euler radians and display/accept degrees, grouped by Head/Body/Arms/Legs - Enriched RIGID_POSES with running, combat, sitting, waving - Enriched BENDABLE_RIGID_POSES with combat and sitting - Marked standard/MinecraftCharacter.tsx and bendable/BendableMinecraftCharacter.tsx as @deprecated (files kept for reference) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_016GhhvSFLMp3M5ubNM4mvTY --- src/views/users/LabUtils/CharacterViewer.tsx | 156 +++++------------- .../bendable/BendableMinecraftCharacter.tsx | 1 + .../LabUtils/rigid-system/PoseControls.tsx | 149 +++++++++++++++++ .../LabUtils/rigid-system/posePresets.ts | 90 +++++++++- .../LabUtils/standard/MinecraftCharacter.tsx | 1 + 5 files changed, 283 insertions(+), 114 deletions(-) create mode 100644 src/views/users/LabUtils/rigid-system/PoseControls.tsx diff --git a/src/views/users/LabUtils/CharacterViewer.tsx b/src/views/users/LabUtils/CharacterViewer.tsx index 371c3f5..b78a120 100644 --- a/src/views/users/LabUtils/CharacterViewer.tsx +++ b/src/views/users/LabUtils/CharacterViewer.tsx @@ -1,24 +1,18 @@ import { useState, useRef, useEffect, Suspense } from "react"; import { Canvas } from "@react-three/fiber"; import { OrbitControls, Environment, GizmoHelper, GizmoViewport } from "@react-three/drei"; +import * as THREE from "three"; -import { MinecraftCharacter } from "./standard/MinecraftCharacter"; -import { BendableMinecraftCharacter } from "./bendable/BendableMinecraftCharacter"; -import { PoseControls as StandardPoseControls } from "./standard/PoseControls"; -import { PoseControls as BendablePoseControls } from "./bendable/PoseControls"; - -import STANDARD_POSES from "./standard/posePresets"; -import BENDABLE_POSES from "./bendable/posePresets"; import { NewMinecraftCharacter } from "./rigid-system/NewMinecraftCharacter"; import { RIGID_POSES, BENDABLE_RIGID_POSES } from "./rigid-system/posePresets"; +import { PoseControls } from "./rigid-system/PoseControls"; import { LightingControls, Light } from "./LightingControls"; import { LightRenderer } from "./LightRenderer"; import { RenderDialog, RenderSettings } from "./RenderDialog"; import { createHighQualityRender } from "./renderUtils"; -import { Camera, Upload, Download, RotateCcw, Lightbulb, Gpu, Box, Accessibility, Smile, LogOut, RotateCw } from "lucide-react"; +import { Camera, Upload, Download, Lightbulb, Gpu, Accessibility, Smile, LogOut } from "lucide-react"; import { toast } from "sonner"; -import * as THREE from "three"; import { QualitySetting } from "./QualitySetting"; import { getQualityPreset } from "./qualitySettings"; import { useDeviceQuality } from "./hooks/useDeviceQuality"; @@ -32,13 +26,9 @@ export interface CharacterViewerProps { } export const CharacterViewer = ({ skinImage, onChangeSkinClick, pose }: CharacterViewerProps) => { - const [characterModel, setCharacterModel] = useState<'default' | 'bendable' | 'new_rigid' | 'new_bendable'>('default'); + const [characterModel, setCharacterModel] = useState<'rigid' | 'bendable'>('rigid'); - const activePresets = - characterModel === 'default' ? STANDARD_POSES : - characterModel === 'bendable' ? BENDABLE_POSES : - characterModel === 'new_rigid' ? RIGID_POSES : - BENDABLE_RIGID_POSES; + const activePresets = characterModel === 'rigid' ? RIGID_POSES : BENDABLE_RIGID_POSES; const defaultPose = activePresets.standing || Object.values(activePresets)[0]; const [currentPose, setCurrentPose] = useState(pose || defaultPose); @@ -61,19 +51,12 @@ export const CharacterViewer = ({ skinImage, onChangeSkinClick, pose }: Characte if (sceneRef.current) enhanceSceneMaterials(sceneRef.current, undefined, qualityPreset); }, [qualityPreset]); - const handleModelChange = (model: 'default' | 'bendable' | 'new_rigid' | 'new_bendable') => { + const handleModelChange = (model: 'rigid' | 'bendable') => { if (model === characterModel) return; - // Get the correct presets for the NEW model - const newPresets = - model === 'default' ? STANDARD_POSES : - model === 'bendable' ? BENDABLE_POSES : - model === 'new_rigid' ? RIGID_POSES : - BENDABLE_RIGID_POSES; + const newPresets = model === 'rigid' ? RIGID_POSES : BENDABLE_RIGID_POSES; const newDefault = newPresets.standing || Object.values(newPresets)[0]; - // Update everything in one batch to ensure they stay in sync - // This is critical to prevent passing a Bendable pose to the Standard renderer setCharacterModel(model); setCurrentPose(newDefault); setSelectedPreset("standing"); @@ -81,17 +64,23 @@ export const CharacterViewer = ({ skinImage, onChangeSkinClick, pose }: Characte toast(`Switched to ${newDefault.poseMeta?.name || "Default"} for ${model} model`); }; - const handlePoseChange = (bodyPart: string, axis: string, value: number) => { - setCurrentPose((prev: any) => ({ - ...prev, - poseConfig: { - ...prev.poseConfig, - [bodyPart]: { - ...prev.poseConfig[bodyPart], - [axis]: value, + const handlePoseChange = (partName: string, axis: 'x' | 'y' | 'z', valueDeg: number) => { + setCurrentPose((prev: any) => { + const prevEuler: THREE.Euler = prev.poseConfig.rotations[partName] || new THREE.Euler(0, 0, 0); + const newEuler = new THREE.Euler(prevEuler.x, prevEuler.y, prevEuler.z, prevEuler.order); + newEuler[axis] = (valueDeg * Math.PI) / 180; + + return { + ...prev, + poseConfig: { + ...prev.poseConfig, + rotations: { + ...prev.poseConfig.rotations, + [partName]: newEuler, + }, }, - }, - })); + }; + }); setSelectedPreset("custom"); }; @@ -215,13 +204,6 @@ export const CharacterViewer = ({ skinImage, onChangeSkinClick, pose }: Characte
- {/* */} - {/* Canvas Area */}
@@ -234,7 +216,7 @@ export const CharacterViewer = ({ skinImage, onChangeSkinClick, pose }: Characte ref={canvasRef} camera={{ position: [0, 2, 12], fov: 45 }} gl={{ preserveDrawingBuffer: true, antialias: true, alpha: true }} - className="z-10 relative" // Added relative to ensure z-index works on the R3F wrapper + className="z-10 relative" onCreated={({ scene, camera }) => { sceneRef.current = scene; cameraRef.current = camera; @@ -246,24 +228,11 @@ export const CharacterViewer = ({ skinImage, onChangeSkinClick, pose }: Characte - {characterModel === 'default' ? ( - - ) : characterModel === 'bendable' ? ( - - ) : ( - - )} + @@ -274,64 +243,29 @@ export const CharacterViewer = ({ skinImage, onChangeSkinClick, pose }: Characte
+ - {/* */} - {/* - - - */} - - {/* - - - - - - - - */} - setRenderDialogOpen(true)} /> @@ -394,9 +328,7 @@ export const CharacterViewer = ({ skinImage, onChangeSkinClick, pose }: Characte
)} {openPanel === "poseControls" && ( - characterModel === 'default' ? - : - + )}
diff --git a/src/views/users/LabUtils/bendable/BendableMinecraftCharacter.tsx b/src/views/users/LabUtils/bendable/BendableMinecraftCharacter.tsx index 17fe250..6a4abd4 100644 --- a/src/views/users/LabUtils/bendable/BendableMinecraftCharacter.tsx +++ b/src/views/users/LabUtils/bendable/BendableMinecraftCharacter.tsx @@ -1,3 +1,4 @@ +// @deprecated — Use rigid-system/NewMinecraftCharacter instead. Kept for reference only. import { useMemo, useRef, useEffect, useCallback } from "react"; import * as THREE from "three"; import { createPortal } from "@react-three/fiber"; diff --git a/src/views/users/LabUtils/rigid-system/PoseControls.tsx b/src/views/users/LabUtils/rigid-system/PoseControls.tsx new file mode 100644 index 0000000..e610e05 --- /dev/null +++ b/src/views/users/LabUtils/rigid-system/PoseControls.tsx @@ -0,0 +1,149 @@ +import * as THREE from 'three'; +import { Label } from "@/components/ui/label"; +import { Slider } from "@/components/ui/slider"; +import { RigState } from './types'; + +interface PoseControlsProps { + pose: { poseConfig: RigState }; + onPoseChange: (partName: string, axis: 'x' | 'y' | 'z', valueDeg: number) => void; +} + +// Groups define render order and visual color coding +const PART_GROUPS: { label: string; color: string; keys: string[] }[] = [ + { + label: "Head", + color: "text-yellow-400", + keys: ["head"], + }, + { + label: "Body", + color: "text-orange-400", + keys: ["body"], + }, + { + label: "Arms", + color: "text-blue-400", + keys: ["leftArm", "rightArm", "leftUpperArm", "leftLowerArm", "rightUpperArm", "rightLowerArm"], + }, + { + label: "Legs", + color: "text-green-400", + keys: ["leftLeg", "rightLeg", "leftUpperLeg", "leftLowerLeg", "rightUpperLeg", "rightLowerLeg"], + }, +]; + +const PART_LABELS: Record = { + head: "Head", + body: "Body", + leftArm: "Left Arm", + rightArm: "Right Arm", + leftUpperArm: "Left Upper Arm", + leftLowerArm: "Left Forearm", + rightUpperArm: "Right Upper Arm", + rightLowerArm: "Right Forearm", + leftLeg: "Left Leg", + rightLeg: "Right Leg", + leftUpperLeg: "Left Thigh", + leftLowerLeg: "Left Shin", + rightUpperLeg: "Right Thigh", + rightLowerLeg: "Right Shin", +}; + +const PART_COLORS: Record = { + head: "text-yellow-400", + body: "text-orange-400", + leftArm: "text-blue-400", + rightArm: "text-purple-400", + leftUpperArm: "text-blue-400", + leftLowerArm: "text-cyan-400", + rightUpperArm: "text-purple-400", + rightLowerArm: "text-pink-400", + leftLeg: "text-green-400", + rightLeg: "text-indigo-400", + leftUpperLeg: "text-green-400", + leftLowerLeg: "text-lime-400", + rightUpperLeg: "text-indigo-400", + rightLowerLeg: "text-violet-400", +}; + +const axes: { key: 'x' | 'y' | 'z'; label: string; color: string }[] = [ + { key: 'x', label: 'X (Pitch)', color: 'text-red-400' }, + { key: 'y', label: 'Y (Yaw)', color: 'text-green-400' }, + { key: 'z', label: 'Z (Roll)', color: 'text-blue-400' }, +]; + +const radToDeg = (rad: number) => Math.round((rad * 180) / Math.PI); + +export const PoseControls = ({ pose, onPoseChange }: PoseControlsProps) => { + const rotations = pose.poseConfig.rotations; + const availableKeys = Object.keys(rotations); + + return ( +
+

+ Manual Joint Manipulation +

+ + {PART_GROUPS.map((group) => { + // Only render parts that actually exist in current pose + const groupParts = group.keys.filter((k) => availableKeys.includes(k)); + if (groupParts.length === 0) return null; + + return ( +
+

+ {group.label} +

+ +
+ {groupParts.map((partName) => { + const euler = rotations[partName] as THREE.Euler; + const color = PART_COLORS[partName] || "text-gray-400"; + const label = PART_LABELS[partName] || partName; + + return ( +
+

+ {label} +

+ +
+ {axes.map((axis) => { + const deg = radToDeg(euler[axis.key]); + return ( +
+
+ + + {deg}° + +
+ + onPoseChange(partName, axis.key, value) + } + min={-180} + max={180} + step={5} + className="w-full" + /> +
+ ); + })} +
+
+ ); + })} +
+
+ ); + })} +
+ ); +}; diff --git a/src/views/users/LabUtils/rigid-system/posePresets.ts b/src/views/users/LabUtils/rigid-system/posePresets.ts index fbf379f..96a3535 100644 --- a/src/views/users/LabUtils/rigid-system/posePresets.ts +++ b/src/views/users/LabUtils/rigid-system/posePresets.ts @@ -42,7 +42,59 @@ export const RIGID_POSES: Record = { @@ -96,5 +148,39 @@ export const BENDABLE_RIGID_POSES: Record Date: Wed, 17 Jun 2026 16:37:54 +0000 Subject: [PATCH 08/12] feat: implement Creator Space with stats, pose management, and publish flow (Goal 5) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_016GhhvSFLMp3M5ubNM4mvTY --- src/views/users/creatorSpace/index.tsx | 283 ++++++++++++++++++++++++- 1 file changed, 276 insertions(+), 7 deletions(-) diff --git a/src/views/users/creatorSpace/index.tsx b/src/views/users/creatorSpace/index.tsx index 154f57c..8ee3ab8 100644 --- a/src/views/users/creatorSpace/index.tsx +++ b/src/views/users/creatorSpace/index.tsx @@ -1,13 +1,282 @@ -import ComingSoon from '@/components/shared/ComingSoon' -import { Telescope } from 'lucide-react' +import { useState } from 'react' +import { motion } from 'framer-motion' +import { toast } from 'sonner' +import { BarChart2, Eye, Heart, Users, Edit2, EyeOff, Plus, CheckCircle2, Star } from 'lucide-react' +import { Button } from '@/components/ui/Button' +import { Badge } from '@/components/ui/badge' +import { Input } from '@/components/ui/Input' +import { useSessionUser } from '@/store/authStore' + +// --- Hardcoded placeholder stats --- +const STATS = [ + { label: 'Published Poses', value: 12, trend: '+2', icon: Star }, + { label: 'Total Views', value: '8.4K', trend: '+1.2K', icon: Eye }, + { label: 'Total Likes', value: '1.2K', trend: '+184', icon: Heart }, + { label: 'Followers', value: 342, trend: '+28', icon: Users }, +] + +// --- Sample poses --- +const SAMPLE_POSES = [ + { id: '1', name: 'Hero Stance', views: 1240, likes: 89, preview: '/poseLabsPose/standing.png', tags: ['action', 'hero'] }, + { id: '2', name: 'Combat Ready', views: 856, likes: 62, preview: '/poseLabsPose/standing.png', tags: ['combat', 'pvp'] }, + { id: '3', name: 'Chill Walk', views: 2145, likes: 178, preview: '/poseLabsPose/standing.png', tags: ['casual'] }, +] + +const cardVariants = { + hidden: { opacity: 0, y: 20 }, + visible: (i: number) => ({ + opacity: 1, + y: 0, + transition: { delay: i * 0.08, duration: 0.35, ease: 'easeOut' }, + }), +} const CreatorSpace = () => { + const { user } = useSessionUser() + const isVerifiedCreator = user?.authority?.includes('creator') + + // Publish form state + const [poseName, setPoseName] = useState('') + const [description, setDescription] = useState('') + const [tags, setTags] = useState('') + + // Creator application state + const [applied, setApplied] = useState(false) + + const handleUnpublish = () => { + toast.info('Coming soon') + } + + const handlePublish = (e: React.FormEvent) => { + e.preventDefault() + toast.info('Pose publishing coming soon — connect to the PoseLab editor first!') + } + + const handleApply = () => { + setApplied(true) + toast.success('Application submitted! We\'ll review within 48 hours.') + } + return ( - } - /> +
+ + {/* Page Header */} +
+

+ + Creator Space +

+

Your hub for managing and publishing poses.

+
+ + {/* ── Section 1: Stats Bar ── */} +
+ {STATS.map((stat, i) => { + const Icon = stat.icon + return ( + +
+ {stat.label} + +
+
{stat.value}
+
{stat.trend} this week
+
+ ) + })} +
+ + {/* ── Section 2: Published Poses ── */} +
+

+ + Published Poses +

+ +
+ {SAMPLE_POSES.map((pose, i) => ( + + {/* Preview image */} +
+ {pose.name} +
+ + {/* Card body */} +
+
+ {pose.name} +
+ + +
+
+ + {/* Stats row */} +
+ + + {pose.views.toLocaleString()} + + + + {pose.likes.toLocaleString()} + +
+ + {/* Tags */} +
+ {pose.tags.map(tag => ( + + {tag} + + ))} +
+
+
+ ))} +
+
+ + {/* ── Section 3: Publish New Pose ── */} + +

+ + Publish New Pose +

+ +
+
+ + setPoseName(e.target.value)} + className="bg-gray-950 border-white/10 text-white placeholder:text-gray-600 focus-visible:ring-primary/50" + /> +
+ +
+ +