diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c318884
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,136 @@
+name: WebDrop CI
+
+on:
+ push:
+ branches:
+ - main
+ - develop
+ pull_request:
+ branches:
+ - main
+ - develop
+
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Run linter
+ run: pnpm lint
+
+ unit-test:
+ name: Unit Tests
+ runs-on: ubuntu-latest
+ needs: lint
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Run unit tests
+ run: pnpm test run
+
+ - name: Generate coverage report
+ run: pnpm test:coverage run
+
+ - name: Upload coverage report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: coverage-report
+ path: coverage/
+ retention-days: 7
+
+ e2e-test:
+ name: E2E Tests
+ runs-on: ubuntu-latest
+ needs: unit-test
+ permissions:
+ contents: read
+
+ # Set environment variables for all test steps
+ env:
+ NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
+ TESTMAIL_API_KEY: ${{ secrets.TESTMAIL_API_KEY }}
+ TESTMAIL_NAMESPACE: ${{ secrets.TESTMAIL_NAMESPACE }} # e.g., "abcde"
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Install Playwright Browsers
+ run: pnpm exec playwright install --with-deps
+
+ - name: Build Next.js App
+ run: pnpm build
+
+ - name: Run Playwright tests
+ run: pnpm test:e2e
+
+ - name: Upload Playwright report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 7
+
+ # deploy-dev:
+ # name: Deploy to Development
+ # runs-on: ubuntu-latest
+ # needs: e2e-test
+ # if: github.ref == 'refs/heads/develop'
+ # steps:
+ # - name: Checkout code
+ # uses: actions/checkout@v4
+ #
+ # - name: Deploy to Vercel (or other provider)
+ # # Add your deployment steps here
+ # run: echo "Deploying to dev..."
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 37c2b6f..49a0512 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,11 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+# Playwright
+/playwright-report/
+/test-results/
+/playwright/.auth/
+
+# Test coverage
+/coverage/
\ No newline at end of file
diff --git a/README.md b/README.md
index d02fd8d..eedfa97 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,31 @@ Open [http://localhost:3000](http://localhost:3000) in your browser to see the r
---
+## ๐งช Testing
+
+WebDrop has comprehensive test coverage including unit tests and end-to-end tests.
+
+### Running Tests
+
+```bash
+# Run unit tests
+pnpm test
+
+# Run E2E tests (requires build first)
+pnpm build
+pnpm test:e2e
+
+# Run all tests
+pnpm test:all
+
+# Generate coverage report
+pnpm test:coverage
+```
+
+For detailed testing documentation, see [TESTING.md](TESTING.md).
+
+---
+
## โ ๏ธ File Size Limit
This application is designed to chunk files and send them peer-to-peer. The file chunks are re-assembled in the **receiver's browser memory (RAM)**.
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 0000000..5687902
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,276 @@
+# Testing Documentation
+
+This document describes the testing strategy and how to run tests for the WebDrop application.
+
+## Testing Stack
+
+- **Unit & Component Tests**: [Vitest](https://vitest.dev/) + [@testing-library/react](https://testing-library.com/react)
+- **E2E Tests**: [Playwright](https://playwright.dev/)
+- **Coverage**: Vitest Coverage (v8)
+
+## Test Structure
+
+```
+tests/
+โโโ unit/ # Unit tests
+โ โโโ lib/ # Library/utility tests
+โ โ โโโ utils.test.ts
+โ โ โโโ webrtc/
+โ โ โโโ file-transfer.test.ts
+โ โโโ components/ # Component tests (to be added)
+โโโ auth.spec.ts # E2E: Authentication tests
+โโโ profile.spec.ts # E2E: Profile management tests
+โโโ room.spec.ts # E2E: Room management tests
+โโโ transfer.spec.ts # E2E: P2P file transfer tests
+โโโ ui.spec.ts # E2E: UI/UX tests
+โโโ global.setup.ts # Playwright global setup
+โโโ setup.ts # Vitest test setup
+โโโ fixtures/ # Test fixtures
+โโโ utils/ # Test utilities
+```
+
+## Running Tests
+
+### Unit Tests
+
+```bash
+# Run all unit tests
+pnpm test
+
+# Run tests in watch mode
+pnpm test
+
+# Run tests with UI
+pnpm test:ui
+
+# Run tests with coverage
+pnpm test:coverage
+```
+
+### E2E Tests
+
+**Prerequisites:**
+- Build the application first: `pnpm build`
+- Set up environment variables in `.env.local`:
+ - `NEXT_PUBLIC_SUPABASE_URL`
+ - `NEXT_PUBLIC_SUPABASE_ANON_KEY`
+ - `TESTMAIL_API_KEY` (for email-based tests)
+ - `TESTMAIL_NAMESPACE` (for email-based tests)
+
+```bash
+# Run all E2E tests
+pnpm test:e2e
+
+# Run E2E tests with UI
+pnpm test:e2e:ui
+
+# View last test report
+pnpm test:e2e:report
+```
+
+### Run All Tests
+
+```bash
+# Run both unit and E2E tests
+pnpm test:all
+```
+
+## Test Categories
+
+### Unit Tests
+
+#### Utility Tests (`tests/unit/lib/utils.test.ts`)
+- Tests the `cn()` utility for merging CSS classes
+- Validates conditional class handling
+- Tests Tailwind CSS class merging
+
+#### File Transfer Tests (`tests/unit/lib/webrtc/file-transfer.test.ts`)
+- Tests file chunking and sending logic
+- Tests metadata handling
+- Tests chunk accumulation and progress tracking
+- Tests blob assembly from chunks
+- Tests transfer cancellation and cleanup
+
+### E2E Tests
+
+#### Authentication Tests (`tests/auth.spec.ts`)
+- Sign-out flow
+- Sign-in with email
+- Session persistence
+
+#### Profile Tests (`tests/profile.spec.ts`)
+- Username updates
+- Avatar uploads
+- Profile persistence
+
+#### Room Management Tests (`tests/room.spec.ts`)
+- Room creation
+- Room joining with validation
+- Room ID format validation
+- Leaving rooms
+- Room persistence on page refresh
+- User presence display
+
+#### File Transfer Tests (`tests/transfer.spec.ts`)
+- P2P file sending and receiving
+- Progress tracking
+- Multi-user connection
+
+#### UI Tests (`tests/ui.spec.ts`)
+- Dark mode toggle
+- Theme persistence
+- Page navigation
+- Responsive design
+- User dropdown menu
+- Loading states
+
+## Writing Tests
+
+### Unit Test Example
+
+```typescript
+import { describe, it, expect } from 'vitest'
+import { myFunction } from '@/lib/myModule'
+
+describe('myFunction', () => {
+ it('should do something', () => {
+ const result = myFunction('input')
+ expect(result).toBe('expected output')
+ })
+})
+```
+
+### Component Test Example
+
+```typescript
+import { describe, it, expect } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { MyComponent } from '@/components/MyComponent'
+
+describe('MyComponent', () => {
+ it('should render correctly', () => {
+ render()
+ expect(screen.getByText('Hello')).toBeInTheDocument()
+ })
+})
+```
+
+### E2E Test Example
+
+```typescript
+import { test, expect } from '@playwright/test'
+
+test('should perform action', async ({ page }) => {
+ await page.goto('/path')
+ await page.click('button')
+ await expect(page.locator('text=Success')).toBeVisible()
+})
+```
+
+## CI/CD Integration
+
+Tests are automatically run in GitHub Actions on:
+- Push to `main` or `develop` branches
+- Pull requests to `main` or `develop` branches
+
+### CI Pipeline
+
+1. **Lint** - Code style checks
+2. **Unit Tests** - Fast unit and component tests
+ - Generates coverage report
+3. **E2E Tests** - Full integration tests
+ - Requires Supabase secrets
+ - Generates Playwright report
+
+## Coverage Goals
+
+- **Overall**: Aim for >80% code coverage
+- **Critical Paths**: 100% coverage for:
+ - File transfer logic
+ - Authentication flows
+ - Data validation
+
+### Coverage Exclusions
+
+The following are intentionally excluded from coverage metrics:
+
+- **`components/ui/**`**: These are third-party shadcn/ui components that come pre-tested from the shadcn/ui library. Testing these would duplicate the library's own test suite and provide minimal value.
+- **`tests/**`**: Test files themselves are excluded
+- **`*.config.{ts,js}`**: Configuration files
+- **`.next/`**: Next.js build artifacts
+- **`playwright/`**: Playwright-specific files
+- **`node_modules/`**: Third-party dependencies
+
+## Test Best Practices
+
+1. **Isolation**: Each test should be independent
+2. **Speed**: Keep unit tests fast (<1s each)
+3. **Clarity**: Use descriptive test names
+4. **Cleanup**: Always clean up after tests
+5. **Mocking**: Mock external dependencies in unit tests
+6. **Fixtures**: Use test fixtures for consistent data
+7. **Assertions**: Make specific, meaningful assertions
+
+## Debugging Tests
+
+### Unit Tests
+
+```bash
+# Run specific test file
+pnpm test tests/unit/lib/utils.test.ts
+
+# Run tests matching pattern
+pnpm test -t "should merge"
+
+# Debug with Chrome DevTools
+pnpm test --inspect-brk
+```
+
+### E2E Tests
+
+```bash
+# Run in headed mode
+pnpm test:e2e --headed
+
+# Run specific test file
+pnpm test:e2e tests/auth.spec.ts
+
+# Debug specific test
+pnpm test:e2e --debug
+```
+
+## Common Issues
+
+### Unit Tests
+
+**Issue**: `chunk.arrayBuffer is not a function`
+**Solution**: Ensure `tests/setup.ts` includes the Blob polyfill
+
+**Issue**: Module import errors
+**Solution**: Check path aliases in `vitest.config.ts`
+
+### E2E Tests
+
+**Issue**: Tests timing out
+**Solution**: Increase timeout in test or check network connectivity
+
+**Issue**: Authentication failures
+**Solution**: Verify `TESTMAIL_API_KEY` and `TESTMAIL_NAMESPACE` environment variables
+
+## Future Testing Plans
+
+- [ ] Add component tests for all React components
+- [ ] Add integration tests for API routes
+- [ ] Add visual regression tests
+- [ ] Add performance tests
+- [ ] Add accessibility tests (a11y)
+- [ ] Add load/stress tests for file transfers
+- [ ] Implement test data factories
+- [ ] Add mutation testing
+
+## Resources
+
+- [Vitest Documentation](https://vitest.dev/)
+- [Testing Library Documentation](https://testing-library.com/)
+- [Playwright Documentation](https://playwright.dev/)
+- [Next.js Testing Guide](https://nextjs.org/docs/testing)
diff --git a/package.json b/package.json
index bb6c606..a77fda9 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,14 @@
"build": "next build",
"dev": "next dev",
"lint": "eslint .",
- "start": "next start"
+ "start": "next start",
+ "test": "vitest",
+ "test:ui": "vitest --ui",
+ "test:coverage": "vitest --coverage",
+ "test:e2e": "playwright test",
+ "test:e2e:ui": "playwright test --ui",
+ "test:e2e:report": "playwright show-report",
+ "test:all": "vitest run && playwright test"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
@@ -63,13 +70,23 @@
"zod": "3.25.76"
},
"devDependencies": {
+ "@playwright/test": "^1.45.3",
"@tailwindcss/postcss": "^4.1.9",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@testmail.app/graphql-request": "^1.8.4",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
+ "@vitejs/plugin-react": "^5.1.0",
+ "@vitest/coverage-v8": "^4.0.7",
+ "@vitest/ui": "^4.0.7",
+ "jsdom": "^27.1.0",
"postcss": "^8.5",
"tailwindcss": "^4.1.9",
"tw-animate-css": "1.3.3",
- "typescript": "^5"
+ "typescript": "^5",
+ "vitest": "^4.0.7"
}
}
\ No newline at end of file
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 0000000..02ac1e8
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,87 @@
+import { defineConfig, devices } from '@playwright/test';
+import path from 'path';
+
+// Define storage state paths
+export const USER_1_STATE = path.join(__dirname, 'playwright/.auth/user1.json');
+export const USER_2_STATE = path.join(__dirname, 'playwright/.auth/user2.json');
+
+export default defineConfig({
+ testDir: './tests',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: 'html',
+
+ // Use the dedicated 'setup' project below to seed auth states.
+
+ use: {
+ baseURL: 'http://localhost:3000',
+ trace: 'on-first-retry',
+ },
+
+ // Configure projects for major browsers
+ projects: [
+ // --- Setup Project ---
+ // This project runs first to authenticate our two test users.
+ {
+ name: 'setup',
+ testMatch: 'global.setup.ts',
+ },
+
+ // --- Main Test Project (Chromium) ---
+ // This project depends on 'setup' and uses the auth state of User 1.
+ {
+ name: 'chromium-user1',
+ use: {
+ ...devices['Desktop Chrome'],
+ storageState: USER_1_STATE,
+ },
+ dependencies: ['setup'],
+ testIgnore: 'transfer.spec.ts', // Transfer test is special
+ },
+
+ // --- Transfer Test Project ---
+ // This project runs the P2P transfer test, which needs both users.
+ // It's separated because it has a different setup (launches 2 browsers).
+ {
+ name: 'chromium-transfer',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ dependencies: ['setup'],
+ testMatch: 'transfer.spec.ts',
+ },
+
+ /*
+ {
+ name: 'firefox',
+ use: {
+ ...devices['Desktop Firefox'],
+ storageState: USER_1_STATE,
+ },
+ dependencies: ['setup'],
+ testIgnore: 'transfer.spec.ts',
+ },
+
+ {
+ name: 'webkit',
+ use: {
+ ...devices['Desktop Safari'],
+ storageState: USER_1_STATE,
+ },
+ dependencies: ['setup'],
+ testIgnore: 'transfer.spec.ts',
+ },
+ */
+ ],
+
+ // Run your local dev server before starting the tests
+ webServer: {
+ command: 'pnpm start', // Assumes you have run `pnpm build` first
+ url: 'http://localhost:3000',
+ reuseExistingServer: !process.env.CI,
+ stdout: 'ignore',
+ stderr: 'pipe',
+ },
+});
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2b063ed..3e36bad 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -100,7 +100,7 @@ importers:
version: 2.78.0
'@vercel/analytics':
specifier: latest
- version: 1.5.0(next@16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
+ version: 1.5.0(next@16.0.0(@babel/core@7.28.5)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.5.0)
@@ -127,7 +127,7 @@ importers:
version: 0.454.0(react@19.2.0)
next:
specifier: 16.0.0
- version: 16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ version: 16.0.0(@babel/core@7.28.5)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
next-themes:
specifier: latest
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -165,9 +165,24 @@ importers:
specifier: 3.25.76
version: 3.25.76
devDependencies:
+ '@playwright/test':
+ specifier: ^1.45.3
+ version: 1.56.1
'@tailwindcss/postcss':
specifier: ^4.1.9
version: 4.1.9
+ '@testing-library/jest-dom':
+ specifier: ^6.9.1
+ version: 6.9.1
+ '@testing-library/react':
+ specifier: ^16.3.0
+ version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@testing-library/user-event':
+ specifier: ^14.6.1
+ version: 14.6.1(@testing-library/dom@10.4.1)
+ '@testmail.app/graphql-request':
+ specifier: ^1.8.4
+ version: 1.8.4
'@types/node':
specifier: ^22
version: 22.0.0
@@ -177,6 +192,18 @@ importers:
'@types/react-dom':
specifier: ^19
version: 19.0.0
+ '@vitejs/plugin-react':
+ specifier: ^5.1.0
+ version: 5.1.0(vite@7.2.0(@types/node@22.0.0)(jiti@2.6.1)(lightningcss@1.30.1))
+ '@vitest/coverage-v8':
+ specifier: ^4.0.7
+ version: 4.0.7(vitest@4.0.7)
+ '@vitest/ui':
+ specifier: ^4.0.7
+ version: 4.0.7(vitest@4.0.7)
+ jsdom:
+ specifier: ^27.1.0
+ version: 27.1.0
postcss:
specifier: ^8.5
version: 8.5.0
@@ -189,9 +216,18 @@ importers:
typescript:
specifier: ^5
version: 5.0.2
+ vitest:
+ specifier: ^4.0.7
+ version: 4.0.7(@types/node@22.0.0)(@vitest/ui@4.0.7)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.1)
packages:
+ '@acemir/cssom@0.9.19':
+ resolution: {integrity: sha512-Pp2gAQXPZ2o7lt4j0IMwNRXqQ3pagxtDj5wctL5U2Lz4oV0ocDNlkgx4DpxfyKav4S/bePuI+SMqcBSUHLy9kg==}
+
+ '@adobe/css-tools@4.4.4':
+ resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
+
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@@ -200,16 +236,300 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
+ '@asamuzakjp/css-color@4.0.5':
+ resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==}
+
+ '@asamuzakjp/dom-selector@6.7.4':
+ resolution: {integrity: sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==}
+
+ '@asamuzakjp/nwsapi@2.3.9':
+ resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
+
+ '@babel/code-frame@7.27.1':
+ resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.28.5':
+ resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.28.5':
+ resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.28.5':
+ resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.27.2':
+ resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.27.1':
+ resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.3':
+ resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.27.1':
+ resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.28.4':
+ resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.28.5':
+ resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1':
+ resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1':
+ resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
+ '@babel/template@7.27.2':
+ resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.28.5':
+ resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.28.5':
+ resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
+ engines: {node: '>=6.9.0'}
+
+ '@bcoe/v8-coverage@1.0.2':
+ resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
+ engines: {node: '>=18'}
+
+ '@csstools/color-helpers@5.1.0':
+ resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
+ engines: {node: '>=18'}
+
+ '@csstools/css-calc@2.1.4':
+ resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-color-parser@3.1.0':
+ resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5':
+ resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-syntax-patches-for-csstree@1.0.15':
+ resolution: {integrity: sha512-q0p6zkVq2lJnmzZVPR33doA51G7YOja+FBvRdp5ISIthL0MtFCgYHHhR563z9WFGxcOn0WfjSkPDJ5Qig3H3Sw==}
+ engines: {node: '>=18'}
+
+ '@csstools/css-tokenizer@3.0.4':
+ resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
+ engines: {node: '>=18'}
+
'@date-fns/tz@1.2.0':
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
'@emnapi/runtime@1.7.0':
resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==}
+ '@esbuild/aix-ppc64@0.25.12':
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.12':
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.12':
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.12':
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.12':
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.12':
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.12':
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.12':
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.12':
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.12':
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.12':
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.12':
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.12':
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.12':
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.12':
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.12':
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.12':
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
'@floating-ui/core@1.7.3':
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
@@ -363,6 +683,9 @@ packages:
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
@@ -424,6 +747,14 @@ packages:
cpu: [x64]
os: [win32]
+ '@playwright/test@1.56.1':
+ resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ '@polka/url@1.0.0-next.29':
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+
'@radix-ui/number@1.1.0':
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
@@ -1318,6 +1649,122 @@ packages:
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+ '@rolldown/pluginutils@1.0.0-beta.43':
+ resolution: {integrity: sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==}
+
+ '@rollup/rollup-android-arm-eabi@4.52.5':
+ resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.52.5':
+ resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.52.5':
+ resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.52.5':
+ resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.52.5':
+ resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.52.5':
+ resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.52.5':
+ resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.52.5':
+ resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.52.5':
+ resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.52.5':
+ resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-gnu@4.52.5':
+ resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.52.5':
+ resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.52.5':
+ resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.52.5':
+ resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.52.5':
+ resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.52.5':
+ resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.52.5':
+ resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-openharmony-arm64@4.52.5':
+ resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.52.5':
+ resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.52.5':
+ resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.52.5':
+ resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.52.5':
+ resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@standard-schema/spec@1.0.0':
+ resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
+
'@supabase/auth-js@2.78.0':
resolution: {integrity: sha512-cXDtu1U0LeZj/xfnFoV7yCze37TcbNo8FCxy1FpqhMbB9u9QxxDSW6pA5gm/07Ei7m260Lof4CZx67Cu6DPeig==}
@@ -1436,6 +1883,56 @@ packages:
'@tailwindcss/postcss@4.1.9':
resolution: {integrity: sha512-v3DKzHibZO8ioVDmuVHCW1PR0XSM7nS40EjZFJEA1xPuvTuQPaR5flE1LyikU3hu2u1KNWBtEaSe8qsQjX3tyg==}
+ '@testing-library/dom@10.4.1':
+ resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
+ engines: {node: '>=18'}
+
+ '@testing-library/jest-dom@6.9.1':
+ resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
+ engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+
+ '@testing-library/react@16.3.0':
+ resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==}
+ 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
+
+ '@testing-library/user-event@14.6.1':
+ resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
+ '@testmail.app/graphql-request@1.8.4':
+ resolution: {integrity: sha512-tHTXfoSLNlZJIKLI/6lpNYriIqigg8Ga8JRatY3978YL4hEvt5DswcRPFAd2O3v/ulv5OfAIzWF66Z5XteNfFw==}
+
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
'@types/d3-array@3.2.2':
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
@@ -1463,6 +1960,12 @@ packages:
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
'@types/node@22.0.0':
resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==}
@@ -1504,12 +2007,87 @@ packages:
vue-router:
optional: true
- aria-hidden@1.2.6:
- resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
- engines: {node: '>=10'}
+ '@vitejs/plugin-react@5.1.0':
+ resolution: {integrity: sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
- autoprefixer@10.4.20:
- resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
+ '@vitest/coverage-v8@4.0.7':
+ resolution: {integrity: sha512-MXc+kEA5EUwMMGmNt1S6CIOEl/iCmAhGZQq1QgMNC3/QpYSOxkysEi6pxWhkqJ7YT/RduoVEV5rxFxHG18V3LA==}
+ peerDependencies:
+ '@vitest/browser': 4.0.7
+ vitest: 4.0.7
+ peerDependenciesMeta:
+ '@vitest/browser':
+ optional: true
+
+ '@vitest/expect@4.0.7':
+ resolution: {integrity: sha512-jGRG6HghnJDjljdjYIoVzX17S6uCVCBRFnsgdLGJ6CaxfPh8kzUKe/2n533y4O/aeZ/sIr7q7GbuEbeGDsWv4Q==}
+
+ '@vitest/mocker@4.0.7':
+ resolution: {integrity: sha512-OsDwLS7WnpuNslOV6bJkXVYVV/6RSc4eeVxV7h9wxQPNxnjRvTTrIikfwCbMyl8XJmW6oOccBj2Q07YwZtQcCw==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0-0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.0.7':
+ resolution: {integrity: sha512-YY//yxqTmk29+/pK+Wi1UB4DUH3lSVgIm+M10rAJ74pOSMgT7rydMSc+vFuq9LjZLhFvVEXir8EcqMke3SVM6Q==}
+
+ '@vitest/runner@4.0.7':
+ resolution: {integrity: sha512-orU1lsu4PxLEcDWfjVCNGIedOSF/YtZ+XMrd1PZb90E68khWCNzD8y1dtxtgd0hyBIQk8XggteKN/38VQLvzuw==}
+
+ '@vitest/snapshot@4.0.7':
+ resolution: {integrity: sha512-xJL+Nkw0OjaUXXQf13B8iKK5pI9QVtN9uOtzNHYuG/o/B7fIEg0DQ+xOe0/RcqwDEI15rud1k7y5xznBKGUXAA==}
+
+ '@vitest/spy@4.0.7':
+ resolution: {integrity: sha512-FW4X8hzIEn4z+HublB4hBF/FhCVaXfIHm8sUfvlznrcy1MQG7VooBgZPMtVCGZtHi0yl3KESaXTqsKh16d8cFg==}
+
+ '@vitest/ui@4.0.7':
+ resolution: {integrity: sha512-aIFPci9xoTmVkxpqsSKcRG/Hn0lTy421jsCehHydYeIMd+getn0Pue0JqY5cW8yZglZjMeX0YfIy5wDtQDHEcA==}
+ peerDependencies:
+ vitest: 4.0.7
+
+ '@vitest/utils@4.0.7':
+ resolution: {integrity: sha512-HNrg9CM/Z4ZWB6RuExhuC6FPmLipiShKVMnT9JlQvfhwR47JatWLChA6mtZqVHqypE6p/z6ofcjbyWpM7YLxPQ==}
+
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+
+ aria-hidden@1.2.6:
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+ engines: {node: '>=10'}
+
+ aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ ast-v8-to-istanbul@0.3.8:
+ resolution: {integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==}
+
+ autoprefixer@10.4.20:
+ resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
@@ -1519,6 +2097,9 @@ packages:
resolution: {integrity: sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==}
hasBin: true
+ bidi-js@1.0.3:
+ resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
+
browserslist@4.27.0:
resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -1527,6 +2108,10 @@ packages:
caniuse-lite@1.0.30001753:
resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==}
+ chai@6.2.0:
+ resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==}
+ engines: {node: '>=18'}
+
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
@@ -1547,10 +2132,27 @@ packages:
react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
+ cross-fetch@3.2.0:
+ resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
+
+ css-tree@3.1.0:
+ resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ css.escape@1.5.1:
+ resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+
+ cssstyle@5.3.2:
+ resolution: {integrity: sha512-zDMqXh8Vs1CdRYZQ2M633m/SFgcjlu8RB8b/1h82i+6vpArF507NSYIWJHGlJaTWoS+imcnctmEz43txhbVkOw==}
+ engines: {node: '>=20'}
+
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -1598,15 +2200,35 @@ packages:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
+ data-urls@6.0.0:
+ resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
+ engines: {node: '>=20'}
+
date-fns-jalali@4.1.0-0:
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -1614,6 +2236,12 @@ packages:
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
+ dom-accessibility-api@0.6.3:
+ resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
+
dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
@@ -1637,20 +2265,74 @@ packages:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
+ es-module-lexer@1.7.0:
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
+ es6-promise@4.2.8:
+ resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
+
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+ expect-type@1.2.2:
+ resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
+ engines: {node: '>=12.0.0'}
+
fast-equals@5.3.2:
resolution: {integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==}
engines: {node: '>=6.0.0'}
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fetch-retry@3.2.3:
+ resolution: {integrity: sha512-baMBEv4uZ1X1cUZAvnM+C9XI7tl4CgHgJE0KBHo3JzuXO7atOeWD5HSkDA2oLYpbzLTZNslFckLkIn6T96hlew==}
+
+ fflate@0.8.2:
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+
+ flatted@3.3.3:
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
@@ -1658,6 +2340,33 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ html-encoding-sniffer@4.0.0:
+ resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
+ engines: {node: '>=18'}
+
+ html-escaper@2.0.2:
+ resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+
+ http-proxy-agent@7.0.2:
+ resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
+ engines: {node: '>= 14'}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
+ indent-string@4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+
input-otp@1.4.1:
resolution: {integrity: sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==}
peerDependencies:
@@ -1668,6 +2377,25 @@ packages:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
+ istanbul-lib-coverage@3.2.2:
+ resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
+ engines: {node: '>=8'}
+
+ istanbul-lib-report@3.0.1:
+ resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+ engines: {node: '>=10'}
+
+ istanbul-lib-source-maps@5.0.6:
+ resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
+ engines: {node: '>=10'}
+
+ istanbul-reports@3.2.0:
+ resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
+ engines: {node: '>=8'}
+
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
@@ -1675,6 +2403,28 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ js-tokens@9.0.1:
+ resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+
+ jsdom@27.1.0:
+ resolution: {integrity: sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
lightningcss-darwin-arm64@1.30.1:
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
engines: {node: '>= 12.0.0'}
@@ -1746,14 +2496,39 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
+ lru-cache@11.2.2:
+ resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
+ engines: {node: 20 || >=22}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
lucide-react@0.454.0:
resolution: {integrity: sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+ magicast@0.3.5:
+ resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
+
+ make-dir@4.0.0:
+ resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+ engines: {node: '>=10'}
+
+ mdn-data@2.12.2:
+ resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
+
+ min-indent@1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
+
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -1762,6 +2537,13 @@ packages:
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 18'}
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1794,6 +2576,15 @@ packages:
sass:
optional: true
+ node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
@@ -1805,9 +2596,29 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
+ parse5@8.0.0:
+ resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
+ playwright-core@1.56.1:
+ resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.56.1:
+ resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
@@ -1819,9 +2630,21 @@ packages:
resolution: {integrity: sha512-27VKOqrYfPncKA2NrFOVhP5MGAfHKLYn/Q0mz9cNQyRAKYi3VNHwYU2qKKqPCqgBmeeJ0uAFB56NumXZ5ZReXg==}
engines: {node: ^10 || ^12 || >=14}
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
react-day-picker@9.8.0:
resolution: {integrity: sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==}
engines: {node: '>=18'}
@@ -1842,9 +2665,16 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+ react-refresh@0.18.0:
+ resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
+ engines: {node: '>=0.10.0'}
+
react-remove-scroll-bar@2.3.8:
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
engines: {node: '>=10'}
@@ -1907,9 +2737,33 @@ packages:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ redent@3.0.0:
+ resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
+ engines: {node: '>=8'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ rollup@4.52.5:
+ resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
@@ -1919,6 +2773,13 @@ packages:
resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ sirv@3.0.2:
+ resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
+ engines: {node: '>=18'}
+
sonner@1.7.4:
resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==}
peerDependencies:
@@ -1929,6 +2790,16 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@3.10.0:
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+
+ strip-indent@3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+ engines: {node: '>=8'}
+
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@@ -1942,6 +2813,13 @@ packages:
babel-plugin-macros:
optional: true
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
tailwind-merge@2.5.5:
resolution: {integrity: sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==}
@@ -1964,9 +2842,42 @@ packages:
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@0.3.2:
+ resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ tinyrainbow@3.0.3:
+ resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
+ engines: {node: '>=14.0.0'}
+
+ tldts-core@7.0.17:
+ resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==}
+
+ tldts@7.0.17:
+ resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==}
+ hasBin: true
+
+ totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+
+ tough-cookie@6.0.0:
+ resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
+ engines: {node: '>=16'}
+
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+ tr46@6.0.0:
+ resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
+ engines: {node: '>=20'}
+
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -2021,12 +2932,111 @@ packages:
victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+ vite@7.2.0:
+ resolution: {integrity: sha512-C/Naxf8H0pBx1PA4BdpT+c/5wdqI9ILMdwjSMILw7tVIh3JsxzZqdeTLmmdaoh5MYUEOyBnM9K3o0DzoZ/fe+w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitest@4.0.7:
+ resolution: {integrity: sha512-xQroKAadK503CrmbzCISvQUjeuvEZzv6U0wlnlVFOi5i3gnzfH4onyQ29f3lzpe0FresAiTAd3aqK0Bi/jLI8w==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/debug': ^4.1.12
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.0.7
+ '@vitest/browser-preview': 4.0.7
+ '@vitest/browser-webdriverio': 4.0.7
+ '@vitest/ui': 4.0.7
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/debug':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+ webidl-conversions@8.0.0:
+ resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==}
+ engines: {node: '>=20'}
+
+ whatwg-encoding@3.1.1:
+ resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
+ engines: {node: '>=18'}
+
+ whatwg-mimetype@4.0.0:
+ resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
+ engines: {node: '>=18'}
+
+ whatwg-url@15.1.0:
+ resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==}
+ engines: {node: '>=20'}
+
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
@@ -2039,6 +3049,16 @@ packages:
utf-8-validate:
optional: true
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
@@ -2048,6 +3068,10 @@ packages:
snapshots:
+ '@acemir/cssom@0.9.19': {}
+
+ '@adobe/css-tools@4.4.4': {}
+
'@alloc/quick-lru@5.2.0': {}
'@ampproject/remapping@2.3.0':
@@ -2055,8 +3079,162 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
+ '@asamuzakjp/css-color@4.0.5':
+ dependencies:
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+ lru-cache: 11.2.2
+
+ '@asamuzakjp/dom-selector@6.7.4':
+ dependencies:
+ '@asamuzakjp/nwsapi': 2.3.9
+ bidi-js: 1.0.3
+ css-tree: 3.1.0
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.2.2
+
+ '@asamuzakjp/nwsapi@2.3.9': {}
+
+ '@babel/code-frame@7.27.1':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.28.5': {}
+
+ '@babel/core@7.28.5':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.28.5
+ '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5)
+ '@babel/helpers': 7.28.4
+ '@babel/parser': 7.28.5
+ '@babel/template': 7.27.2
+ '@babel/traverse': 7.28.5
+ '@babel/types': 7.28.5
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.28.5':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.27.2':
+ dependencies:
+ '@babel/compat-data': 7.28.5
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.27.0
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-module-imports@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.28.5
+ '@babel/types': 7.28.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/helper-module-imports': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.28.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.27.1': {}
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.28.4':
+ dependencies:
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.5
+
+ '@babel/parser@7.28.5':
+ dependencies:
+ '@babel/types': 7.28.5
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/helper-plugin-utils': 7.27.1
+
'@babel/runtime@7.28.4': {}
+ '@babel/template@7.27.2':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+
+ '@babel/traverse@7.28.5':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.28.5
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.28.5
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.5
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.28.5':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@bcoe/v8-coverage@1.0.2': {}
+
+ '@csstools/color-helpers@5.1.0': {}
+
+ '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/color-helpers': 5.1.0
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-syntax-patches-for-csstree@1.0.15': {}
+
+ '@csstools/css-tokenizer@3.0.4': {}
+
'@date-fns/tz@1.2.0': {}
'@emnapi/runtime@1.7.0':
@@ -2064,6 +3242,84 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@esbuild/aix-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm@0.25.12':
+ optional: true
+
+ '@esbuild/android-x64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.12':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.12':
+ optional: true
+
'@floating-ui/core@1.7.3':
dependencies:
'@floating-ui/utils': 0.2.10
@@ -2183,6 +3439,11 @@ snapshots:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
@@ -2218,6 +3479,12 @@ snapshots:
'@next/swc-win32-x64-msvc@16.0.0':
optional: true
+ '@playwright/test@1.56.1':
+ dependencies:
+ playwright: 1.56.1
+
+ '@polka/url@1.0.0-next.29': {}
+
'@radix-ui/number@1.1.0': {}
'@radix-ui/number@1.1.1': {}
@@ -3096,34 +4363,104 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.0
- '@radix-ui/react-use-size@1.1.1(@types/react@19.0.0)(react@19.2.0)':
- dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.0)(react@19.2.0)
- react: 19.2.0
- optionalDependencies:
- '@types/react': 19.0.0
+ '@radix-ui/react-use-size@1.1.1(@types/react@19.0.0)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.0)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.0.0
+
+ '@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.0.0
+ '@types/react-dom': 19.0.0
+
+ '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.0.0
+ '@types/react-dom': 19.0.0
+
+ '@radix-ui/rect@1.1.0': {}
+
+ '@radix-ui/rect@1.1.1': {}
+
+ '@rolldown/pluginutils@1.0.0-beta.43': {}
+
+ '@rollup/rollup-android-arm-eabi@4.52.5':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.52.5':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.52.5':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.52.5':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.52.5':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.52.5':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.52.5':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.52.5':
+ optional: true
- '@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
- dependencies:
- '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
- optionalDependencies:
- '@types/react': 19.0.0
- '@types/react-dom': 19.0.0
+ '@rollup/rollup-win32-arm64-msvc@4.52.5':
+ optional: true
- '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
- dependencies:
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
- react: 19.2.0
- react-dom: 19.2.0(react@19.2.0)
- optionalDependencies:
- '@types/react': 19.0.0
- '@types/react-dom': 19.0.0
+ '@rollup/rollup-win32-ia32-msvc@4.52.5':
+ optional: true
- '@radix-ui/rect@1.1.0': {}
+ '@rollup/rollup-win32-x64-gnu@4.52.5':
+ optional: true
- '@radix-ui/rect@1.1.1': {}
+ '@rollup/rollup-win32-x64-msvc@4.52.5':
+ optional: true
+
+ '@standard-schema/spec@1.0.0': {}
'@supabase/auth-js@2.78.0':
dependencies:
@@ -3253,6 +4590,75 @@ snapshots:
postcss: 8.5.0
tailwindcss: 4.1.9
+ '@testing-library/dom@10.4.1':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/runtime': 7.28.4
+ '@types/aria-query': 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ '@testing-library/jest-dom@6.9.1':
+ dependencies:
+ '@adobe/css-tools': 4.4.4
+ aria-query: 5.3.2
+ css.escape: 1.5.1
+ dom-accessibility-api: 0.6.3
+ picocolors: 1.1.1
+ redent: 3.0.0
+
+ '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@babel/runtime': 7.28.4
+ '@testing-library/dom': 10.4.1
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.0.0
+ '@types/react-dom': 19.0.0
+
+ '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
+ dependencies:
+ '@testing-library/dom': 10.4.1
+
+ '@testmail.app/graphql-request@1.8.4':
+ dependencies:
+ cross-fetch: 3.2.0
+ fetch-retry: 3.2.3
+ transitivePeerDependencies:
+ - encoding
+
+ '@types/aria-query@5.0.4': {}
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.28.5
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.28.5
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
'@types/d3-array@3.2.2': {}
'@types/d3-color@3.1.3': {}
@@ -3277,6 +4683,10 @@ snapshots:
'@types/d3-timer@3.0.2': {}
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/estree@1.0.8': {}
+
'@types/node@22.0.0':
dependencies:
undici-types: 6.11.1
@@ -3295,15 +4705,114 @@ snapshots:
dependencies:
'@types/node': 22.0.0
- '@vercel/analytics@1.5.0(next@16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)':
+ '@vercel/analytics@1.5.0(next@16.0.0(@babel/core@7.28.5)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)':
optionalDependencies:
- next: 16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ next: 16.0.0(@babel/core@7.28.5)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0
+ '@vitejs/plugin-react@5.1.0(vite@7.2.0(@types/node@22.0.0)(jiti@2.6.1)(lightningcss@1.30.1))':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5)
+ '@rolldown/pluginutils': 1.0.0-beta.43
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.18.0
+ vite: 7.2.0(@types/node@22.0.0)(jiti@2.6.1)(lightningcss@1.30.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vitest/coverage-v8@4.0.7(vitest@4.0.7)':
+ dependencies:
+ '@bcoe/v8-coverage': 1.0.2
+ '@vitest/utils': 4.0.7
+ ast-v8-to-istanbul: 0.3.8
+ debug: 4.4.3
+ istanbul-lib-coverage: 3.2.2
+ istanbul-lib-report: 3.0.1
+ istanbul-lib-source-maps: 5.0.6
+ istanbul-reports: 3.2.0
+ magicast: 0.3.5
+ std-env: 3.10.0
+ tinyrainbow: 3.0.3
+ vitest: 4.0.7(@types/node@22.0.0)(@vitest/ui@4.0.7)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vitest/expect@4.0.7':
+ dependencies:
+ '@standard-schema/spec': 1.0.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.0.7
+ '@vitest/utils': 4.0.7
+ chai: 6.2.0
+ tinyrainbow: 3.0.3
+
+ '@vitest/mocker@4.0.7(vite@7.2.0(@types/node@22.0.0)(jiti@2.6.1)(lightningcss@1.30.1))':
+ dependencies:
+ '@vitest/spy': 4.0.7
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 7.2.0(@types/node@22.0.0)(jiti@2.6.1)(lightningcss@1.30.1)
+
+ '@vitest/pretty-format@4.0.7':
+ dependencies:
+ tinyrainbow: 3.0.3
+
+ '@vitest/runner@4.0.7':
+ dependencies:
+ '@vitest/utils': 4.0.7
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.0.7':
+ dependencies:
+ '@vitest/pretty-format': 4.0.7
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.0.7': {}
+
+ '@vitest/ui@4.0.7(vitest@4.0.7)':
+ dependencies:
+ '@vitest/utils': 4.0.7
+ fflate: 0.8.2
+ flatted: 3.3.3
+ pathe: 2.0.3
+ sirv: 3.0.2
+ tinyglobby: 0.2.15
+ tinyrainbow: 3.0.3
+ vitest: 4.0.7(@types/node@22.0.0)(@vitest/ui@4.0.7)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.1)
+
+ '@vitest/utils@4.0.7':
+ dependencies:
+ '@vitest/pretty-format': 4.0.7
+ tinyrainbow: 3.0.3
+
+ agent-base@7.1.4: {}
+
+ ansi-regex@5.0.1: {}
+
+ ansi-styles@5.2.0: {}
+
aria-hidden@1.2.6:
dependencies:
tslib: 2.8.1
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
+ aria-query@5.3.2: {}
+
+ assertion-error@2.0.1: {}
+
+ ast-v8-to-istanbul@0.3.8:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ estree-walker: 3.0.3
+ js-tokens: 9.0.1
+
autoprefixer@10.4.20(postcss@8.5.0):
dependencies:
browserslist: 4.27.0
@@ -3316,6 +4825,10 @@ snapshots:
baseline-browser-mapping@2.8.23: {}
+ bidi-js@1.0.3:
+ dependencies:
+ require-from-string: 2.0.2
+
browserslist@4.27.0:
dependencies:
baseline-browser-mapping: 2.8.23
@@ -3326,6 +4839,8 @@ snapshots:
caniuse-lite@1.0.30001753: {}
+ chai@6.2.0: {}
+
chownr@3.0.0: {}
class-variance-authority@0.7.1:
@@ -3348,8 +4863,29 @@ snapshots:
- '@types/react'
- '@types/react-dom'
+ convert-source-map@2.0.0: {}
+
cookie@1.0.2: {}
+ cross-fetch@3.2.0:
+ dependencies:
+ node-fetch: 2.7.0
+ transitivePeerDependencies:
+ - encoding
+
+ css-tree@3.1.0:
+ dependencies:
+ mdn-data: 2.12.2
+ source-map-js: 1.2.1
+
+ css.escape@1.5.1: {}
+
+ cssstyle@5.3.2:
+ dependencies:
+ '@asamuzakjp/css-color': 4.0.5
+ '@csstools/css-syntax-patches-for-csstree': 1.0.15
+ css-tree: 3.1.0
+
csstype@3.1.3: {}
d3-array@3.2.4:
@@ -3390,16 +4926,33 @@ snapshots:
d3-timer@3.0.1: {}
+ data-urls@6.0.0:
+ dependencies:
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 15.1.0
+
date-fns-jalali@4.1.0-0: {}
date-fns@4.1.0: {}
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
decimal.js-light@2.5.1: {}
+ decimal.js@10.6.0: {}
+
+ dequal@2.0.3: {}
+
detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
+ dom-accessibility-api@0.5.16: {}
+
+ dom-accessibility-api@0.6.3: {}
+
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.28.4
@@ -3424,18 +4977,107 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
+ entities@6.0.1: {}
+
+ es-module-lexer@1.7.0: {}
+
+ es6-promise@4.2.8: {}
+
+ esbuild@0.25.12:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.12
+ '@esbuild/android-arm': 0.25.12
+ '@esbuild/android-arm64': 0.25.12
+ '@esbuild/android-x64': 0.25.12
+ '@esbuild/darwin-arm64': 0.25.12
+ '@esbuild/darwin-x64': 0.25.12
+ '@esbuild/freebsd-arm64': 0.25.12
+ '@esbuild/freebsd-x64': 0.25.12
+ '@esbuild/linux-arm': 0.25.12
+ '@esbuild/linux-arm64': 0.25.12
+ '@esbuild/linux-ia32': 0.25.12
+ '@esbuild/linux-loong64': 0.25.12
+ '@esbuild/linux-mips64el': 0.25.12
+ '@esbuild/linux-ppc64': 0.25.12
+ '@esbuild/linux-riscv64': 0.25.12
+ '@esbuild/linux-s390x': 0.25.12
+ '@esbuild/linux-x64': 0.25.12
+ '@esbuild/netbsd-arm64': 0.25.12
+ '@esbuild/netbsd-x64': 0.25.12
+ '@esbuild/openbsd-arm64': 0.25.12
+ '@esbuild/openbsd-x64': 0.25.12
+ '@esbuild/openharmony-arm64': 0.25.12
+ '@esbuild/sunos-x64': 0.25.12
+ '@esbuild/win32-arm64': 0.25.12
+ '@esbuild/win32-ia32': 0.25.12
+ '@esbuild/win32-x64': 0.25.12
+
escalade@3.2.0: {}
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
eventemitter3@4.0.7: {}
+ expect-type@1.2.2: {}
+
fast-equals@5.3.2: {}
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ fetch-retry@3.2.3:
+ dependencies:
+ es6-promise: 4.2.8
+
+ fflate@0.8.2: {}
+
+ flatted@3.3.3: {}
+
fraction.js@4.3.7: {}
+ fsevents@2.3.2:
+ optional: true
+
+ fsevents@2.3.3:
+ optional: true
+
+ gensync@1.0.0-beta.2: {}
+
get-nonce@1.0.1: {}
graceful-fs@4.2.11: {}
+ has-flag@4.0.0: {}
+
+ html-encoding-sniffer@4.0.0:
+ dependencies:
+ whatwg-encoding: 3.1.1
+
+ html-escaper@2.0.2: {}
+
+ http-proxy-agent@7.0.2:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ indent-string@4.0.0: {}
+
input-otp@1.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
@@ -3443,10 +5085,66 @@ snapshots:
internmap@2.0.3: {}
+ is-potential-custom-element-name@1.0.1: {}
+
+ istanbul-lib-coverage@3.2.2: {}
+
+ istanbul-lib-report@3.0.1:
+ dependencies:
+ istanbul-lib-coverage: 3.2.2
+ make-dir: 4.0.0
+ supports-color: 7.2.0
+
+ istanbul-lib-source-maps@5.0.6:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ debug: 4.4.3
+ istanbul-lib-coverage: 3.2.2
+ transitivePeerDependencies:
+ - supports-color
+
+ istanbul-reports@3.2.0:
+ dependencies:
+ html-escaper: 2.0.2
+ istanbul-lib-report: 3.0.1
+
jiti@2.6.1: {}
js-tokens@4.0.0: {}
+ js-tokens@9.0.1: {}
+
+ jsdom@27.1.0:
+ dependencies:
+ '@acemir/cssom': 0.9.19
+ '@asamuzakjp/dom-selector': 6.7.4
+ cssstyle: 5.3.2
+ data-urls: 6.0.0
+ decimal.js: 10.6.0
+ html-encoding-sniffer: 4.0.0
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ is-potential-custom-element-name: 1.0.1
+ parse5: 8.0.0
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 6.0.0
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 8.0.0
+ whatwg-encoding: 3.1.1
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 15.1.0
+ ws: 8.18.3
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ jsesc@3.1.0: {}
+
+ json5@2.2.3: {}
+
lightningcss-darwin-arm64@1.30.1:
optional: true
@@ -3498,20 +5196,46 @@ snapshots:
dependencies:
js-tokens: 4.0.0
+ lru-cache@11.2.2: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
lucide-react@0.454.0(react@19.2.0):
dependencies:
react: 19.2.0
+ lz-string@1.5.0: {}
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
+ magicast@0.3.5:
+ dependencies:
+ '@babel/parser': 7.28.5
+ '@babel/types': 7.28.5
+ source-map-js: 1.2.1
+
+ make-dir@4.0.0:
+ dependencies:
+ semver: 7.7.3
+
+ mdn-data@2.12.2: {}
+
+ min-indent@1.0.1: {}
+
minipass@7.1.2: {}
minizlib@3.1.0:
dependencies:
minipass: 7.1.2
+ mrmime@2.0.1: {}
+
+ ms@2.1.3: {}
+
nanoid@3.3.11: {}
next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
@@ -3519,7 +5243,7 @@ snapshots:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
- next@16.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ next@16.0.0(@babel/core@7.28.5)(@playwright/test@1.56.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
'@next/env': 16.0.0
'@swc/helpers': 0.5.15
@@ -3527,7 +5251,7 @@ snapshots:
postcss: 8.4.31
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
- styled-jsx: 5.1.6(react@19.2.0)
+ styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.0)
optionalDependencies:
'@next/swc-darwin-arm64': 16.0.0
'@next/swc-darwin-x64': 16.0.0
@@ -3537,19 +5261,40 @@ snapshots:
'@next/swc-linux-x64-musl': 16.0.0
'@next/swc-win32-arm64-msvc': 16.0.0
'@next/swc-win32-x64-msvc': 16.0.0
+ '@playwright/test': 1.56.1
sharp: 0.34.4
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
+ node-fetch@2.7.0:
+ dependencies:
+ whatwg-url: 5.0.0
+
node-releases@2.0.27: {}
normalize-range@0.1.2: {}
object-assign@4.1.1: {}
+ parse5@8.0.0:
+ dependencies:
+ entities: 6.0.1
+
+ pathe@2.0.3: {}
+
picocolors@1.1.1: {}
+ picomatch@4.0.3: {}
+
+ playwright-core@1.56.1: {}
+
+ playwright@1.56.1:
+ dependencies:
+ playwright-core: 1.56.1
+ optionalDependencies:
+ fsevents: 2.3.2
+
postcss-value-parser@4.2.0: {}
postcss@8.4.31:
@@ -3564,12 +5309,26 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
+ punycode@2.3.1: {}
+
react-day-picker@9.8.0(react@19.2.0):
dependencies:
'@date-fns/tz': 1.2.0
@@ -3588,8 +5347,12 @@ snapshots:
react-is@16.13.1: {}
+ react-is@17.0.2: {}
+
react-is@18.3.1: {}
+ react-refresh@0.18.0: {}
+
react-remove-scroll-bar@2.3.8(@types/react@19.0.0)(react@19.2.0):
dependencies:
react: 19.2.0
@@ -3658,10 +5421,52 @@ snapshots:
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
+ redent@3.0.0:
+ dependencies:
+ indent-string: 4.0.0
+ strip-indent: 3.0.0
+
+ require-from-string@2.0.2: {}
+
+ rollup@4.52.5:
+ dependencies:
+ '@types/estree': 1.0.8
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.52.5
+ '@rollup/rollup-android-arm64': 4.52.5
+ '@rollup/rollup-darwin-arm64': 4.52.5
+ '@rollup/rollup-darwin-x64': 4.52.5
+ '@rollup/rollup-freebsd-arm64': 4.52.5
+ '@rollup/rollup-freebsd-x64': 4.52.5
+ '@rollup/rollup-linux-arm-gnueabihf': 4.52.5
+ '@rollup/rollup-linux-arm-musleabihf': 4.52.5
+ '@rollup/rollup-linux-arm64-gnu': 4.52.5
+ '@rollup/rollup-linux-arm64-musl': 4.52.5
+ '@rollup/rollup-linux-loong64-gnu': 4.52.5
+ '@rollup/rollup-linux-ppc64-gnu': 4.52.5
+ '@rollup/rollup-linux-riscv64-gnu': 4.52.5
+ '@rollup/rollup-linux-riscv64-musl': 4.52.5
+ '@rollup/rollup-linux-s390x-gnu': 4.52.5
+ '@rollup/rollup-linux-x64-gnu': 4.52.5
+ '@rollup/rollup-linux-x64-musl': 4.52.5
+ '@rollup/rollup-openharmony-arm64': 4.52.5
+ '@rollup/rollup-win32-arm64-msvc': 4.52.5
+ '@rollup/rollup-win32-ia32-msvc': 4.52.5
+ '@rollup/rollup-win32-x64-gnu': 4.52.5
+ '@rollup/rollup-win32-x64-msvc': 4.52.5
+ fsevents: 2.3.3
+
+ safer-buffer@2.1.2: {}
+
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
scheduler@0.27.0: {}
- semver@7.7.3:
- optional: true
+ semver@6.3.1: {}
+
+ semver@7.7.3: {}
sharp@0.34.4:
dependencies:
@@ -3693,6 +5498,14 @@ snapshots:
'@img/sharp-win32-x64': 0.34.4
optional: true
+ siginfo@2.0.0: {}
+
+ sirv@3.0.2:
+ dependencies:
+ '@polka/url': 1.0.0-next.29
+ mrmime: 2.0.1
+ totalist: 3.0.1
+
sonner@1.7.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
@@ -3700,10 +5513,26 @@ snapshots:
source-map-js@1.2.1: {}
- styled-jsx@5.1.6(react@19.2.0):
+ stackback@0.0.2: {}
+
+ std-env@3.10.0: {}
+
+ strip-indent@3.0.0:
+ dependencies:
+ min-indent: 1.0.1
+
+ styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.0):
dependencies:
client-only: 0.0.1
react: 19.2.0
+ optionalDependencies:
+ '@babel/core': 7.28.5
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ symbol-tree@3.2.4: {}
tailwind-merge@2.5.5: {}
@@ -3725,8 +5554,35 @@ snapshots:
tiny-invariant@1.3.3: {}
+ tinybench@2.9.0: {}
+
+ tinyexec@0.3.2: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ tinyrainbow@3.0.3: {}
+
+ tldts-core@7.0.17: {}
+
+ tldts@7.0.17:
+ dependencies:
+ tldts-core: 7.0.17
+
+ totalist@3.0.1: {}
+
+ tough-cookie@6.0.0:
+ dependencies:
+ tldts: 7.0.17
+
tr46@0.0.3: {}
+ tr46@6.0.0:
+ dependencies:
+ punycode: 2.3.1
+
tslib@2.8.1: {}
tw-animate-css@1.3.3: {}
@@ -3786,15 +5642,97 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
+ vite@7.2.0(@types/node@22.0.0)(jiti@2.6.1)(lightningcss@1.30.1):
+ dependencies:
+ esbuild: 0.25.12
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+ postcss: 8.5.6
+ rollup: 4.52.5
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ '@types/node': 22.0.0
+ fsevents: 2.3.3
+ jiti: 2.6.1
+ lightningcss: 1.30.1
+
+ vitest@4.0.7(@types/node@22.0.0)(@vitest/ui@4.0.7)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.1):
+ dependencies:
+ '@vitest/expect': 4.0.7
+ '@vitest/mocker': 4.0.7(vite@7.2.0(@types/node@22.0.0)(jiti@2.6.1)(lightningcss@1.30.1))
+ '@vitest/pretty-format': 4.0.7
+ '@vitest/runner': 4.0.7
+ '@vitest/snapshot': 4.0.7
+ '@vitest/spy': 4.0.7
+ '@vitest/utils': 4.0.7
+ debug: 4.4.3
+ es-module-lexer: 1.7.0
+ expect-type: 1.2.2
+ magic-string: 0.30.21
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tinyrainbow: 3.0.3
+ vite: 7.2.0(@types/node@22.0.0)(jiti@2.6.1)(lightningcss@1.30.1)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 22.0.0
+ '@vitest/ui': 4.0.7(vitest@4.0.7)
+ jsdom: 27.1.0
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - yaml
+
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
webidl-conversions@3.0.1: {}
+ webidl-conversions@8.0.0: {}
+
+ whatwg-encoding@3.1.1:
+ dependencies:
+ iconv-lite: 0.6.3
+
+ whatwg-mimetype@4.0.0: {}
+
+ whatwg-url@15.1.0:
+ dependencies:
+ tr46: 6.0.0
+ webidl-conversions: 8.0.0
+
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
ws@8.18.3: {}
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
+
+ yallist@3.1.1: {}
+
yallist@5.0.0: {}
zod@3.25.76: {}
diff --git a/tests/auth.spec.ts b/tests/auth.spec.ts
new file mode 100644
index 0000000..12d4e25
--- /dev/null
+++ b/tests/auth.spec.ts
@@ -0,0 +1,53 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Authentication', () => {
+
+ test('should sign out successfully', async ({ page }) => {
+ // Page starts authenticated due to `storageState`
+ await page.goto('/room');
+
+ // Check that we are logged in
+ await expect(page.getByRole('button', { name: 'User avatar' })).toBeVisible();
+
+ // Sign out
+ await page.getByRole('button', { name: 'User avatar' }).click();
+ await page.getByRole('menuitem', { name: 'Sign Out' }).click();
+
+ // Assert redirection to login
+ await expect(page).toHaveURL('/auth/login');
+ await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible();
+ });
+
+ test('should sign in successfully', async ({ page, context }) => {
+ // 1. Get User 1's credentials from the auth setup
+ // Note: This is a simplified way. In a real setup, you'd pull this
+ // from the `global.setup.ts` state or a shared file.
+ // For this example, we'll read the email from the storage state (which is not ideal, but works)
+ // A better way is to write the email/password to a .env file from global.setup.
+
+ // For simplicity, we'll just log out and log back in
+ await page.goto('/room');
+ const userAvatar = page.getByRole('button', { name: 'User avatar' });
+ await expect(userAvatar).toBeVisible();
+
+ // Get email
+ await userAvatar.click();
+ const userEmail = await page.locator('p.text-xs.leading-none.text-muted-foreground').textContent();
+ await page.keyboard.press('Escape'); // Close dropdown
+
+ // Sign out
+ await userAvatar.click();
+ await page.getByRole('menuitem', { name: 'Sign Out' }).click();
+ await expect(page).toHaveURL('/auth/login');
+
+ // Sign back in
+ await page.getByLabel('Email').fill(userEmail!);
+ await page.getByLabel('Password').fill('password123');
+ await page.getByRole('button', { name: 'Sign in with Email' }).click();
+
+ // Assert successful login
+ await expect(page).toHaveURL('/room');
+ await expect(userAvatar).toBeVisible();
+ });
+
+});
\ No newline at end of file
diff --git a/tests/fixtures/test-avatar.png b/tests/fixtures/test-avatar.png
new file mode 100644
index 0000000..b0f46a5
Binary files /dev/null and b/tests/fixtures/test-avatar.png differ
diff --git a/tests/fixtures/test-file.txt b/tests/fixtures/test-file.txt
new file mode 100644
index 0000000..1a3702a
--- /dev/null
+++ b/tests/fixtures/test-file.txt
@@ -0,0 +1 @@
+This is a test file for WebDrop E2E testing.
\ No newline at end of file
diff --git a/tests/global.setup.ts b/tests/global.setup.ts
new file mode 100644
index 0000000..1085162
--- /dev/null
+++ b/tests/global.setup.ts
@@ -0,0 +1,56 @@
+import { test as setup, expect } from '@playwright/test';
+import { USER_1_STATE, USER_2_STATE } from '../playwright.config';
+import { generateTestEmail, waitForEmail, extractConfirmationLink } from './utils/testmail.helper';
+
+// Skip entire setup if required env vars are missing
+const MISSING_TESTMAIL_ENV = !process.env.TESTMAIL_NAMESPACE || !process.env.TESTMAIL_API_KEY;
+setup.skip(MISSING_TESTMAIL_ENV, 'TESTMAIL_NAMESPACE/TESTMAIL_API_KEY not set. Setup is skipped.');
+
+// --- Reusable Sign-Up Function ---
+async function signUpAndConfirm(page: any) {
+ const { tag, email } = generateTestEmail();
+ const password = 'password123';
+ const username = tag;
+
+ // 1. Navigate to sign-up
+ await page.goto('/auth/sign-up');
+
+ // 2. Fill and submit form
+ await page.getByLabel('Username').fill(username);
+ await page.getByLabel('Email').fill(email);
+ await page.getByLabel('Password').fill(password);
+ await page.getByLabel('Confirm Password').fill(password);
+ await page.getByRole('button', { name: 'Sign up with Email' }).click();
+
+ // 3. Wait for success page
+ await expect(page).toHaveURL('/auth/sign-up-success');
+
+ // 4. Get confirmation email (allow up to 60s)
+ const received = await waitForEmail(tag, 60000);
+ const emailBody: string = received?.html || received?.text || '';
+ const confirmationLink = extractConfirmationLink(emailBody);
+
+ // 5. Visit confirmation link
+ await page.goto(confirmationLink);
+
+ // 6. Should be redirected to the app
+ await expect(page).toHaveURL('/room');
+ await expect(page.getByLabel('Toggle dark mode')).toBeVisible(); // Wait for page to load
+
+ return { email, password, username };
+}
+
+// --- Setup ---
+setup.describe('Global Auth Setup', () => {
+
+ setup('create user 1', async ({ page }) => {
+ await signUpAndConfirm(page);
+ await page.context().storageState({ path: USER_1_STATE });
+ });
+
+ setup('create user 2', async ({ page }) => {
+ await signUpAndConfirm(page);
+ await page.context().storageState({ path: USER_2_STATE });
+ });
+
+});
\ No newline at end of file
diff --git a/tests/profile.spec.ts b/tests/profile.spec.ts
new file mode 100644
index 0000000..53b5fa6
--- /dev/null
+++ b/tests/profile.spec.ts
@@ -0,0 +1,49 @@
+import { test, expect } from '@playwright/test';
+import path from 'path';
+import { randomUUID } from 'crypto';
+
+test.describe('Profile Management', () => {
+
+ const testAvatarPath = path.join(__dirname, 'fixtures/test-avatar.png');
+ const newUsername = `TestUser-${randomUUID().split('-')[0]}`;
+
+ test.beforeEach(async ({ page }) => {
+ // All tests start authenticated (User 1) and on the profile page
+ await page.goto('/profile');
+ });
+
+ test('should update username', async ({ page }) => {
+ // 1. Update username
+ await page.getByLabel('Username').fill(newUsername);
+ await page.getByRole('button', { name: 'Save Changes' }).click();
+
+ // 2. Check for success toast
+ await expect(page.locator('text=Profile updated successfully!')).toBeVisible();
+
+ // 3. Reload page to ensure persistence
+ await page.reload();
+
+ // 4. Assert new username is saved
+ await expect(page.getByLabel('Username')).toHaveValue(newUsername);
+ });
+
+ test('should upload a new avatar', async ({ page }) => {
+ // 1. Set up file input
+ const fileChooserPromise = page.waitForEvent('filechooser');
+ await page.getByRole('button', { name: 'Choose Image' }).click();
+ const fileChooser = await fileChooserPromise;
+ await fileChooser.setFiles(testAvatarPath);
+
+ // 2. Wait for upload to complete
+ await expect(page.getByRole('button', { name: 'Uploading...' })).toBeVisible();
+ await expect(page.locator('text=Avatar updated successfully!')).toBeVisible({ timeout: 10000 });
+
+ // 3. Get the new avatar URL
+ const avatarImg = page.locator('.h-24.w-24 img');
+ const newAvatarSrc = await avatarImg.getAttribute('src');
+
+ // 4. Assert the new URL is from Supabase storage and not the placeholder
+ expect(newAvatarSrc).not.toContain('placeholder.svg');
+ expect(newAvatarSrc).toContain('supabase.co');
+ });
+});
\ No newline at end of file
diff --git a/tests/room.spec.ts b/tests/room.spec.ts
new file mode 100644
index 0000000..f6ce5a9
--- /dev/null
+++ b/tests/room.spec.ts
@@ -0,0 +1,99 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Room Management', () => {
+
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/room');
+ });
+
+ test('should create a new room successfully', async ({ page }) => {
+ // Click create room button
+ await page.getByRole('button', { name: 'Create New Room' }).click();
+
+ // Should show connected room status
+ await expect(page.locator('text=Connected Room')).toBeVisible({ timeout: 10000 });
+
+ // Should display an 8-character room ID
+ const roomIdElement = page.locator('.font-mono.text-3xl');
+ await expect(roomIdElement).toBeVisible();
+ const roomId = await roomIdElement.textContent();
+ expect(roomId).toHaveLength(8);
+ expect(roomId).toMatch(/^[A-Z0-9]{8}$/);
+ });
+
+ test('should show validation error for empty room ID', async ({ page }) => {
+ // Try to join with empty room ID
+ await page.getByRole('button', { name: 'Join Room' }).click();
+
+ // Should show validation or remain on the page
+ // The button might be disabled or show an error
+ await expect(page.getByPlaceholder('Enter room ID')).toBeVisible();
+ });
+
+ test('should show validation error for invalid room ID format', async ({ page }) => {
+ // Enter invalid room ID (less than 8 characters)
+ await page.getByPlaceholder('Enter room ID').fill('ABC');
+ await page.getByRole('button', { name: 'Join Room' }).click();
+
+ // Should still be on room page (invalid input)
+ await expect(page.getByPlaceholder('Enter room ID')).toBeVisible();
+ });
+
+ test('should allow leaving a room', async ({ page }) => {
+ // Create a room first
+ await page.getByRole('button', { name: 'Create New Room' }).click();
+ await expect(page.locator('text=Connected Room')).toBeVisible({ timeout: 10000 });
+
+ // Find and click leave room button
+ await page.getByRole('button', { name: 'Leave Room' }).click();
+
+ // Should return to room selection
+ await expect(page.getByRole('button', { name: 'Create New Room' })).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Join Room' })).toBeVisible();
+ });
+
+ test('should display room ID correctly', async ({ page }) => {
+ // Create a room
+ await page.getByRole('button', { name: 'Create New Room' }).click();
+ await expect(page.locator('text=Connected Room')).toBeVisible({ timeout: 10000 });
+
+ // Get the room ID
+ const roomId = await page.locator('.font-mono.text-3xl').textContent();
+
+ // Room ID should be visible and copyable
+ expect(roomId).toBeTruthy();
+ expect(roomId).toHaveLength(8);
+ });
+
+ test('should handle page refresh in a room', async ({ page }) => {
+ // Create a room
+ await page.getByRole('button', { name: 'Create New Room' }).click();
+ await expect(page.locator('text=Connected Room')).toBeVisible({ timeout: 10000 });
+
+ const roomIdBefore = await page.locator('.font-mono.text-3xl').textContent();
+
+ // Refresh the page
+ await page.reload();
+
+ // Should still be in the same room after reload
+ await expect(page.locator('text=Connected Room')).toBeVisible({ timeout: 10000 });
+ const roomIdAfter = await page.locator('.font-mono.text-3xl').textContent();
+
+ expect(roomIdAfter).toBe(roomIdBefore);
+ });
+
+ test('should show user presence in room', async ({ page }) => {
+ // Create a room
+ await page.getByRole('button', { name: 'Create New Room' }).click();
+ await expect(page.locator('text=Connected Room')).toBeVisible({ timeout: 10000 });
+
+ // Get current user info
+ await page.getByRole('button', { name: 'User avatar' }).click();
+ const username = await page.locator('.text-sm.font-medium').textContent();
+ await page.keyboard.press('Escape');
+
+ // User should appear in the peer list
+ await expect(page.locator(`text=${username}`)).toBeVisible();
+ });
+
+});
diff --git a/tests/setup.ts b/tests/setup.ts
new file mode 100644
index 0000000..92e16f4
--- /dev/null
+++ b/tests/setup.ts
@@ -0,0 +1,46 @@
+import { expect, afterEach, beforeAll, vi } from 'vitest'
+import { cleanup } from '@testing-library/react'
+import * as matchers from '@testing-library/jest-dom/matchers'
+import { setupBlobPolyfill } from './utils/polyfills'
+
+// Extend Vitest's expect with jest-dom matchers
+expect.extend(matchers)
+
+// Cleanup after each test
+afterEach(() => {
+ cleanup()
+})
+
+// Mock environment variables for tests
+process.env.NEXT_PUBLIC_SUPABASE_URL = 'https://test.supabase.co'
+process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = 'test-anon-key'
+
+// Setup polyfills for jsdom
+beforeAll(() => {
+ setupBlobPolyfill()
+})
+
+// Mock Next.js router
+vi.mock('next/navigation', () => ({
+ useRouter: () => ({
+ push: vi.fn(),
+ replace: vi.fn(),
+ prefetch: vi.fn(),
+ back: vi.fn(),
+ }),
+ usePathname: () => '/',
+ useSearchParams: () => new URLSearchParams(),
+}))
+
+
+// Mock Next.js router
+vi.mock('next/navigation', () => ({
+ useRouter: () => ({
+ push: vi.fn(),
+ replace: vi.fn(),
+ prefetch: vi.fn(),
+ back: vi.fn(),
+ }),
+ usePathname: () => '/',
+ useSearchParams: () => new URLSearchParams(),
+}))
diff --git a/tests/transfer.spec.ts b/tests/transfer.spec.ts
new file mode 100644
index 0000000..113773e
--- /dev/null
+++ b/tests/transfer.spec.ts
@@ -0,0 +1,73 @@
+import { test, expect } from '@playwright/test';
+import path from 'path';
+import { USER_1_STATE, USER_2_STATE } from '../playwright.config';
+
+// This test uses two authenticated browser contexts.
+
+test.describe('File Transfer (P2P)', () => {
+
+ const testFilePath = path.join(__dirname, 'fixtures/test-file.txt');
+
+ test('should send and receive a file between two peers', async ({ browser }) => {
+ // --- Setup Sender (User 1) ---
+ const senderContext = await browser.newContext({ storageState: USER_1_STATE });
+ const senderPage = await senderContext.newPage();
+
+ // --- Setup Receiver (User 2) ---
+ const receiverContext = await browser.newContext({ storageState: USER_2_STATE });
+ const receiverPage = await receiverContext.newPage();
+
+ try {
+ // 1. (Sender) Go to room and create
+ await senderPage.goto('/room');
+ await senderPage.getByRole('button', { name: 'Create New Room' }).click();
+ await expect(senderPage.locator('text=Connected Room')).toBeVisible({ timeout: 10000 });
+
+ // 2. (Sender) Get Room ID and Receiver's username
+ const roomId = await senderPage.locator('.font-mono.text-3xl').textContent();
+ expect(roomId).toBeTruthy();
+
+ await senderPage.getByRole('button', { name: 'User avatar' }).click();
+ const senderUsername = await senderPage.locator('.text-sm.font-medium').textContent();
+ await senderPage.keyboard.press('Escape');
+
+ await receiverPage.getByRole('button', { name: 'User avatar' }).click();
+ const receiverUsername = await receiverPage.locator('.text-sm.font-medium').textContent();
+ await receiverPage.keyboard.press('Escape');
+
+ // 3. (Receiver) Go to room and join
+ await receiverPage.goto('/room');
+ await receiverPage.getByPlaceholder('Enter room ID').fill(roomId!);
+ await receiverPage.getByRole('button', { name: 'Join Room' }).click();
+
+ // 4. Wait for peers to connect (WebRTC can take a moment)
+ // Sender asserts Receiver is 'Live'
+ await expect(senderPage.locator(`div:has-text("${receiverUsername}")`).getByText('Live')).toBeVisible({ timeout: 20000 });
+ // Receiver asserts Sender is 'Live'
+ await expect(receiverPage.locator(`div:has-text("${senderUsername}")`).getByText('Live')).toBeVisible({ timeout: 20000 });
+
+ // 5. (Sender) Select file, recipient, and send
+ await senderPage.locator('input[type="file"]').setInputFiles(testFilePath);
+ await expect(senderPage.locator('text=1 file selected')).toBeVisible();
+
+ await senderPage.locator('button[role="combobox"]').click();
+ await senderPage.getByRole('option', { name: receiverUsername! }).click();
+ await senderPage.getByRole('button', { name: 'Send' }).click();
+
+ // 6. (Sender) Assert send is complete
+ await expect(senderPage.locator('text=File sent')).toBeVisible({ timeout: 15000 });
+ await expect(senderPage.locator('div[role="alert"] div:has-text("100%")')).toBeVisible();
+
+ // 7. (Receiver) Assert file is received
+ // The app shows a toast on success
+ await expect(receiverPage.locator('text=File received')).toBeVisible({ timeout: 15000 });
+ await expect(receiverPage.locator('div[role="alert"] div:has-text("100%")')).toBeVisible();
+ await expect(receiverPage.locator('div[role="alert"] div:has-text("test-file.txt")')).toBeVisible();
+
+ } finally {
+ // Clean up contexts
+ await senderContext.close();
+ await receiverContext.close();
+ }
+ });
+});
\ No newline at end of file
diff --git a/tests/ui.spec.ts b/tests/ui.spec.ts
new file mode 100644
index 0000000..cf7b666
--- /dev/null
+++ b/tests/ui.spec.ts
@@ -0,0 +1,133 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('User Interface', () => {
+
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/room');
+ });
+
+ test('should toggle dark mode', async ({ page }) => {
+ // Get the theme toggle button
+ const themeToggle = page.getByLabel('Toggle dark mode');
+ await expect(themeToggle).toBeVisible();
+
+ // Get initial theme (check HTML element)
+ const htmlElement = page.locator('html');
+ const initialClass = await htmlElement.getAttribute('class');
+ const isDarkInitially = initialClass?.includes('dark');
+
+ // Toggle theme
+ await themeToggle.click();
+
+ // Wait a moment for theme to update
+ await page.waitForTimeout(500);
+
+ // Check theme has changed
+ const newClass = await htmlElement.getAttribute('class');
+ const isDarkAfter = newClass?.includes('dark');
+
+ expect(isDarkAfter).not.toBe(isDarkInitially);
+
+ // Toggle back
+ await themeToggle.click();
+ await page.waitForTimeout(500);
+
+ // Should return to original state
+ const finalClass = await htmlElement.getAttribute('class');
+ const isDarkFinal = finalClass?.includes('dark');
+ expect(isDarkFinal).toBe(isDarkInitially);
+ });
+
+ test('should persist theme preference across page reloads', async ({ page }) => {
+ const themeToggle = page.getByLabel('Toggle dark mode');
+ const htmlElement = page.locator('html');
+
+ // Toggle to a specific theme
+ await themeToggle.click();
+ await page.waitForTimeout(500);
+
+ const themeAfterToggle = await htmlElement.getAttribute('class');
+
+ // Reload page
+ await page.reload();
+
+ // Wait for page to fully load
+ await expect(themeToggle).toBeVisible();
+ await page.waitForTimeout(500);
+
+ // Theme should be preserved
+ const themeAfterReload = await htmlElement.getAttribute('class');
+ expect(themeAfterReload).toBe(themeAfterToggle);
+ });
+
+ test('should navigate between pages', async ({ page }) => {
+ // Should be on room page
+ await expect(page).toHaveURL('/room');
+
+ // Navigate to profile
+ await page.getByRole('button', { name: 'User avatar' }).click();
+ await page.getByRole('menuitem', { name: 'Profile' }).click();
+
+ // Should be on profile page
+ await expect(page).toHaveURL('/profile');
+ await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible();
+
+ // Navigate back to room
+ await page.goto('/room');
+ await expect(page).toHaveURL('/room');
+ });
+
+ test('should display header on all pages', async ({ page }) => {
+ // Room page
+ await expect(page.locator('header')).toBeVisible();
+
+ // Profile page
+ await page.goto('/profile');
+ await expect(page.locator('header')).toBeVisible();
+ });
+
+ test('should display footer on main page', async ({ page }) => {
+ await page.goto('/');
+
+ // Footer should be visible
+ const footer = page.locator('footer');
+ await expect(footer).toBeVisible();
+ });
+
+ test('should show user dropdown menu', async ({ page }) => {
+ // Click on user avatar
+ await page.getByRole('button', { name: 'User avatar' }).click();
+
+ // Menu items should be visible
+ await expect(page.getByRole('menuitem', { name: 'Profile' })).toBeVisible();
+ await expect(page.getByRole('menuitem', { name: 'Sign Out' })).toBeVisible();
+
+ // Close menu
+ await page.keyboard.press('Escape');
+
+ // Menu should be hidden
+ await expect(page.getByRole('menuitem', { name: 'Profile' })).not.toBeVisible();
+ });
+
+ test('should handle responsive navigation', async ({ page, viewport }) => {
+ // Test on desktop size
+ await page.setViewportSize({ width: 1280, height: 720 });
+ await expect(page.getByLabel('Toggle dark mode')).toBeVisible();
+
+ // Test on mobile size
+ await page.setViewportSize({ width: 375, height: 667 });
+ // UI should still be accessible
+ await expect(page.getByRole('button', { name: 'User avatar' })).toBeVisible();
+ });
+
+ test('should display loading states', async ({ page }) => {
+ // Create a room
+ await page.getByRole('button', { name: 'Create New Room' }).click();
+
+ // Should show some indication of connection
+ // This might be a spinner or "Connecting..." text
+ // Wait for successful connection
+ await expect(page.locator('text=Connected Room')).toBeVisible({ timeout: 10000 });
+ });
+
+});
diff --git a/tests/unit/components/footer.test.tsx b/tests/unit/components/footer.test.tsx
new file mode 100644
index 0000000..7171d50
--- /dev/null
+++ b/tests/unit/components/footer.test.tsx
@@ -0,0 +1,71 @@
+import { describe, it, expect } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import Footer from '@/components/footer'
+
+// Example component test for the Footer component
+// This demonstrates how to test React components
+describe('Footer Component', () => {
+ it('should render the footer', () => {
+ render()
+
+ // Check that footer element exists
+ const footer = screen.getByRole('contentinfo')
+ expect(footer).toBeInTheDocument()
+ })
+
+ it('should display the WebDrop brand name', () => {
+ render()
+
+ expect(screen.getByText('WebDrop')).toBeInTheDocument()
+ })
+
+ it('should display the copyright year', () => {
+ render()
+
+ const currentYear = new Date().getFullYear()
+ expect(screen.getByText(new RegExp(`ยฉ ${currentYear}`))).toBeInTheDocument()
+ })
+
+ it('should have navigation links', () => {
+ render()
+
+ // Check for Home link
+ const homeLink = screen.getByRole('link', { name: 'Home' })
+ expect(homeLink).toBeInTheDocument()
+ expect(homeLink).toHaveAttribute('href', '/')
+
+ // Check for Profile link
+ const profileLink = screen.getByRole('link', { name: 'Profile' })
+ expect(profileLink).toBeInTheDocument()
+ expect(profileLink).toHaveAttribute('href', '/profile')
+ })
+
+ it('should have GitHub links with correct attributes', () => {
+ render()
+
+ // Get all links that contain "GitHub"
+ const githubLinks = screen.getAllByRole('link', { name: /GitHub/i })
+
+ // Should have at least one GitHub link
+ expect(githubLinks.length).toBeGreaterThan(0)
+
+ // Check first GitHub link attributes
+ expect(githubLinks[0]).toHaveAttribute('target', '_blank')
+ expect(githubLinks[0]).toHaveAttribute('rel', 'noopener noreferrer')
+ })
+
+ it('should display the tagline', () => {
+ render()
+
+ expect(screen.getByText(/Secure peer-to-peer file sharing/i)).toBeInTheDocument()
+ expect(screen.getByText(/WebRTC/i)).toBeInTheDocument()
+ })
+
+ it('should have proper structure with sections', () => {
+ render()
+
+ // Check for section headings
+ expect(screen.getByText('Quick Links')).toBeInTheDocument()
+ expect(screen.getByText('Resources')).toBeInTheDocument()
+ })
+})
diff --git a/tests/unit/hooks/use-room.test.tsx b/tests/unit/hooks/use-room.test.tsx
new file mode 100644
index 0000000..8b97307
--- /dev/null
+++ b/tests/unit/hooks/use-room.test.tsx
@@ -0,0 +1,54 @@
+import { describe, it, expect } from 'vitest'
+
+/**
+ * Example hook test placeholder for use-room
+ *
+ * This demonstrates the structure for testing custom React hooks.
+ *
+ * Testing complex hooks like useRoom that interact with:
+ * - Supabase (database, realtime)
+ * - WebRTC connections
+ * - Complex state management
+ *
+ * Requires extensive mocking and is often better covered by integration tests.
+ *
+ * For useRoom specifically:
+ * - E2E tests in tests/room.spec.ts cover room creation and joining
+ * - E2E tests in tests/transfer.spec.ts cover P2P connections
+ * - These provide excellent real-world coverage
+ *
+ * If you need to add unit tests for hooks, consider:
+ * 1. Testing simpler, pure logic functions extracted from hooks
+ * 2. Using MSW (Mock Service Worker) for API mocking
+ * 3. Creating mock factories for Supabase responses
+ * 4. Using @testing-library/react's renderHook utility
+ *
+ * Example hook test structure:
+ *
+ * import { renderHook, waitFor } from '@testing-library/react'
+ *
+ * describe('useMyHook', () => {
+ * it('should return initial state', () => {
+ * const { result } = renderHook(() => useMyHook())
+ * expect(result.current.value).toBe(initialValue)
+ * })
+ *
+ * it('should update state when action is called', async () => {
+ * const { result } = renderHook(() => useMyHook())
+ * act(() => {
+ * result.current.updateValue(newValue)
+ * })
+ * await waitFor(() => {
+ * expect(result.current.value).toBe(newValue)
+ * })
+ * })
+ * })
+ */
+
+describe('useRoom Hook Example', () => {
+ it('should have E2E coverage in room.spec.ts', () => {
+ // This is a placeholder test to document that useRoom is tested via E2E tests
+ expect(true).toBe(true)
+ })
+})
+
diff --git a/tests/unit/lib/utils.test.ts b/tests/unit/lib/utils.test.ts
new file mode 100644
index 0000000..1801054
--- /dev/null
+++ b/tests/unit/lib/utils.test.ts
@@ -0,0 +1,39 @@
+import { describe, it, expect } from 'vitest'
+import { cn } from '@/lib/utils'
+
+describe('cn utility', () => {
+ it('should merge class names correctly', () => {
+ const result = cn('foo', 'bar')
+ expect(result).toBe('foo bar')
+ })
+
+ it('should handle conditional classes', () => {
+ const result = cn('foo', false && 'bar', 'baz')
+ expect(result).toBe('foo baz')
+ })
+
+ it('should merge Tailwind classes correctly', () => {
+ const result = cn('px-2 py-1', 'px-4')
+ expect(result).toBe('py-1 px-4')
+ })
+
+ it('should handle undefined and null values', () => {
+ const result = cn('foo', undefined, null, 'bar')
+ expect(result).toBe('foo bar')
+ })
+
+ it('should handle arrays', () => {
+ const result = cn(['foo', 'bar'], 'baz')
+ expect(result).toBe('foo bar baz')
+ })
+
+ it('should handle objects', () => {
+ const result = cn({ foo: true, bar: false, baz: true })
+ expect(result).toBe('foo baz')
+ })
+
+ it('should handle empty input', () => {
+ const result = cn()
+ expect(result).toBe('')
+ })
+})
diff --git a/tests/unit/lib/webrtc/file-transfer.test.ts b/tests/unit/lib/webrtc/file-transfer.test.ts
new file mode 100644
index 0000000..93693fa
--- /dev/null
+++ b/tests/unit/lib/webrtc/file-transfer.test.ts
@@ -0,0 +1,227 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { FileTransferManager, type FileMetadata, type FileChunk } from '@/lib/webrtc/file-transfer'
+
+// Test constants
+const NONEXISTENT_FILE_ID = 'nonexistent'
+
+// Helper to convert string to ArrayBuffer for testing
+function stringToArrayBuffer(str: string): ArrayBuffer {
+ const encoder = new TextEncoder()
+ return encoder.encode(str).buffer
+}
+
+describe('FileTransferManager', () => {
+ let manager: FileTransferManager
+
+ beforeEach(() => {
+ manager = new FileTransferManager()
+ })
+
+ describe('sendFile', () => {
+ it('should send file metadata and chunks', async () => {
+ const sendData = vi.fn()
+ const onProgress = vi.fn()
+ const fileContent = 'Hello, World!'
+ const blob = new Blob([fileContent], { type: 'text/plain' })
+ const file = new File([blob], 'test.txt', { type: 'text/plain' })
+
+ await manager.sendFile(file, 'peer123', sendData, onProgress)
+
+ // Should send metadata first
+ expect(sendData).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'file-metadata',
+ metadata: expect.objectContaining({
+ name: 'test.txt',
+ size: fileContent.length,
+ type: 'text/plain',
+ }),
+ peerId: 'peer123',
+ })
+ )
+
+ // Should send chunks
+ expect(sendData).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'file-chunk',
+ peerId: 'peer123',
+ })
+ )
+
+ // Should send completion message
+ expect(sendData).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'file-complete',
+ peerId: 'peer123',
+ })
+ )
+
+ // Should report progress
+ expect(onProgress).toHaveBeenCalled()
+ expect(onProgress).toHaveBeenLastCalledWith(100)
+ })
+
+ it('should handle large files with multiple chunks', async () => {
+ const sendData = vi.fn()
+ const onProgress = vi.fn()
+ // Create a file larger than chunk size (16KB)
+ const largeContent = 'x'.repeat(20000)
+ const blob = new Blob([largeContent], { type: 'text/plain' })
+ const file = new File([blob], 'large.txt', { type: 'text/plain' })
+
+ await manager.sendFile(file, 'peer123', sendData, onProgress)
+
+ // Count chunk messages
+ const chunkCalls = sendData.mock.calls.filter(
+ call => call[0].type === 'file-chunk'
+ )
+ expect(chunkCalls.length).toBeGreaterThan(1)
+
+ // Progress should be called multiple times
+ expect(onProgress.mock.calls.length).toBeGreaterThan(1)
+ })
+ })
+
+ describe('receiveMetadata', () => {
+ it('should store metadata for incoming transfer', () => {
+ const metadata: FileMetadata = {
+ id: 'file123',
+ name: 'test.txt',
+ size: 100,
+ type: 'text/plain',
+ }
+
+ manager.receiveMetadata(metadata)
+
+ expect(manager.getMetadata('file123')).toEqual(metadata)
+ })
+ })
+
+ describe('receiveChunk', () => {
+ it('should accumulate chunks and report progress', () => {
+ const metadata: FileMetadata = {
+ id: 'file123',
+ name: 'test.txt',
+ size: 100,
+ type: 'text/plain',
+ }
+ manager.receiveMetadata(metadata)
+
+ const onProgress = vi.fn()
+ const chunk: FileChunk = {
+ id: 'file123',
+ index: 0,
+ data: stringToArrayBuffer('Hello'),
+ total: 2,
+ }
+
+ manager.receiveChunk(chunk, onProgress)
+
+ expect(onProgress).toHaveBeenCalledWith('file123', 50)
+ })
+
+ it('should handle chunk without metadata gracefully', () => {
+ const onProgress = vi.fn()
+ const chunk: FileChunk = {
+ id: NONEXISTENT_FILE_ID,
+ index: 0,
+ data: new ArrayBuffer(10),
+ total: 1,
+ }
+
+ expect(() => {
+ manager.receiveChunk(chunk, onProgress)
+ }).not.toThrow()
+
+ expect(onProgress).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('completeTransfer', () => {
+ it('should combine chunks and return a blob', () => {
+ const metadata: FileMetadata = {
+ id: 'file123',
+ name: 'test.txt',
+ size: 10,
+ type: 'text/plain',
+ }
+ manager.receiveMetadata(metadata)
+
+ const chunk1: FileChunk = {
+ id: 'file123',
+ index: 0,
+ data: stringToArrayBuffer('Hello'),
+ total: 2,
+ }
+ const chunk2: FileChunk = {
+ id: 'file123',
+ index: 1,
+ data: stringToArrayBuffer(' World'),
+ total: 2,
+ }
+
+ manager.receiveChunk(chunk1, vi.fn())
+ manager.receiveChunk(chunk2, vi.fn())
+
+ const blob = manager.completeTransfer('file123')
+
+ expect(blob).toBeInstanceOf(Blob)
+ expect(blob?.type).toBe('text/plain')
+ expect(blob?.size).toBeGreaterThan(0)
+
+ // Should clean up after completion
+ expect(manager.getMetadata('file123')).toBeUndefined()
+ })
+
+ it('should return null for nonexistent transfer', () => {
+ const blob = manager.completeTransfer(NONEXISTENT_FILE_ID)
+ expect(blob).toBeNull()
+ })
+ })
+
+ describe('cancelTransfer', () => {
+ it('should remove pending transfer', () => {
+ const metadata: FileMetadata = {
+ id: 'file123',
+ name: 'test.txt',
+ size: 100,
+ type: 'text/plain',
+ }
+ manager.receiveMetadata(metadata)
+
+ expect(manager.getMetadata('file123')).toBeDefined()
+
+ manager.cancelTransfer('file123')
+
+ expect(manager.getMetadata('file123')).toBeUndefined()
+ })
+ })
+
+ describe('clearPendingTransfers', () => {
+ it('should clear all pending transfers', () => {
+ const metadata1: FileMetadata = {
+ id: 'file1',
+ name: 'test1.txt',
+ size: 100,
+ type: 'text/plain',
+ }
+ const metadata2: FileMetadata = {
+ id: 'file2',
+ name: 'test2.txt',
+ size: 200,
+ type: 'text/plain',
+ }
+
+ manager.receiveMetadata(metadata1)
+ manager.receiveMetadata(metadata2)
+
+ expect(manager.getMetadata('file1')).toBeDefined()
+ expect(manager.getMetadata('file2')).toBeDefined()
+
+ manager.clearPendingTransfers()
+
+ expect(manager.getMetadata('file1')).toBeUndefined()
+ expect(manager.getMetadata('file2')).toBeUndefined()
+ })
+ })
+})
diff --git a/tests/utils/polyfills.ts b/tests/utils/polyfills.ts
new file mode 100644
index 0000000..45cada0
--- /dev/null
+++ b/tests/utils/polyfills.ts
@@ -0,0 +1,24 @@
+/**
+ * Polyfills for jsdom environment
+ *
+ * These polyfills add missing browser APIs to the jsdom test environment.
+ */
+
+/**
+ * Polyfill for Blob.arrayBuffer()
+ *
+ * jsdom doesn't fully support Blob.arrayBuffer() which is needed by the
+ * FileTransferManager when reading file chunks.
+ */
+export function setupBlobPolyfill() {
+ if (!Blob.prototype.arrayBuffer) {
+ Blob.prototype.arrayBuffer = async function () {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader()
+ reader.onload = () => resolve(reader.result as ArrayBuffer)
+ reader.onerror = () => reject(reader.error)
+ reader.readAsArrayBuffer(this)
+ })
+ }
+ }
+}
diff --git a/tests/utils/testmail.helper.ts b/tests/utils/testmail.helper.ts
new file mode 100644
index 0000000..e02b631
--- /dev/null
+++ b/tests/utils/testmail.helper.ts
@@ -0,0 +1,107 @@
+import { GraphQLClient } from '@testmail.app/graphql-request';
+import { randomUUID } from 'crypto';
+
+// Create a GraphQL client for testmail.app
+const testmailClient = new GraphQLClient('https://api.testmail.app/api/graphql', {
+ headers: {
+ Authorization: `Bearer ${process.env.TESTMAIL_API_KEY}`,
+ },
+});
+
+const TESTMAIL_NAMESPACE = process.env.TESTMAIL_NAMESPACE;
+if (!TESTMAIL_NAMESPACE) {
+ // Soft warning so local devs know why tests may fail
+ // eslint-disable-next-line no-console
+ console.warn('TESTMAIL_NAMESPACE is not set. Email-based tests will fail.');
+}
+if (!process.env.TESTMAIL_API_KEY) {
+ // eslint-disable-next-line no-console
+ console.warn('TESTMAIL_API_KEY is not set. Email-based tests will fail.');
+}
+
+/**
+ * Generates a unique email address for testing.
+ * @returns An object with the tag and the full email address.
+ */
+export function generateTestEmail() {
+ const tag = `user-${randomUUID()}`;
+ const email = `${TESTMAIL_NAMESPACE}.${tag}@inbox.testmail.app`;
+ return { tag, email };
+}
+
+/**
+ * Polls testmail.app until an email with the specified tag is found.
+ * @param tag The unique tag for the email.
+ * @param timeout Max time to wait in ms.
+ * @returns The email object from testmail.app.
+ */
+export async function waitForEmail(tag: string, timeout = 30000): Promise {
+ const startTime = Date.now();
+
+ // GraphQL query to fetch latest emails for a namespace filtered by tag
+ // Note: Schema names may evolve; we access defensively below.
+ const QUERY = /* GraphQL */ `
+ query FetchEmails($namespace: String!, $tag: String!) {
+ inbox(namespace: $namespace) {
+ messages(tag: $tag, limit: 5, order: DESC) {
+ id
+ subject
+ from
+ to
+ html
+ text
+ createdAt
+ }
+ }
+ }
+ `;
+
+ while (Date.now() - startTime < timeout) {
+ try {
+ const data: any = await testmailClient.request(QUERY, {
+ namespace: TESTMAIL_NAMESPACE,
+ tag,
+ });
+
+ const emails: any[] =
+ data?.inbox?.messages ||
+ data?.inbox?.emails ||
+ data?.emails ||
+ [];
+
+ if (Array.isArray(emails) && emails.length > 0) {
+ return emails[0];
+ }
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.warn('testmail.app API error, retrying...', error instanceof Error ? error.message : error);
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2s before retrying
+ }
+ throw new Error(`Email with tag "${tag}" not found within ${timeout}ms.`);
+}
+
+/**
+ * Extracts the Supabase confirmation link from an email body.
+ * @param emailBody The HTML or text content of the email.
+ * @returns The confirmation URL.
+ */
+export function extractConfirmationLink(emailBody: string): string {
+ // Try to match an anchor href first
+ let regex = /href="(https?:\/\/[^"]*\/auth\/callback[^"]*)"/i;
+ let match = emailBody.match(regex);
+
+ if (!match) {
+ // Fallback: match plain URL in the text
+ regex = /(https?:\/\/[^\s"']*\/auth\/callback[^\s"']*)/i;
+ match = emailBody.match(regex);
+ }
+
+ if (match && match[1]) {
+ // The link might be HTML-escaped
+ return match[1].replace(/&/g, '&');
+ }
+
+ throw new Error('Could not extract confirmation link from email body.');
+}
\ No newline at end of file
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..8f13867
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,38 @@
+import { defineConfig } from 'vitest/config'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: 'jsdom',
+ globals: true,
+ setupFiles: ['./tests/setup.ts'],
+ include: ['tests/unit/**/*.{test,spec}.{js,ts,jsx,tsx}'],
+ exclude: [
+ 'node_modules/',
+ 'tests/*.spec.ts', // Exclude Playwright tests
+ 'tests/fixtures/**',
+ 'tests/utils/**',
+ '.next/',
+ 'playwright/',
+ ],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json', 'html'],
+ exclude: [
+ 'node_modules/',
+ 'tests/',
+ '*.config.{ts,js}',
+ '.next/',
+ 'playwright/',
+ 'components/ui/**', // shadcn/ui components
+ ],
+ },
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './'),
+ },
+ },
+})