diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7ea23d9 --- /dev/null +++ b/.env.example @@ -0,0 +1,122 @@ +# Chess Master Database - Environment Variables +# Copy this file to .env.local and fill in your values + +# ====================== +# Application +# ====================== +VITE_APP_NAME=Chess Master Database +VITE_APP_VERSION=2.1.0 +VITE_APP_URL=http://localhost:5173 + +# ====================== +# Sentry (Error Tracking) +# ====================== +# Get your DSN from: https://sentry.io/ +VITE_SENTRY_DSN=your_sentry_dsn_here +VITE_SENTRY_ENVIRONMENT=development +# Sentry traces sample rate (0.0 to 1.0) +VITE_SENTRY_TRACES_SAMPLE_RATE=0.1 + +# ====================== +# Lichess API +# ====================== +# API Base URL (usually no need to change) +VITE_LICHESS_API_URL=https://lichess.org/api +# Optional: Lichess API token for authenticated requests +# Get a token from: https://lichess.org/account/oauth/token +VITE_LICHESS_API_TOKEN= + +# ====================== +# Chess Engine +# ====================== +# Stockfish configuration +VITE_STOCKFISH_MIN_DEPTH=10 +VITE_STOCKFISH_MAX_DEPTH=18 +VITE_STOCKFISH_DEFAULT_DEPTH=15 +VITE_STOCKFISH_MULTI_PV=3 + +# ====================== +# Feature Flags +# ====================== +VITE_ENABLE_AI_HINTS=true +VITE_ENABLE_CLOUD_SYNC=false +VITE_ENABLE_MULTIPLAYER=false +VITE_ENABLE_VOICE_COMMANDS=false +VITE_ENABLE_ADVANCED_STATS=true + +# ====================== +# Analytics (Optional) +# ====================== +# Google Analytics ID +VITE_GA_MEASUREMENT_ID= +# Plausible Analytics +VITE_PLAUSIBLE_DOMAIN= +# Mixpanel +VITE_MIXPANEL_TOKEN= + +# ====================== +# Development +# ====================== +# Enable development tools +VITE_ENABLE_DEV_TOOLS=true +# Enable verbose logging +VITE_ENABLE_LOGGING=false +# Mock API responses for development +VITE_MOCK_API=false + +# ====================== +# Cache Configuration +# ====================== +# Cache duration in milliseconds +VITE_CACHE_DURATION=300000 +# localStorage cache expiry for analysis (7 days) +VITE_ANALYSIS_CACHE_EXPIRY=604800000 + +# ====================== +# Rate Limiting +# ====================== +# Lichess API rate limit delay (ms) +VITE_LICHESS_RATE_LIMIT_DELAY=1000 +# Max games per Lichess request +VITE_LICHESS_MAX_GAMES=100 + +# ====================== +# UI Configuration +# ====================== +# Default theme (light, dark, system) +VITE_DEFAULT_THEME=system +# Default language (fr, en, es) +VITE_DEFAULT_LANGUAGE=fr +# Default board theme +VITE_DEFAULT_BOARD_THEME=classic + +# ====================== +# PWA Configuration +# ====================== +# Service Worker cache name +VITE_SW_CACHE_NAME=chess-master-v1 +# Enable offline mode +VITE_ENABLE_OFFLINE_MODE=true + +# ====================== +# Database Configuration +# ====================== +# For future cloud sync feature +VITE_DB_URL= +VITE_DB_API_KEY= + +# ====================== +# Security +# ====================== +# CORS allowed origins (comma-separated) +VITE_CORS_ORIGINS=http://localhost:5173,http://localhost:3000 +# Enable HTTPS in production +VITE_FORCE_HTTPS=false + +# ====================== +# External Services +# ====================== +# YouTube API Key (for video library) +VITE_YOUTUBE_API_KEY= +# Chess.com API +VITE_CHESSCOM_API_URL=https://api.chess.com/pub diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..46868b0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,154 @@ +name: CI/CD + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + - name: Check formatting + run: npm run format:check + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test -- --run + + - name: Generate coverage report + run: npm run test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage/coverage-final.json + flags: unittests + name: codecov-umbrella + + e2e: + name: E2E Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps + + - name: Run E2E tests + run: npm run test:e2e + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v3 + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + + build: + name: Build + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist/ + retention-days: 7 + + type-check: + name: Type Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run TypeScript compiler + run: npx tsc --noEmit + + security: + name: Security Audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Run npm audit + run: npm audit --audit-level=moderate + continue-on-error: true + + - name: Run Snyk security scan + uses: snyk/actions/node@master + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --severity-threshold=high diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..2312dc5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..7c5f10a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,26 @@ +# Dependencies +node_modules + +# Build outputs +dist +build +.vite + +# Coverage +coverage + +# PWA +public/sw.js +public/workbox-*.js + +# Generated files +*.min.js +*.min.css + +# Package files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Logs +*.log diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..49fb29b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "avoid", + "endOfLine": "lf" +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..0faea00 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,46 @@ +{ + "recommendations": [ + // Essential + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "ms-playwright.playwright", + + // TypeScript/JavaScript + "usernamehw.errorlens", + "streetsidesoftware.code-spell-checker", + "christian-kohler.path-intellisense", + "christian-kohler.npm-intellisense", + + // React + "dsznajder.es7-react-js-snippets", + "burkeholland.simple-react-snippets", + + // Tailwind CSS + "bradlc.vscode-tailwindcss", + + // Git + "eamodio.gitlens", + "donjayamanne.githistory", + + // Testing + "vitest.explorer", + + // Utility + "aaron-bond.better-comments", + "wix.vscode-import-cost", + "formulahendry.auto-rename-tag", + "formulahendry.auto-close-tag", + "naumovs.color-highlight", + "ms-vsliveshare.vsliveshare", + + // Documentation + "yzhang.markdown-all-in-one", + "davidanson.vscode-markdownlint", + + // Productivity + "usernamehw.errorlens", + "gruntfuggly.todo-tree", + "wayou.vscode-todo-highlight" + ], + "unwantedRecommendations": [] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..52ee3dd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,135 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Prettier configuration for consistent code formatting +- Husky and lint-staged for pre-commit hooks +- Vitest for unit testing with comprehensive test coverage +- React Testing Library for component testing +- Unit tests for all utility functions (ELO, material evaluation, captured pieces, tournament) +- Global ErrorBoundary component for graceful error handling +- Constants file (`constants.ts`) for centralized configuration values +- Custom hooks for better code reusability: + - `useKeyboardShortcuts` - Manage keyboard shortcuts + - `useLocalStorage` - Sync state with localStorage + - `useDebounce` - Debounce values for performance + - `useMediaQuery` - Responsive design utilities +- Keyboard shortcuts modal component +- Skeleton loading components for better UX during data fetching +- JSON export functionality for games +- CSV export/import functionality for games +- Excel (TSV) export functionality for games +- FEN import/export utilities with validation +- FEN clipboard operations +- Position comparison and description utilities +- GitHub Actions CI/CD workflow: + - Automated linting + - Unit and E2E testing + - Type checking + - Security audits + - Build verification +- Contributing guidelines (CONTRIBUTING.md) +- This changelog (CHANGELOG.md) + +### Changed + +- Improved TypeScript type safety throughout the codebase +- Enhanced error messages with user-friendly descriptions +- Better code organization with extracted custom hooks + +### Fixed + +- Improved error handling in file import operations +- Better validation for FEN and PGN inputs + +## [2.0.0] - 2024-11-14 + +### Added + +- PWA (Progressive Web App) support with offline capabilities +- Multi-language support (French, English, Spanish) +- Dark mode with system preference detection +- Stockfish 16 NNUE chess engine integration +- 13 different views and game modes: + - Games database with advanced search + - Openings encyclopedia (15+ documented openings) + - Players database with CRUD operations + - Play vs Computer (10 difficulty levels) + - Puzzle Trainer + - Daily Puzzle with streak tracking + - Puzzle Rush (timed mode) + - Puzzle Battle (competitive mode) + - Spectator Mode + - Learning Mode with structured lessons + - Video Library with YouTube integration + - Tournaments (round-robin system) + - Leaderboards +- 9 board themes (Classic, Blue, Green, Wood, Brown, Purple, Pink, Marble, Metal) +- Lichess API integration for fetching games and player data +- Game statistics with Chart.js visualizations +- PGN import/export functionality +- GIF export for animated game sequences +- Post-game analysis reports +- Notification system with customizable settings +- Achievement system +- WCAG 2.1 Level AA accessibility compliance +- Comprehensive E2E tests with Playwright +- Sentry error tracking and monitoring + +### Changed + +- Migrated to Vite for faster build times +- Updated to React 18 with modern hooks +- Improved state management with Zustand +- Enhanced UI/UX with Tailwind CSS +- Better responsive design for mobile devices + +### Fixed + +- Memory leaks in Stockfish Web Worker +- Race conditions in game loading +- Accessibility issues with keyboard navigation + +## [1.0.0] - 2023-XX-XX + +### Added + +- Initial release +- Basic chess game viewer +- Simple PGN parser +- Game database with master games +- Basic search functionality + +--- + +## Version History + +### Version Numbering + +We follow Semantic Versioning (MAJOR.MINOR.PATCH): + +- **MAJOR**: Incompatible API changes +- **MINOR**: New functionality (backwards-compatible) +- **PATCH**: Bug fixes (backwards-compatible) + +### Types of Changes + +- **Added**: New features +- **Changed**: Changes to existing functionality +- **Deprecated**: Soon-to-be removed features +- **Removed**: Removed features +- **Fixed**: Bug fixes +- **Security**: Security vulnerability fixes + +--- + +[Unreleased]: https://github.com/phuetz/ChessDatabase/compare/v2.0.0...HEAD +[2.0.0]: https://github.com/phuetz/ChessDatabase/compare/v1.0.0...v2.0.0 +[1.0.0]: https://github.com/phuetz/ChessDatabase/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dee2a85 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,326 @@ +# Contributing to Chess Master Database + +Thank you for your interest in contributing to Chess Master Database! This document provides guidelines and instructions for contributing to the project. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Workflow](#development-workflow) +- [Coding Standards](#coding-standards) +- [Testing](#testing) +- [Submitting Changes](#submitting-changes) +- [Reporting Bugs](#reporting-bugs) +- [Feature Requests](#feature-requests) + +## Code of Conduct + +This project adheres to a Code of Conduct that all contributors are expected to follow. Please be respectful and constructive in all interactions. + +## Getting Started + +### Prerequisites + +- Node.js 18+ and npm +- Git +- A code editor (VS Code recommended) + +### Setup + +1. Fork the repository on GitHub +2. Clone your fork locally: + ```bash + git clone https://github.com/YOUR_USERNAME/ChessDatabase.git + cd ChessDatabase + ``` +3. Install dependencies: + ```bash + npm install + ``` +4. Create a new branch for your feature: + ```bash + git checkout -b feature/your-feature-name + ``` + +## Development Workflow + +### Running the Development Server + +```bash +npm run dev +``` + +The application will be available at `http://localhost:5173` + +### Available Scripts + +- `npm run dev` - Start development server +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run lint` - Run ESLint +- `npm run format` - Format code with Prettier +- `npm run format:check` - Check code formatting +- `npm test` - Run unit tests +- `npm run test:ui` - Run tests with UI +- `npm run test:coverage` - Generate coverage report +- `npm run test:e2e` - Run end-to-end tests + +## Coding Standards + +### TypeScript + +- Use TypeScript for all new code +- Avoid `any` types - use proper typing +- Use interfaces for object types +- Export types that are used in multiple files + +### React + +- Use functional components with hooks +- Follow React best practices +- Use meaningful component and variable names +- Keep components focused and single-purpose +- Extract reusable logic into custom hooks + +### Code Style + +We use Prettier and ESLint to maintain consistent code style: + +- Code is automatically formatted on commit via Husky hooks +- Run `npm run format` to format all files +- Run `npm run lint` to check for linting errors + +### File Organization + +``` +src/ +├── components/ # React components +├── screens/ # Full-page screen components +├── data/ # Data files and utilities +├── hooks/ # Custom React hooks +├── store/ # Zustand state management +├── utils/ # Utility functions +├── workers/ # Web Workers (Stockfish) +└── @types/ # TypeScript type definitions +``` + +### Naming Conventions + +- **Components**: PascalCase (e.g., `ChessBoard.tsx`) +- **Hooks**: camelCase starting with 'use' (e.g., `useChessStore.ts`) +- **Utilities**: camelCase (e.g., `fenUtils.ts`) +- **Constants**: UPPER_SNAKE_CASE (e.g., `MAX_DEPTH`) +- **Interfaces/Types**: PascalCase (e.g., `ChessGame`, `FENInfo`) + +## Testing + +### Unit Tests + +Write unit tests for: + +- Utility functions +- Custom hooks +- Complex business logic + +Example: + +```typescript +import { describe, it, expect } from 'vitest'; +import { calculateNewRating } from './elo'; + +describe('elo', () => { + it('should calculate new rating correctly', () => { + expect(calculateNewRating(1500, 1500, 1)).toBe(1510); + }); +}); +``` + +### Component Tests + +Write component tests for: + +- Critical user interactions +- Complex UI components +- Integration between components + +Example: + +```typescript +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import MyComponent from './MyComponent'; + +describe('MyComponent', () => { + it('renders correctly', () => { + render(); + expect(screen.getByText('Expected Text')).toBeInTheDocument(); + }); +}); +``` + +### E2E Tests + +Run E2E tests before submitting: + +```bash +npm run test:e2e +``` + +## Submitting Changes + +### Commit Messages + +Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: + +- `feat: add new feature` +- `fix: fix bug in component` +- `docs: update documentation` +- `style: format code` +- `refactor: refactor component` +- `test: add tests` +- `chore: update dependencies` + +Examples: + +```bash +git commit -m "feat: add FEN import/export functionality" +git commit -m "fix: correct ELO calculation for draws" +git commit -m "docs: update README with new features" +``` + +### Pull Request Process + +1. Ensure all tests pass: + + ```bash + npm test + npm run test:e2e + npm run lint + ``` + +2. Update documentation if needed + +3. Push your branch to GitHub: + + ```bash + git push origin feature/your-feature-name + ``` + +4. Create a Pull Request on GitHub with: + - Clear title and description + - Reference to any related issues + - Screenshots for UI changes + - List of changes made + +5. Wait for code review and address any feedback + +6. Once approved, your PR will be merged! + +### PR Checklist + +- [ ] Code follows the project's style guidelines +- [ ] Tests added/updated and passing +- [ ] Documentation updated +- [ ] No console errors or warnings +- [ ] Tested in multiple browsers (Chrome, Firefox, Safari) +- [ ] Responsive design tested on mobile devices +- [ ] Accessibility considerations addressed + +## Reporting Bugs + +### Before Reporting + +1. Check existing issues to avoid duplicates +2. Verify the bug exists in the latest version +3. Try to reproduce the bug consistently + +### Bug Report Template + +```markdown +**Description** +A clear description of the bug. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '...' +3. See error + +**Expected Behavior** +What you expected to happen. + +**Screenshots** +If applicable, add screenshots. + +**Environment** + +- OS: [e.g., Windows 10, macOS 13] +- Browser: [e.g., Chrome 120, Firefox 121] +- Version: [e.g., 2.0.0] + +**Additional Context** +Any other relevant information. +``` + +## Feature Requests + +We welcome feature requests! Please: + +1. Check if the feature already exists or is planned +2. Provide a clear use case +3. Explain the expected behavior +4. Include mockups or examples if possible + +### Feature Request Template + +```markdown +**Feature Description** +A clear description of the feature. + +**Use Case** +Why is this feature needed? + +**Proposed Solution** +How you envision this feature working. + +**Alternatives Considered** +Other approaches you've thought about. + +**Additional Context** +Any other relevant information. +``` + +## Development Tips + +### Debugging + +- Use React DevTools for component debugging +- Use Redux DevTools for state inspection +- Check browser console for errors +- Use `console.log` or debugger statements strategically + +### Performance + +- Use `React.memo` for expensive components +- Implement virtualization for long lists +- Lazy load heavy components +- Profile with React DevTools Profiler + +### Accessibility + +- Use semantic HTML +- Include ARIA labels where needed +- Test with keyboard navigation +- Test with screen readers +- Ensure sufficient color contrast + +## Questions? + +If you have questions: + +- Check the [README](./README.md) +- Search existing issues +- Open a new issue with the 'question' label + +Thank you for contributing to Chess Master Database! 🎉 diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..0a2b7fe --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,267 @@ +# 🚀 Quick Start Guide + +Get up and running with Chess Master Database in 5 minutes! + +## Prerequisites + +Before you begin, ensure you have the following installed: + +- **Node.js** 18 or higher ([Download](https://nodejs.org/)) +- **Git** ([Download](https://git-scm.com/)) +- A code editor (we recommend [VSCode](https://code.visualstudio.com/)) + +## Installation + +### 1. Clone the Repository + +```bash +git clone https://github.com/phuetz/ChessDatabase.git +cd ChessDatabase +``` + +### 2. Install Dependencies + +```bash +npm install +``` + +This will install all required dependencies and set up Playwright for E2E testing. + +### 3. Set Up Environment Variables (Optional) + +Copy the example environment file: + +```bash +cp .env.example .env.local +``` + +Edit `.env.local` to configure optional features like Sentry error tracking, Lichess API tokens, etc. + +### 4. Start the Development Server + +```bash +npm run dev +``` + +The application will be available at **http://localhost:5173** + +That's it! You're ready to start developing! 🎉 + +## Quick Commands + +Here are the most common commands you'll use: + +### Development + +```bash +npm run dev # Start development server +npm run build # Build for production +npm run preview # Preview production build +``` + +### Testing + +```bash +npm test # Run unit tests (watch mode) +npm run test:coverage # Run tests with coverage report +npm run test:e2e # Run E2E tests with Playwright +npm run test:ui # Open Vitest UI +``` + +### Code Quality + +```bash +npm run lint # Check for linting errors +npm run lint:fix # Fix linting errors +npm run format # Format all files with Prettier +npm run type-check # Check TypeScript types +npm run validate # Run all checks (lint + types + tests) +``` + +### Analysis + +```bash +npm run build:analyze # Analyze bundle size +npm run build:prod # Production build with all checks +``` + +## Project Structure + +``` +ChessDatabase/ +├── src/ +│ ├── components/ # React components +│ ├── screens/ # Full-page screens +│ ├── data/ # Data files and utilities +│ ├── hooks/ # Custom React hooks +│ ├── store/ # Zustand state management +│ ├── utils/ # Utility functions +│ ├── workers/ # Web Workers (Stockfish) +│ └── test/ # Test setup +├── tests/e2e/ # End-to-end tests +├── .vscode/ # VSCode configuration +└── public/ # Static assets +``` + +## First Steps + +### 1. Explore the Application + +Open http://localhost:5173 and you'll see the start menu with 13 different views: + +- **Games Database** - Browse and analyze master games +- **Openings Encyclopedia** - Learn chess openings +- **Players Database** - Manage player profiles +- **Play vs Computer** - Challenge Stockfish engine +- **Puzzle Trainer** - Solve tactical puzzles +- And much more! + +### 2. Make Your First Change + +Try modifying the welcome text: + +1. Open `src/components/StartMenu.tsx` +2. Find the main heading +3. Change the text +4. Save the file - the page will hot-reload automatically! + +### 3. Run the Tests + +```bash +npm test +``` + +Watch the tests run in your terminal. Try modifying a test to see it fail, then fix it! + +## Common Tasks + +### Adding a New Component + +1. Create a new file in `src/components/`: + + ```tsx + // src/components/MyComponent.tsx + import React from 'react'; + + interface MyComponentProps { + title: string; + } + + const MyComponent: React.FC = ({ title }) => { + return
{title}
; + }; + + export default MyComponent; + ``` + +2. Import and use it: + + ```tsx + import MyComponent from './components/MyComponent'; + + // In your JSX: + ; + ``` + +### Adding a Custom Hook + +1. Create a new file in `src/hooks/`: + + ```tsx + // src/hooks/useMyHook.ts + import { useState, useEffect } from 'react'; + + export function useMyHook(initialValue: string) { + const [value, setValue] = useState(initialValue); + + useEffect(() => { + console.log('Value changed:', value); + }, [value]); + + return [value, setValue] as const; + } + ``` + +2. Use it in a component: + ```tsx + const [value, setValue] = useMyHook('initial'); + ``` + +### Writing Tests + +Create a test file next to your code: + +```tsx +// src/utils/myUtil.test.ts +import { describe, it, expect } from 'vitest'; +import { myFunction } from './myUtil'; + +describe('myFunction', () => { + it('should return correct result', () => { + expect(myFunction(1, 2)).toBe(3); + }); +}); +``` + +## VSCode Setup + +If you're using VSCode, install the recommended extensions when prompted. They provide: + +- ✅ Automatic code formatting on save +- ✅ Linting error highlights +- ✅ TypeScript intellisense +- ✅ Tailwind CSS autocomplete +- ✅ Test runner integration + +## Troubleshooting + +### Port Already in Use + +If port 5173 is already in use: + +```bash +# Edit vite.config.ts and change the port +server: { + port: 3000 // or any other port +} +``` + +### Dependencies Issues + +Clear everything and reinstall: + +```bash +npm run clean:all +npm install +``` + +### Tests Failing + +Make sure you're running the latest version: + +```bash +git pull +npm install +npm test +``` + +## Next Steps + +- 📖 Read the full [README.md](./README.md) for detailed documentation +- 🤝 Check [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines +- 📋 Review [CHANGELOG.md](./CHANGELOG.md) for recent changes +- 🐛 Report bugs or request features on [GitHub Issues](https://github.com/phuetz/ChessDatabase/issues) + +## Need Help? + +- Check existing [GitHub Issues](https://github.com/phuetz/ChessDatabase/issues) +- Read the [Contributing Guide](./CONTRIBUTING.md) +- Review the code - it's well-documented with JSDoc comments! + +## Keyboard Shortcuts + +Press `?` in the application to see all available keyboard shortcuts! + +--- + +Happy coding! ♟️ If you find this project useful, consider giving it a ⭐ on GitHub! diff --git a/README.md b/README.md index 5162529..7c384b1 100644 --- a/README.md +++ b/README.md @@ -32,36 +32,52 @@
### 🎮 Interface Principale + -*Interface principale avec navigation intuitive entre 13 vues différentes* + +_Interface principale avec navigation intuitive entre 13 vues différentes_ ### 🔍 Analyseur de Positions avec Stockfish 16 + -*Analyse en profondeur avec visualisation des menaces et suggestions de coups* + +_Analyse en profondeur avec visualisation des menaces et suggestions de coups_ ### 📚 Encyclopédie d'Ouvertures + -*Base de données complète d'ouvertures avec codes ECO et statistiques* + +_Base de données complète d'ouvertures avec codes ECO et statistiques_ ### 👥 Base de Données de Joueurs + -*Gestion complète de joueurs avec statistiques, classements et historiques* + +_Gestion complète de joueurs avec statistiques, classements et historiques_ ### 🎯 Entraînement Tactique + -*4 modes de puzzles pour améliorer vos compétences tactiques* + +_4 modes de puzzles pour améliorer vos compétences tactiques_ ### 🎨 9 Thèmes d'Échiquier + -*Classic, Blue, Green, Wood, Brown, Purple, Pink, Marble, Metal* + +_Classic, Blue, Green, Wood, Brown, Purple, Pink, Marble, Metal_ ### 🌓 Mode Sombre + -*Interface élégante avec support du mode sombre et système* + +_Interface élégante avec support du mode sombre et système_ ### 🏆 Système de Tournois + -*Organisation de tournois Round-Robin avec classements en temps réel* + +_Organisation de tournois Round-Robin avec classements en temps réel_
@@ -69,6 +85,7 @@ ## 📑 Table des Matières +- [🆕 Nouveautés v2.1](#-nouveautés-v21) - [✨ Fonctionnalités](#-fonctionnalités) - [🎯 Fonctionnalités Principales](#-fonctionnalités-principales) - [📊 Analyse Avancée](#-analyse-avancée) @@ -90,11 +107,104 @@ --- +## 🆕 Nouveautés v2.1 + +### 🛠️ Qualité de Code & DevEx + +#### ✅ Tests Automatisés + +- **Vitest** pour les tests unitaires ultra-rapides +- **React Testing Library** pour les tests de composants +- **Suite de tests complète** : + - ✓ Tests pour le calcul ELO + - ✓ Tests pour l'évaluation matérielle + - ✓ Tests pour les pièces capturées + - ✓ Tests pour le système de tournois +- **Coverage reporting** avec rapports HTML détaillés + +#### 🎨 Formatage & Qualité + +- **Prettier** pour un code formaté uniformément +- **Husky + lint-staged** pour des pre-commit hooks +- **ESLint** avec configuration stricte +- Formatage automatique à chaque commit + +#### 🔄 CI/CD + +- **GitHub Actions** workflow complet : + - ✓ Linting automatique + - ✓ Tests unitaires et E2E + - ✓ Vérification TypeScript + - ✓ Audits de sécurité + - ✓ Build de production +- **Codecov** pour le suivi de la couverture de code + +### 📦 Nouvelles Fonctionnalités + +#### 📤 Export Avancé + +- **Export JSON** pour backup et analyse +- **Export CSV** pour Excel/Google Sheets +- **Export Excel (TSV)** avec encodage UTF-8 +- **Import CSV/JSON** pour restaurer des données +- Gestion des caractères spéciaux et échappement + +#### ♟️ Gestion FEN Complète + +- **Import/Export FEN** avec validation +- **Copie vers le presse-papiers** +- **Chargement depuis fichier** +- **Comparaison de positions** +- **Descriptions automatiques** de positions +- **Générateur de positions aléatoires** +- Utilitaires pour FEN simplifiées + +#### 🧩 Nouveaux Composants + +- **ErrorBoundary** global pour une gestion gracieuse des erreurs +- **Skeleton Components** pour de meilleures expériences de chargement +- **KeyboardShortcutsModal** pour afficher les raccourcis clavier + +#### 🔧 Hooks Personnalisés + +- `useKeyboardShortcuts` - Gestion des raccourcis clavier +- `useLocalStorage` - Synchronisation avec localStorage +- `useDebounce` - Optimisation des performances +- `useMediaQuery` - Responsive design +- `useIsMobile`, `useIsTablet`, `useIsDesktop` - Breakpoints + +### 📚 Documentation Enrichie + +#### 📝 Nouveaux Fichiers + +- **CONTRIBUTING.md** - Guide complet de contribution +- **CHANGELOG.md** - Historique des versions +- **constants.ts** - Configuration centralisée +- Commentaires **JSDoc** pour toutes les fonctions + +#### 📖 Ressources Développeurs + +- Templates pour bug reports et feature requests +- Conventions de nommage et standards de code +- Exemples de tests unitaires et de composants +- Guide de commit messages (Conventional Commits) + +### 🚀 Améliorations Techniques + +- **TypeScript strict mode** amélioré +- **Validation renforcée** pour FEN et PGN +- **Messages d'erreur** user-friendly +- **Gestion d'erreurs** améliorée +- **Code organization** optimisée avec extraction de hooks + +--- + ## ✨ Fonctionnalités ### 🎯 Fonctionnalités Principales #### 📚 Base de Données de Parties de Maîtres + - **Bibliothèque complète** de parties célèbres annotées - **Import multi-sources** : - Fichiers PGN locaux (drag & drop) @@ -110,6 +220,7 @@ - Filtrage en temps réel #### 🔍 Encyclopédie d'Ouvertures + - Base de données complète avec **codes ECO** - **Variations principales** documentées - **Statistiques détaillées** : @@ -120,6 +231,7 @@ - Navigation dans les variantes #### 👥 Base de Données de Joueurs Professionnelle + - **CRUD complet** (Create, Read, Update, Delete) - **Informations détaillées** : - Données personnelles (nom, prénom, date de naissance) @@ -142,6 +254,7 @@ ### 📊 Analyse Avancée #### 🧠 Moteur Stockfish 16 NNUE + - **Analyse ultra-performante** avec Web Worker dédié - **Configuration intelligente** : - Profondeur adaptative (10-18 basée sur CPU) @@ -161,6 +274,7 @@ - Indicateurs de menaces #### 📈 Statistiques de Parties + - **Graphiques interactifs** (Chart.js) : - Distribution des ouvertures - Taux de victoire par ouverture @@ -175,6 +289,7 @@ - Suggestions d'amélioration #### 🎯 Reconnaissance de Patterns + - Détection automatique des **patterns classiques** : - Mat du couloir - Mat étouffé @@ -191,6 +306,7 @@ #### 🧩 4 Modes de Puzzles Tactiques **1. 🎯 Puzzle Trainer** + - Base de données de puzzles par thème - Rating des puzzles (débutant à maître) - Validation automatique des solutions @@ -198,23 +314,27 @@ - Feedback instantané **2. 📅 Daily Puzzle** + - Puzzle quotidien unique - **Streak tracking** (série de jours consécutifs) - Notifications automatiques - Historique des puzzles résolus **3. ⚡ Puzzle Rush** + - Mode **contre-la-montre** - Score basé sur vitesse et précision - Classement mondial - Statistiques de performance **4. ⚔️ Puzzle Battle** + - Mode **compétitif** contre d'autres joueurs - Système de ranking ELO - Matchmaking équilibré #### 📚 Mode Apprentissage + - **Leçons structurées** par thème : - Ouvertures fondamentales - Tactiques de milieu de partie @@ -225,6 +345,7 @@ - Solutions détaillées avec explications #### 🎥 Bibliothèque Vidéo + - Collection de **vidéos éducatives** : - Intégration YouTube - Catégories organisées (Ouvertures, Milieu, Finales) @@ -232,6 +353,7 @@ - Ressources de grands maîtres #### 🏆 Système de Succès (Achievements) + - **Tracking automatique** : - Puzzles résolus - Parties jouées @@ -245,6 +367,7 @@ ### 🎮 Modes de Jeu #### 🤖 Jouer contre l'Ordinateur + - **10 niveaux de difficulté** (profondeur 1-20) - **Sélection de position de départ** : - Partie standard @@ -265,6 +388,7 @@ - Graphiques de performance #### 🏆 Système de Tournois + - **Création de tournois personnalisés** - **Format Round-Robin** automatique - Sélection de joueurs depuis la base de données @@ -277,11 +401,13 @@ - Export des résultats #### 👀 Mode Spectateur + - Visualisation de parties en cours - Streaming de parties live - Commentaires en temps réel #### 🎖️ Leaderboards + - Classements mondiaux : - Meilleurs scores puzzle - Top joueurs du mode ordinateur @@ -320,6 +446,7 @@ ### 🎨 Personnalisation #### 🎨 9 Thèmes d'Échiquier + 1. **Classic** - Marron et beige traditionnel 2. **Blue** - Bleu océan moderne 3. **Green** - Vert forêt apaisant @@ -333,6 +460,7 @@ Persistence des préférences en localStorage. #### 🌓 3 Modes de Thème UI + - **Light** - Interface claire - **Dark** - Mode sombre élégant - **System** - Suit les préférences système @@ -341,6 +469,7 @@ Persistence des préférences en localStorage. - Transitions fluides #### 🔔 Centre de Notifications + - **Système complet** avec React Toastify : - 4 types : Info, Success, Error, Warning - Durée configurable @@ -487,6 +616,7 @@ ChessDatabase/ ### 🏛️ Patterns Architecturaux #### State Management (Zustand) + - **Store centralisé** avec `subscribeWithSelector` - **Persistence localStorage** pour : - Préférences utilisateur (thème, langue) @@ -501,6 +631,7 @@ ChessDatabase/ - Données de tournois #### Server State (React Query) + - Cache optimisé (5 min staleTime) - Retry automatique (2 tentatives) - Gestion d'erreurs API (429, 404) @@ -508,6 +639,7 @@ ChessDatabase/ - Rate limiting avec `p-queue` #### Component Architecture + - **Lazy Loading** pour performances : - `React.lazy` pour ChessAnalyzer - `React.lazy` pour ChessOpenings @@ -522,64 +654,64 @@ ChessDatabase/ ### 🎯 Core Stack -| Technologie | Version | Usage | -|------------|---------|-------| -| ![React](https://img.shields.io/badge/-React-61DAFB?style=flat-square&logo=react&logoColor=black) | 18.3.1 | Framework UI | -| ![TypeScript](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) | 5.5.3 | Type safety | -| ![Vite](https://img.shields.io/badge/-Vite-646CFF?style=flat-square&logo=vite&logoColor=white) | 5.4.2 | Build tool ultra-rapide | -| ![Tailwind CSS](https://img.shields.io/badge/-Tailwind-38B2AC?style=flat-square&logo=tailwind-css&logoColor=white) | 3.4.1 | Styling utility-first | +| Technologie | Version | Usage | +| ------------------------------------------------------------------------------------------------------------------ | ------- | ----------------------- | +| ![React](https://img.shields.io/badge/-React-61DAFB?style=flat-square&logo=react&logoColor=black) | 18.3.1 | Framework UI | +| ![TypeScript](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) | 5.5.3 | Type safety | +| ![Vite](https://img.shields.io/badge/-Vite-646CFF?style=flat-square&logo=vite&logoColor=white) | 5.4.2 | Build tool ultra-rapide | +| ![Tailwind CSS](https://img.shields.io/badge/-Tailwind-38B2AC?style=flat-square&logo=tailwind-css&logoColor=white) | 3.4.1 | Styling utility-first | ### ♟️ Chess Logic -| Librairie | Version | Usage | -|-----------|---------|-------| -| ![Chess.js](https://img.shields.io/badge/-Chess.js-000000?style=flat-square) | 1.0.0-beta.7 | Logique d'échecs | -| ![Stockfish](https://img.shields.io/badge/-Stockfish-000000?style=flat-square) | 16.0.0 | Moteur d'analyse NNUE | -| ![React Chessboard](https://img.shields.io/badge/-React%20Chessboard-61DAFB?style=flat-square) | 4.7.3 | Échiquier interactif | -| ![PGN Parser](https://img.shields.io/badge/-PGN%20Parser-000000?style=flat-square) | 1.4.18 | Parsing PGN robuste | +| Librairie | Version | Usage | +| ---------------------------------------------------------------------------------------------- | ------------ | --------------------- | +| ![Chess.js](https://img.shields.io/badge/-Chess.js-000000?style=flat-square) | 1.0.0-beta.7 | Logique d'échecs | +| ![Stockfish](https://img.shields.io/badge/-Stockfish-000000?style=flat-square) | 16.0.0 | Moteur d'analyse NNUE | +| ![React Chessboard](https://img.shields.io/badge/-React%20Chessboard-61DAFB?style=flat-square) | 4.7.3 | Échiquier interactif | +| ![PGN Parser](https://img.shields.io/badge/-PGN%20Parser-000000?style=flat-square) | 1.4.18 | Parsing PGN robuste | ### 🎨 UI/UX -| Librairie | Version | Usage | -|-----------|---------|-------| -| ![Lucide React](https://img.shields.io/badge/-Lucide-F56565?style=flat-square) | 0.344.0 | Icons modernes | -| ![React DnD](https://img.shields.io/badge/-React%20DnD-000000?style=flat-square) | 16.0.1 | Drag & Drop | -| ![React Dropzone](https://img.shields.io/badge/-Dropzone-61DAFB?style=flat-square) | 14.2.3 | Upload fichiers | -| ![React Toastify](https://img.shields.io/badge/-Toastify-EB4D4B?style=flat-square) | 10.0.4 | Notifications toast | -| ![React Focus Lock](https://img.shields.io/badge/-Focus%20Lock-61DAFB?style=flat-square) | 2.9.6 | Accessibilité modal | -| ![Chart.js](https://img.shields.io/badge/-Chart.js-FF6384?style=flat-square&logo=chart.js&logoColor=white) | 4.5.0 | Graphiques | +| Librairie | Version | Usage | +| ---------------------------------------------------------------------------------------------------------- | ------- | ------------------- | +| ![Lucide React](https://img.shields.io/badge/-Lucide-F56565?style=flat-square) | 0.344.0 | Icons modernes | +| ![React DnD](https://img.shields.io/badge/-React%20DnD-000000?style=flat-square) | 16.0.1 | Drag & Drop | +| ![React Dropzone](https://img.shields.io/badge/-Dropzone-61DAFB?style=flat-square) | 14.2.3 | Upload fichiers | +| ![React Toastify](https://img.shields.io/badge/-Toastify-EB4D4B?style=flat-square) | 10.0.4 | Notifications toast | +| ![React Focus Lock](https://img.shields.io/badge/-Focus%20Lock-61DAFB?style=flat-square) | 2.9.6 | Accessibilité modal | +| ![Chart.js](https://img.shields.io/badge/-Chart.js-FF6384?style=flat-square&logo=chart.js&logoColor=white) | 4.5.0 | Graphiques | ### 📊 State & Data -| Librairie | Version | Usage | -|-----------|---------|-------| -| ![Zustand](https://img.shields.io/badge/-Zustand-000000?style=flat-square) | 4.4.7 | State management | -| ![React Query](https://img.shields.io/badge/-React%20Query-FF4154?style=flat-square&logo=react-query&logoColor=white) | 5.17.0 | Server state & cache | -| ![p-queue](https://img.shields.io/badge/-p--queue-000000?style=flat-square) | 8.1.0 | Rate limiting API | +| Librairie | Version | Usage | +| --------------------------------------------------------------------------------------------------------------------- | ------- | -------------------- | +| ![Zustand](https://img.shields.io/badge/-Zustand-000000?style=flat-square) | 4.4.7 | State management | +| ![React Query](https://img.shields.io/badge/-React%20Query-FF4154?style=flat-square&logo=react-query&logoColor=white) | 5.17.0 | Server state & cache | +| ![p-queue](https://img.shields.io/badge/-p--queue-000000?style=flat-square) | 8.1.0 | Rate limiting API | ### 🌍 i18n & Monitoring -| Librairie | Version | Usage | -|-----------|---------|-------| -| ![i18next](https://img.shields.io/badge/-i18next-26A69A?style=flat-square&logo=i18next&logoColor=white) | 25.3.1 | Internationalisation | -| ![Sentry](https://img.shields.io/badge/-Sentry-362D59?style=flat-square&logo=sentry&logoColor=white) | 7.99.0 | Error tracking & monitoring | +| Librairie | Version | Usage | +| ------------------------------------------------------------------------------------------------------- | ------- | --------------------------- | +| ![i18next](https://img.shields.io/badge/-i18next-26A69A?style=flat-square&logo=i18next&logoColor=white) | 25.3.1 | Internationalisation | +| ![Sentry](https://img.shields.io/badge/-Sentry-362D59?style=flat-square&logo=sentry&logoColor=white) | 7.99.0 | Error tracking & monitoring | ### 🧪 Dev Tools -| Outil | Version | Usage | -|-------|---------|-------| -| ![ESLint](https://img.shields.io/badge/-ESLint-4B32C3?style=flat-square&logo=eslint&logoColor=white) | 9.9.1 | Linting | -| ![Playwright](https://img.shields.io/badge/-Playwright-2EAD33?style=flat-square&logo=playwright&logoColor=white) | 1.40.1 | Tests E2E | -| ![TypeScript ESLint](https://img.shields.io/badge/-TS%20ESLint-3178C6?style=flat-square) | 8.3.0 | Linting TypeScript | +| Outil | Version | Usage | +| ---------------------------------------------------------------------------------------------------------------- | ------- | ------------------ | +| ![ESLint](https://img.shields.io/badge/-ESLint-4B32C3?style=flat-square&logo=eslint&logoColor=white) | 9.9.1 | Linting | +| ![Playwright](https://img.shields.io/badge/-Playwright-2EAD33?style=flat-square&logo=playwright&logoColor=white) | 1.40.1 | Tests E2E | +| ![TypeScript ESLint](https://img.shields.io/badge/-TS%20ESLint-3178C6?style=flat-square) | 8.3.0 | Linting TypeScript | ### 🚀 PWA & Export -| Librairie | Version | Usage | -|-----------|---------|-------| -| ![PWA](https://img.shields.io/badge/-Vite%20PWA-5A0FC8?style=flat-square) | 0.17.4 | Progressive Web App | -| ![html2canvas](https://img.shields.io/badge/-html2canvas-E44D26?style=flat-square) | 1.4.1 | Screenshots | -| ![gifshot](https://img.shields.io/badge/-gifshot-000000?style=flat-square) | 0.4.5 | Création GIF | -| ![sanitize-html](https://img.shields.io/badge/-sanitize--html-E44D26?style=flat-square) | 2.11.0 | Sécurité HTML | +| Librairie | Version | Usage | +| --------------------------------------------------------------------------------------- | ------- | ------------------- | +| ![PWA](https://img.shields.io/badge/-Vite%20PWA-5A0FC8?style=flat-square) | 0.17.4 | Progressive Web App | +| ![html2canvas](https://img.shields.io/badge/-html2canvas-E44D26?style=flat-square) | 1.4.1 | Screenshots | +| ![gifshot](https://img.shields.io/badge/-gifshot-000000?style=flat-square) | 0.4.5 | Création GIF | +| ![sanitize-html](https://img.shields.io/badge/-sanitize--html-E44D26?style=flat-square) | 2.11.0 | Sécurité HTML | --- @@ -645,6 +777,7 @@ npm run build ``` Les fichiers optimisés seront générés dans le dossier `dist/` : + - Minification JavaScript/CSS - Tree-shaking automatique - Code splitting @@ -659,32 +792,32 @@ Les fichiers optimisés seront générés dans le dossier `dist/` : L'application propose **13 vues** accessibles via le **Start Menu** : -| Touche | Vue | Description | -|--------|-----|-------------| -| `1` | **Games Database** | Base de données de parties | -| `2` | **Players Database** | Gestion des joueurs | -| `3` | **Openings Encyclopedia** | Encyclopédie d'ouvertures | -| `4` | **Play vs Computer** | Jouer contre Stockfish | -| `5` | **Puzzle Trainer** | Entraînement tactique | -| `6` | **Daily Puzzle** | Puzzle quotidien | -| `7` | **Puzzle Rush** | Mode contre-la-montre | -| `8` | **Puzzle Battle** | Bataille de puzzles | -| `9` | **Spectator Mode** | Mode spectateur | -| `10` | **Learning** | Cours et leçons | -| `11` | **Video Library** | Bibliothèque vidéo | -| `12` | **Tournaments** | Système de tournois | -| `13` | **Leaderboards** | Classements | +| Touche | Vue | Description | +| ------ | ------------------------- | -------------------------- | +| `1` | **Games Database** | Base de données de parties | +| `2` | **Players Database** | Gestion des joueurs | +| `3` | **Openings Encyclopedia** | Encyclopédie d'ouvertures | +| `4` | **Play vs Computer** | Jouer contre Stockfish | +| `5` | **Puzzle Trainer** | Entraînement tactique | +| `6` | **Daily Puzzle** | Puzzle quotidien | +| `7` | **Puzzle Rush** | Mode contre-la-montre | +| `8` | **Puzzle Battle** | Bataille de puzzles | +| `9` | **Spectator Mode** | Mode spectateur | +| `10` | **Learning** | Cours et leçons | +| `11` | **Video Library** | Bibliothèque vidéo | +| `12` | **Tournaments** | Système de tournois | +| `13` | **Leaderboards** | Classements | ### ⌨️ Raccourcis Clavier -| Raccourci | Action | -|-----------|--------| +| Raccourci | Action | +| --------- | ------------------------------------ | | `←` / `→` | Navigation coups (précédent/suivant) | -| `Alt + G` | Vue Games Database | -| `Alt + P` | Vue Players Database | -| `Alt + O` | Vue Openings | -| `Esc` | Fermer modal | -| `F11` | Plein écran | +| `Alt + G` | Vue Games Database | +| `Alt + P` | Vue Players Database | +| `Alt + O` | Vue Openings | +| `Esc` | Fermer modal | +| `F11` | Plein écran | ### 📥 Import de Parties @@ -729,6 +862,7 @@ L'application propose **13 vues** accessibles via le **Start Menu** : ### 🧩 Résoudre des Puzzles #### Mode Trainer + 1. Aller dans **Puzzle Trainer** (touche `5`) 2. Choisir un thème tactique 3. Résoudre le puzzle en jouant les coups @@ -736,6 +870,7 @@ L'application propose **13 vues** accessibles via le **Start Menu** : 5. Explication détaillée si erreur #### Daily Puzzle + 1. Aller dans **Daily Puzzle** (touche `6`) 2. Un nouveau puzzle chaque jour 3. Maintenir votre **streak** quotidien @@ -762,15 +897,18 @@ L'application propose **13 vues** accessibles via le **Start Menu** : ### 📲 Installation #### Desktop (Chrome/Edge) + 1. Ouvrir l'application dans le navigateur 2. Cliquer sur l'icône **"+""** dans la barre d'adresse 3. Confirmer l'installation #### Mobile (Android) + 1. Ouvrir l'application dans Chrome 2. Menu ⋮ → **"Ajouter à l'écran d'accueil"** #### iOS (Safari) + 1. Ouvrir l'application dans Safari 2. Partager → **"Sur l'écran d'accueil"** @@ -786,11 +924,11 @@ L'application propose **13 vues** accessibles via le **Start Menu** : ### Langues Supportées -| Langue | Code | État | -|--------|------|------| +| Langue | Code | État | +| ----------- | ---- | ------------------- | | 🇫🇷 Français | `fr` | ✅ Complet (défaut) | -| 🇬🇧 English | `en` | ✅ Complet | -| 🇪🇸 Español | `es` | ✅ Complet | +| 🇬🇧 English | `en` | ✅ Complet | +| 🇪🇸 Español | `es` | ✅ Complet | ### Configuration @@ -824,18 +962,21 @@ L'application propose **13 vues** accessibles via le **Start Menu** : L'application respecte les standards **WCAG 2.1 Level AA** : #### ⌨️ Navigation Clavier + - **Tab** : Navigation entre éléments - **Enter/Space** : Activation des boutons - **Escape** : Fermeture des modals - **Arrow keys** : Navigation dans les coups #### 🔒 Focus Management + - **Focus Lock** dans les modals (react-focus-lock) - **Focus trap** : Empêche la sortie du modal - **Restauration du focus** après fermeture - **Indicateurs visuels** clairs #### 🏷️ ARIA & Sémantique + - **ARIA labels** sur tous les boutons interactifs - **Rôles sémantiques** appropriés (`dialog`, `button`, `navigation`) - **Live regions** pour les mises à jour dynamiques @@ -843,18 +984,21 @@ L'application respecte les standards **WCAG 2.1 Level AA** : - **aria-hidden** sur les icônes décoratives #### 📱 Responsive & Touch + - **Mobile-first** design - **Touch-friendly** (cibles >= 44x44px) - **Responsive** sur tous les breakpoints - **Zoom** : Support jusqu'à 200% #### 🎨 Contraste & Visibilité + - **Contraste minimum** : 4.5:1 (texte normal) - **Contraste amélioré** : 7:1 (titres) - **Mode sombre** avec contrastes adaptés - **Pas de dépendance à la couleur** seule #### 🔊 Screen Readers + - **Compatible** NVDA, JAWS, VoiceOver - **Descriptions** claires et concises - **Annonces** de changements d'état @@ -941,14 +1085,14 @@ Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN, integrations: [ new Sentry.BrowserTracing({ - tracePropagationTargets: ["localhost", "yourdomain.com"], + tracePropagationTargets: ['localhost', 'yourdomain.com'], }), new Sentry.Replay({ maskAllText: false, blockAllMedia: false, }), ], - tracesSampleRate: 0.1, // 10% des transactions + tracesSampleRate: 0.1, // 10% des transactions replaysSessionSampleRate: 0.1, // 10% des sessions replaysOnErrorSampleRate: 1.0, // 100% en cas d'erreur beforeSend(event) { @@ -977,6 +1121,7 @@ Sentry.init({ #### Monitoring en Production Dashboard Sentry affiche : + - Taux d'erreur - Erreurs les plus fréquentes - Performance des transactions @@ -1009,6 +1154,7 @@ Les contributions sont les bienvenues ! Voici comment participer : ### 📝 Guidelines #### Code Style + - **ESLint** : Respecter la configuration ESLint - **TypeScript** : Typage strict obligatoire - **Naming** : @@ -1017,9 +1163,11 @@ Les contributions sont les bienvenues ! Voici comment participer : - Constants: UPPER_SNAKE_CASE #### Commits + Format : `type(scope): message` Types : + - `feat`: Nouvelle fonctionnalité - `fix`: Correction de bug - `docs`: Documentation @@ -1029,6 +1177,7 @@ Types : - `chore`: Maintenance, dépendances Exemples : + ```bash feat(analyzer): add multi-pv support fix(import): handle malformed PGN files @@ -1036,10 +1185,12 @@ docs(readme): update installation instructions ``` #### Tests + - Ajouter des **tests E2E** pour les nouvelles features - S'assurer que tous les tests passent avant PR #### Documentation + - Commenter le **code complexe** - Mettre à jour le **README** si nécessaire - Ajouter des **types TypeScript** complets @@ -1047,6 +1198,7 @@ docs(readme): update installation instructions ### 🐛 Reporter un Bug Utilisez les [GitHub Issues](https://github.com/phuetz/ChessDatabase/issues) avec : + - Description claire du problème - Steps to reproduce - Comportement attendu vs actuel @@ -1056,6 +1208,7 @@ Utilisez les [GitHub Issues](https://github.com/phuetz/ChessDatabase/issues) ave ### 💡 Proposer une Feature Ouvrir une issue avec : + - Description de la feature - Use case / problème résolu - Proposition d'implémentation @@ -1117,6 +1270,7 @@ Un grand merci aux projets open source qui rendent cette application possible : ### ♟️ Communauté Échecs Merci à la communauté mondiale des échecs pour : + - Les parties historiques annotées - Les bases de données d'ouvertures - Les ressources éducatives @@ -1193,7 +1347,7 @@ Pour obtenir de l'aide : ### 🎯 Fait avec 💙 par des passionnés d'échecs pour la communauté -**Chess Master Database** - *Élevez votre jeu au niveau supérieur* +**Chess Master Database** - _Élevez votre jeu au niveau supérieur_ --- diff --git a/package-lock.json b/package-lock.json index dec5401..377bfd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,23 +35,48 @@ "devDependencies": { "@eslint/js": "^9.9.1", "@playwright/test": "^1.40.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", + "@vitest/ui": "^4.0.9", "autoprefixer": "^10.4.18", "eslint": "^9.9.1", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.11", "globals": "^15.9.0", + "husky": "^9.1.7", + "jsdom": "^27.2.0", + "lint-staged": "^16.2.6", "postcss": "^8.4.35", + "prettier": "^3.6.2", "tailwindcss": "^3.4.1", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", "vite": "^5.4.2", - "vite-plugin-pwa": "^0.17.4" + "vite-bundle-visualizer": "^1.2.1", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-pwa": "^0.17.4", + "vitest": "^4.0.9" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.23", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.23.tgz", + "integrity": "sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -77,6 +102,61 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz", + "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==", + "dev": true, + "license": "MIT", + "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" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1618,6 +1698,141 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.16.tgz", + "integrity": "sha512-2SpS4/UaWQaGpBINyG5ZuCHnUDeVByOhvbkARwfmnfxDvTaj80yOI1cD8Tw93ICV5Fx4fnyDKWQZI1CDtcWyUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1890,6 +2105,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -1906,6 +2138,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -1922,6 +2171,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", @@ -2234,10 +2500,11 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -2334,6 +2601,13 @@ "node": ">=18" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@react-dnd/asap": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", @@ -2434,208 +2708,308 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2786,6 +3160,13 @@ "node": ">=8" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -2825,17 +3206,126 @@ "react": "^18 || ^19" } }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, "node_modules/@types/babel__generator": { @@ -2866,11 +3356,30 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -3167,6 +3676,122 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.9.tgz", + "integrity": "sha512-C2vyXf5/Jfj1vl4DQYxjib3jzyuswMi/KHHVN2z+H4v16hdJ7jMZ0OGe3uOVIt6LyJsAofDdaJNIFEpQcrSTFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", + "chai": "^6.2.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.9.tgz", + "integrity": "sha512-Hor0IBTwEi/uZqB7pvGepyElaM8J75pYjrrqbC8ZYMB9/4n5QA63KC15xhT+sqHpdGWfdnPo96E8lQUxs2YzSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.9.tgz", + "integrity": "sha512-aF77tsXdEvIJRkj9uJZnHtovsVIx22Ambft9HudC+XuG/on1NY/bf5dlDti1N35eJT+QZLb4RF/5dTIG18s98w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.9.tgz", + "integrity": "sha512-r1qR4oYstPbnOjg0Vgd3E8ADJbi4ditCzqr+Z9foUrRhIy778BleNyZMeAJ2EjV+r4ASAaDsdciC9ryMy8xMMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.9.tgz", + "integrity": "sha512-J9Ttsq0hDXmxmT8CUOWUr1cqqAj2FJRGTdyEjSR+NjoOGKEqkEWj+09yC0HhI8t1W6t4Ctqawl1onHgipJve1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.9.tgz", + "integrity": "sha512-6HV2HHl9aRJ09TlYj/WAQxaa797Ezb5u0LpgabthlASAUAWKgw/W1DSPX7t848mMZmIUvzZgnUHGIylAoYHP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.9", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.9" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.9.tgz", + "integrity": "sha512-cEol6ygTzY4rUPvNZM19sDf7zGa35IYTm9wfzkHoT/f5jX10IOY7QleWSOh5T0e3I3WVozwK5Asom79qW8DiuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.9", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -3189,6 +3814,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3205,6 +3840,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -3374,6 +4025,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -3547,6 +4208,16 @@ "node": ">= 0.6.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3622,6 +4293,16 @@ "dev": true, "license": "MIT" }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3711,6 +4392,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3781,73 +4472,208 @@ "node": ">= 6" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, "engines": { - "node": ">= 6" + "node": ">=12" } }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4.0.0" + "node": ">=8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, - "node_modules/core-js-compat": { - "version": "3.43.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz", - "integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==", + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/core-js-compat": { + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz", + "integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.0" }, "funding": { "type": "opencollective", @@ -3888,6 +4714,27 @@ "utrie": "^1.0.2" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3900,6 +4747,21 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz", + "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3913,6 +4775,57 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3968,10 +4881,11 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -3984,6 +4898,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4017,6 +4938,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -4035,6 +4966,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -4063,6 +5005,14 @@ "redux": "^4.2.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -4180,6 +5130,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -4269,6 +5232,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4600,6 +5570,16 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4671,6 +5651,13 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4770,10 +5757,11 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" }, "node_modules/focus-lock": { "version": "1.3.6", @@ -4918,6 +5906,29 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5201,6 +6212,19 @@ "react-is": "^16.7.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -5242,6 +6266,50 @@ "entities": "^4.4.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/i18next": { "version": "25.3.1", "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.1.tgz", @@ -5273,9 +6341,22 @@ } } }, - "node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", "dev": true, "license": "ISC" @@ -5311,6 +6392,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-from-esm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.3.4.tgz", + "integrity": "sha512-7EyUlPFC0HOlBDpUFGfYstsU7XHxZJKAAMzCT8wZ0hMW7b+hG51LIKTDcsgtz8Pu6YC0HqRVbX+rVUtsGMUKvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": ">=16.20" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -5320,6 +6426,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5500,6 +6616,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5643,6 +6775,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5811,6 +6950,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -5884,6 +7036,83 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", + "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.23", + "@asamuzakjp/dom-selector": "^6.7.4", + "cssstyle": "^5.3.3", + "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" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -6052,6 +7281,115 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/lint-staged": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.6.tgz", + "integrity": "sha512-s1gphtDbV4bmW1eylXpVMk2u7is7YsrLl8hzrtvC70h4ByhcMLZFY01Fx05ZUDNuv1H8HO4E+e2zgejV1jVwNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.1", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -6103,6 +7441,82 @@ "dev": true, "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6131,6 +7545,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -6151,6 +7576,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6173,6 +7605,29 @@ "node": ">=8.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6194,6 +7649,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6211,6 +7676,19 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -6369,23 +7847,57 @@ "wrappy": "1" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">= 0.8.0" - } - }, + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -6486,6 +7998,32 @@ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", "license": "MIT" }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6542,6 +8080,13 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6560,6 +8105,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -6636,9 +8194,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -6653,9 +8211,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -6786,6 +8345,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", @@ -6799,6 +8374,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7068,6 +8692,20 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -7178,6 +8816,16 @@ "regjsparser": "bin/parser" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -7214,6 +8862,23 @@ "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7224,13 +8889,21 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -7240,25 +8913,85 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-visualizer": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz", + "integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.4.0", + "picomatch": "^4.0.2", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "rolldown": "1.x", + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7358,6 +9091,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/sanitize-html": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz", @@ -7384,6 +9124,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -7557,6 +9310,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -7569,6 +9329,67 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/smob": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", @@ -7626,6 +9447,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stockfish": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/stockfish/-/stockfish-16.0.0.tgz", @@ -7649,6 +9484,16 @@ "node": ">= 0.4" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -7872,6 +9717,19 @@ "node": ">=10" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -7931,6 +9789,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", @@ -8060,10 +9925,112 @@ "node": ">=0.8" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "dependencies": { "is-number": "^7.0.0" @@ -8072,6 +10039,29 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -8373,98 +10363,824 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-bundle-visualizer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/vite-bundle-visualizer/-/vite-bundle-visualizer-1.2.1.tgz", + "integrity": "sha512-cwz/Pg6+95YbgIDp+RPwEToc4TKxfsFWSG/tsl2DSZd9YZicUag1tQXjJ5xcL7ydvEoaC2FOZeaXOU60t9BRXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "import-from-esm": "^1.3.3", + "rollup-plugin-visualizer": "^5.11.0", + "tmp": "^0.2.1" + }, + "bin": { + "vite-bundle-visualizer": "bin.js" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, + "node_modules/vite-plugin-compression": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz", + "integrity": "sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "debug": "^4.3.3", + "fs-extra": "^10.0.0" + }, + "peerDependencies": { + "vite": ">=2.0.0" + } + }, + "node_modules/vite-plugin-compression/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-plugin-pwa": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.17.5.tgz", + "integrity": "sha512-UxRNPiJBzh4tqU/vc8G2TxmrUTzT6BqvSzhszLk62uKsf+npXdvLxGDz9C675f4BJi6MbD2tPnJhi5txlMzxbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "pretty-bytes": "^6.1.1", + "workbox-build": "^7.0.0", + "workbox-window": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0", + "workbox-build": "^7.0.0", + "workbox-window": "^7.0.0" + } + }, + "node_modules/vitest": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.9.tgz", + "integrity": "sha512-E0Ja2AX4th+CG33yAFRC+d1wFx2pzU5r6HtG6LiPSE04flaE0qB6YyjSw9ZcpJAtVPfsvZGtJlKWZpuW7EHRxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.9", + "@vitest/mocker": "4.0.9", + "@vitest/pretty-format": "4.0.9", + "@vitest/runner": "4.0.9", + "@vitest/snapshot": "4.0.9", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", + "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": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.9", + "@vitest/browser-preview": "4.0.9", + "@vitest/browser-webdriverio": "4.0.9", + "@vitest/ui": "4.0.9", + "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 + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "punycode": "^2.1.0" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.9.tgz", + "integrity": "sha512-PUyaowQFHW+9FKb4dsvvBM4o025rWMlEDXdWRxIOilGaHREYTi5Q2Rt9VCgXgPy/hHZu1LeuXtrA/GdzOatP2g==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.0.0" + "@vitest/spy": "4.0.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { - "@types/react": { + "msw": { + "optional": true + }, + "vite": { "optional": true } } }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "node_modules/vitest/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=10" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "@types/react": { + "picomatch": { "optional": true } } }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", - "dependencies": { - "base64-arraybuffer": "^1.0.2" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "node_modules/vitest/node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -8473,19 +11189,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.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 }, @@ -8506,34 +11228,15 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/vite-plugin-pwa": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.17.5.tgz", - "integrity": "sha512-UxRNPiJBzh4tqU/vc8G2TxmrUTzT6BqvSzhszLk62uKsf+npXdvLxGDz9C675f4BJi6MbD2tPnJhi5txlMzxbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "pretty-bytes": "^6.1.1", - "workbox-build": "^7.0.0", - "workbox-window": "^7.0.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0", - "workbox-build": "^7.0.0", - "workbox-window": "^7.0.0" - } - }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -8543,6 +11246,19 @@ "node": ">=0.10.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -8550,6 +11266,29 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", @@ -8666,6 +11405,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -9140,6 +11896,55 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -9147,15 +11952,90 @@ "dev": true }, "node_modules/yaml": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/yocto-queue": { diff --git a/package.json b/package.json index bf95e08..a1d5673 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,25 @@ "scripts": { "dev": "vite", "build": "vite build", + "build:analyze": "vite-bundle-visualizer", + "build:prod": "npm run lint && npm run test:coverage && vite build", "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", "preview": "vite preview", - "test:e2e": "playwright test" + "test": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest --coverage", + "test:e2e": "playwright test", + "test:e2e:headed": "playwright test --headed", + "test:e2e:debug": "playwright test --debug", + "type-check": "tsc --noEmit", + "validate": "npm run lint && npm run type-check && npm test -- --run", + "clean": "rm -rf dist node_modules/.vite", + "clean:all": "rm -rf dist node_modules coverage playwright-report", + "prepare": "husky", + "postinstall": "playwright install --with-deps chromium" }, "dependencies": { "@mliebelt/pgn-parser": "^1.4.18", @@ -38,20 +54,40 @@ "devDependencies": { "@eslint/js": "^9.9.1", "@playwright/test": "^1.40.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", + "@vitest/ui": "^4.0.9", "autoprefixer": "^10.4.18", "eslint": "^9.9.1", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.11", "globals": "^15.9.0", + "husky": "^9.1.7", + "jsdom": "^27.2.0", + "lint-staged": "^16.2.6", "postcss": "^8.4.35", + "prettier": "^3.6.2", "tailwindcss": "^3.4.1", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", "vite": "^5.4.2", - "vite-plugin-pwa": "^0.17.4" + "vite-bundle-visualizer": "^1.2.1", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-pwa": "^0.17.4", + "vitest": "^4.0.9" + }, + "lint-staged": { + "*.{ts,tsx,js,jsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,css,md}": [ + "prettier --write" + ] } } diff --git a/src/App.tsx b/src/App.tsx index c8ce346..e5688e2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import { masterGames } from './data/masterGames'; import ChessMasterDatabase from './components/ChessMasterDatabase'; import StartMenu from './components/StartMenu'; import NotificationCenter from './components/NotificationCenter'; +import ErrorBoundary from './components/ErrorBoundary'; // Initialize Sentry initSentry(); @@ -35,7 +36,7 @@ const App = () => { addNotification({ id: 'tournament', message: 'Un nouveau tournoi débute dans 10 minutes !', - type: 'info' + type: 'info', }); }, 10000); @@ -43,7 +44,7 @@ const App = () => { addNotification({ id: 'challenge', message: 'Vous avez reçu un nouveau défi !', - type: 'info' + type: 'info', }); }, 5000); @@ -54,15 +55,15 @@ const App = () => { }, [addNotification]); return ( - -
- {view === 'menu' ? : } - -
-
+ + +
+ {view === 'menu' ? : } + +
+
+
); }; -export default App; \ No newline at end of file +export default App; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..29f097f --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,146 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { AlertTriangle, RefreshCw, Home } from 'lucide-react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; + errorInfo: ErrorInfo | null; +} + +/** + * ErrorBoundary component to catch and handle React errors gracefully + * Displays a user-friendly error message and provides recovery options + */ +class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo); + this.setState({ + error, + errorInfo, + }); + + // Log to error tracking service if configured + if (window.Sentry) { + window.Sentry.captureException(error, { + contexts: { + react: { + componentStack: errorInfo.componentStack, + }, + }, + }); + } + } + + handleReset = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null, + }); + }; + + handleReload = () => { + window.location.reload(); + }; + + handleGoHome = () => { + window.location.href = '/'; + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+
+
+
+ +
+
+

+ Oops! Something went wrong +

+

+ We encountered an unexpected error +

+
+
+ +
+

+ Error Details: +

+

+ {this.state.error?.toString()} +

+ {process.env.NODE_ENV === 'development' && this.state.errorInfo && ( +
+ + View stack trace + +
+                    {this.state.errorInfo.componentStack}
+                  
+
+ )} +
+ +
+ + + +
+ +

+ If the problem persists, please contact support or try clearing your browser cache. +

+
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/src/components/KeyboardShortcutsModal.tsx b/src/components/KeyboardShortcutsModal.tsx new file mode 100644 index 0000000..60d52e0 --- /dev/null +++ b/src/components/KeyboardShortcutsModal.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import AccessibleModal from './AccessibleModal'; +import { Keyboard } from 'lucide-react'; + +interface KeyboardShortcutsModalProps { + isOpen: boolean; + onClose: () => void; +} + +interface Shortcut { + keys: string[]; + description: string; + category: string; +} + +const KeyboardShortcutsModal: React.FC = ({ isOpen, onClose }) => { + const shortcuts: Shortcut[] = [ + // Navigation + { + keys: ['←'], + description: 'Previous move', + category: 'Navigation', + }, + { + keys: ['→'], + description: 'Next move', + category: 'Navigation', + }, + { + keys: ['Home'], + description: 'Go to first move', + category: 'Navigation', + }, + { + keys: ['End'], + description: 'Go to last move', + category: 'Navigation', + }, + // Board + { + keys: ['F'], + description: 'Flip board', + category: 'Board', + }, + { + keys: ['Alt', 'F'], + description: 'Toggle fullscreen', + category: 'Board', + }, + // General + { + keys: ['?'], + description: 'Show keyboard shortcuts', + category: 'General', + }, + { + keys: ['/'], + description: 'Focus search', + category: 'General', + }, + { + keys: ['Esc'], + description: 'Close modal / Cancel', + category: 'General', + }, + { + keys: ['Ctrl', 'Z'], + description: 'Undo move', + category: 'General', + }, + { + keys: ['Ctrl', 'Y'], + description: 'Redo move', + category: 'General', + }, + // Analysis + { + keys: ['A'], + description: 'Toggle analysis', + category: 'Analysis', + }, + { + keys: ['Space'], + description: 'Start/Pause engine', + category: 'Analysis', + }, + // Views + { + keys: ['1'], + description: 'Games database view', + category: 'Views', + }, + { + keys: ['2'], + description: 'Openings encyclopedia view', + category: 'Views', + }, + { + keys: ['3'], + description: 'Players database view', + category: 'Views', + }, + ]; + + const categories = Array.from(new Set(shortcuts.map(s => s.category))); + + return ( + +
+
+
+ +
+
+

Keyboard Shortcuts

+

+ Master these shortcuts to improve your productivity +

+
+
+ +
+ {categories.map(category => ( +
+

+
+ {category} +

+
+ {shortcuts + .filter(s => s.category === category) + .map((shortcut, index) => ( +
+ + {shortcut.description} + +
+ {shortcut.keys.map((key, i) => ( + + + {key} + + {i < shortcut.keys.length - 1 && ( + + + )} + + ))} +
+
+ ))} +
+
+ ))} +
+ +
+

+ Pro tip: Press{' '} + + ? + {' '} + anytime to view these shortcuts +

+
+ +
+ +
+
+
+ ); +}; + +export default KeyboardShortcutsModal; diff --git a/src/components/Skeleton.tsx b/src/components/Skeleton.tsx new file mode 100644 index 0000000..4572611 --- /dev/null +++ b/src/components/Skeleton.tsx @@ -0,0 +1,169 @@ +import React from 'react'; + +interface SkeletonProps { + className?: string; + variant?: 'text' | 'circular' | 'rectangular'; + width?: string | number; + height?: string | number; + animation?: 'pulse' | 'wave' | 'none'; +} + +/** + * Skeleton component for loading states + * Provides a placeholder UI while content is loading + */ +export const Skeleton: React.FC = ({ + className = '', + variant = 'text', + width, + height, + animation = 'pulse', +}) => { + const baseClasses = 'bg-gray-200 dark:bg-gray-700'; + + const animationClasses = { + pulse: 'animate-pulse', + wave: 'animate-shimmer', + none: '', + }; + + const variantClasses = { + text: 'rounded', + circular: 'rounded-full', + rectangular: 'rounded-lg', + }; + + const style: React.CSSProperties = { + width: width || (variant === 'text' ? '100%' : undefined), + height: height || (variant === 'text' ? '1em' : undefined), + }; + + return ( +
+ ); +}; + +/** + * Skeleton loader for game cards + */ +export const GameCardSkeleton: React.FC = () => { + return ( +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ ); +}; + +/** + * Skeleton loader for player cards + */ +export const PlayerCardSkeleton: React.FC = () => { + return ( +
+
+ +
+ + +
+
+
+ + +
+
+ ); +}; + +/** + * Skeleton loader for chess board + */ +export const BoardSkeleton: React.FC = () => { + return ( +
+
+ {Array.from({ length: 64 }).map((_, i) => ( +
+ ))} +
+
+ ); +}; + +/** + * Skeleton loader for list items + */ +export const ListItemSkeleton: React.FC<{ count?: number }> = ({ count = 5 }) => { + return ( + <> + {Array.from({ length: count }).map((_, i) => ( +
+
+
+ + +
+ +
+
+ ))} + + ); +}; + +/** + * Skeleton loader for table rows + */ +export const TableRowSkeleton: React.FC<{ columns?: number; rows?: number }> = ({ + columns = 4, + rows = 5, +}) => { + return ( + <> + {Array.from({ length: rows }).map((_, rowIndex) => ( + + {Array.from({ length: columns }).map((_, colIndex) => ( + + + + ))} + + ))} + + ); +}; + +export default Skeleton; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..b53b6c0 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,201 @@ +/** + * Application-wide constants + * Centralizes magic numbers and configuration values + */ + +// Stockfish Engine Configuration +export const STOCKFISH_CONFIG = { + MIN_DEPTH: 10, + MAX_DEPTH: 18, + DEFAULT_DEPTH: 15, + MULTI_PV: 3, + DIFFICULTY_LEVELS: { + BEGINNER: 1, + EASY: 5, + MEDIUM: 10, + HARD: 15, + EXPERT: 20, + }, +} as const; + +// ELO Rating Configuration +export const ELO_CONFIG = { + DEFAULT_K_FACTOR: 20, + DEFAULT_RATING: 1500, + MIN_RATING: 100, + MAX_RATING: 3000, + RATING_DIFFERENCE_SCALING: 400, +} as const; + +// Chess Piece Values (in pawns) +export const PIECE_VALUES = { + p: 1, // Pawn + n: 3, // Knight + b: 3, // Bishop + r: 5, // Rook + q: 9, // Queen + k: 0, // King +} as const; + +// UI Configuration +export const UI_CONFIG = { + // Toast/Notification durations (ms) + NOTIFICATION_DURATION: 5000, + SHORT_NOTIFICATION: 2000, + LONG_NOTIFICATION: 10000, + + // Animation durations (ms) + TRANSITION_DURATION: 300, + MODAL_FADE_IN: 200, + + // Pagination + ITEMS_PER_PAGE: 20, + GAMES_PER_PAGE: 10, + + // Debounce/Throttle timings (ms) + SEARCH_DEBOUNCE: 300, + RESIZE_THROTTLE: 100, +} as const; + +// React Query Configuration +export const QUERY_CONFIG = { + STALE_TIME: 5 * 60 * 1000, // 5 minutes + CACHE_TIME: 24 * 60 * 60 * 1000, // 24 hours + RETRY_COUNT: 2, + RETRY_DELAY: 1000, +} as const; + +// Lichess API Configuration +export const LICHESS_API = { + BASE_URL: 'https://lichess.org/api', + RATE_LIMIT_DELAY: 1000, // 1 second between requests + MAX_GAMES_PER_REQUEST: 100, + TIMEOUT: 10000, // 10 seconds +} as const; + +// localStorage Keys +export const STORAGE_KEYS = { + CHESS_STORE: 'chess-store', + THEME_PREFERENCE: 'theme-preference', + LANGUAGE_PREFERENCE: 'language-preference', + ANALYSIS_CACHE: 'analysis-cache', + PUZZLE_STATS: 'puzzle-stats', + USER_PREFERENCES: 'user-preferences', +} as const; + +// Puzzle Configuration +export const PUZZLE_CONFIG = { + DAILY_PUZZLE_RESET_HOUR: 0, // Midnight UTC + RUSH_MODE_TIME_LIMIT: 300, // 5 minutes in seconds + BATTLE_MODE_TIME_PER_PUZZLE: 30, // 30 seconds per puzzle + MIN_RATING: 800, + MAX_RATING: 2800, + DEFAULT_PUZZLE_RATING: 1500, +} as const; + +// Tournament Configuration +export const TOURNAMENT_CONFIG = { + MIN_PLAYERS: 2, + MAX_PLAYERS: 32, + ROUND_ROBIN_MAX_PLAYERS: 16, + DEFAULT_TIME_CONTROL: '10+0', // 10 minutes, no increment +} as const; + +// File Upload Configuration +export const UPLOAD_CONFIG = { + MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB + ALLOWED_FILE_TYPES: ['.pgn', '.fen'], + MAX_FILES_PER_UPLOAD: 10, +} as const; + +// Board Themes +export const BOARD_THEMES = [ + 'classic', + 'blue', + 'green', + 'wood', + 'brown', + 'purple', + 'pink', + 'marble', + 'metal', +] as const; + +// UI Themes +export const UI_THEMES = ['light', 'dark', 'system'] as const; + +// Supported Languages +export const LANGUAGES = [ + { code: 'en', name: 'English', flag: '🇬🇧' }, + { code: 'fr', name: 'Français', flag: '🇫🇷' }, + { code: 'es', name: 'Español', flag: '🇪🇸' }, +] as const; + +// Keyboard Shortcuts +export const KEYBOARD_SHORTCUTS = { + NAVIGATE_NEXT: 'ArrowRight', + NAVIGATE_PREV: 'ArrowLeft', + FLIP_BOARD: 'f', + OPEN_HELP: '?', + OPEN_SEARCH: '/', + ESCAPE: 'Escape', + ENTER: 'Enter', + SPACE: ' ', +} as const; + +// Analysis Configuration +export const ANALYSIS_CONFIG = { + CACHE_EXPIRY: 7 * 24 * 60 * 60 * 1000, // 7 days + MAX_CACHE_ENTRIES: 1000, + EVALUATION_THRESHOLD: { + BLUNDER: 300, // centipawns + MISTAKE: 150, + INACCURACY: 50, + }, +} as const; + +// Error Messages +export const ERROR_MESSAGES = { + NETWORK_ERROR: 'Network error. Please check your connection.', + INVALID_PGN: 'Invalid PGN format.', + INVALID_FEN: 'Invalid FEN notation.', + FILE_TOO_LARGE: 'File size exceeds maximum allowed size.', + UNSUPPORTED_FORMAT: 'Unsupported file format.', + STOCKFISH_ERROR: 'Chess engine error. Please try again.', + LICHESS_API_ERROR: 'Failed to fetch from Lichess API.', +} as const; + +// Success Messages +export const SUCCESS_MESSAGES = { + GAME_IMPORTED: 'Game imported successfully!', + GAME_EXPORTED: 'Game exported successfully!', + SETTINGS_SAVED: 'Settings saved!', + PUZZLE_SOLVED: 'Puzzle solved correctly!', + GAME_WON: 'Congratulations! You won!', +} as const; + +// Regular Expressions +export const REGEX_PATTERNS = { + FEN: /^([rnbqkpRNBQKP1-8]+\/){7}[rnbqkpRNBQKP1-8]+\s[bw]\s(-|K?Q?k?q?)\s(-|[a-h][36])\s\d+\s\d+$/, + CHESS_MOVE: /^[a-h][1-8][a-h][1-8][qrbn]?$/, + ECO_CODE: /^[A-E]\d{2}$/, + LICHESS_USERNAME: /^[a-zA-Z0-9_-]{2,20}$/, +} as const; + +// Feature Flags (for gradual rollout of new features) +export const FEATURE_FLAGS = { + ENABLE_AI_HINTS: true, + ENABLE_CLOUD_SYNC: false, + ENABLE_MULTIPLAYER: false, + ENABLE_VOICE_COMMANDS: false, + ENABLE_ADVANCED_STATS: true, +} as const; + +// SEO and Metadata +export const APP_METADATA = { + NAME: 'Chess Master Database', + DESCRIPTION: 'Professional chess analysis and database application', + VERSION: '2.0.0', + AUTHOR: 'Chess Master Team', + KEYWORDS: ['chess', 'database', 'analysis', 'stockfish', 'pgn'], +} as const; diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 0000000..2501d0d --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react'; + +/** + * Custom hook for debouncing values + * Useful for search inputs, API calls, etc. + * @param value - The value to debounce + * @param delay - Delay in milliseconds (default: 500ms) + * @returns The debounced value + */ +export function useDebounce(value: T, delay = 500): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts new file mode 100644 index 0000000..c7263aa --- /dev/null +++ b/src/hooks/useKeyboardShortcuts.ts @@ -0,0 +1,52 @@ +import { useEffect, useCallback } from 'react'; + +export interface KeyboardShortcut { + key: string; + ctrlKey?: boolean; + altKey?: boolean; + shiftKey?: boolean; + metaKey?: boolean; + description: string; + action: () => void; + preventDefault?: boolean; +} + +/** + * Custom hook for managing keyboard shortcuts + * @param shortcuts - Array of keyboard shortcut configurations + * @param enabled - Whether the shortcuts should be active (default: true) + */ +export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[], enabled = true) { + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (!enabled) return; + + for (const shortcut of shortcuts) { + const keyMatches = event.key === shortcut.key; + const ctrlMatches = shortcut.ctrlKey === undefined || event.ctrlKey === shortcut.ctrlKey; + const altMatches = shortcut.altKey === undefined || event.altKey === shortcut.altKey; + const shiftMatches = + shortcut.shiftKey === undefined || event.shiftKey === shortcut.shiftKey; + const metaMatches = shortcut.metaKey === undefined || event.metaKey === shortcut.metaKey; + + if (keyMatches && ctrlMatches && altMatches && shiftMatches && metaMatches) { + if (shortcut.preventDefault) { + event.preventDefault(); + } + shortcut.action(); + break; + } + } + }, + [shortcuts, enabled] + ); + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [handleKeyDown]); + + return shortcuts; +} diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000..3523d25 --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,74 @@ +import { useState, useEffect, useCallback } from 'react'; + +/** + * Custom hook for managing localStorage with React state + * @param key - The localStorage key + * @param initialValue - The initial value if key doesn't exist + * @returns [value, setValue, removeValue] tuple + */ +export function useLocalStorage(key: string, initialValue: T) { + // State to store our value + const [storedValue, setStoredValue] = useState(() => { + if (typeof window === 'undefined') { + return initialValue; + } + + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.error(`Error reading localStorage key "${key}":`, error); + return initialValue; + } + }); + + // Return a wrapped version of useState's setter function that persists to localStorage + const setValue = useCallback( + (value: T | ((val: T) => T)) => { + try { + // Allow value to be a function so we have same API as useState + const valueToStore = value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + + if (typeof window !== 'undefined') { + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } + } catch (error) { + console.error(`Error setting localStorage key "${key}":`, error); + } + }, + [key, storedValue] + ); + + // Function to remove the value from localStorage + const removeValue = useCallback(() => { + try { + setStoredValue(initialValue); + if (typeof window !== 'undefined') { + window.localStorage.removeItem(key); + } + } catch (error) { + console.error(`Error removing localStorage key "${key}":`, error); + } + }, [key, initialValue]); + + // Listen for changes in other tabs/windows + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === key && e.newValue !== null) { + try { + setStoredValue(JSON.parse(e.newValue)); + } catch (error) { + console.error(`Error parsing storage event for key "${key}":`, error); + } + } + }; + + window.addEventListener('storage', handleStorageChange); + return () => { + window.removeEventListener('storage', handleStorageChange); + }; + }, [key]); + + return [storedValue, setValue, removeValue] as const; +} diff --git a/src/hooks/useMediaQuery.ts b/src/hooks/useMediaQuery.ts new file mode 100644 index 0000000..9520469 --- /dev/null +++ b/src/hooks/useMediaQuery.ts @@ -0,0 +1,55 @@ +import { useState, useEffect } from 'react'; + +/** + * Custom hook for responsive design using media queries + * @param query - The media query string (e.g., '(min-width: 768px)') + * @returns boolean indicating if the media query matches + */ +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(() => { + if (typeof window === 'undefined') return false; + return window.matchMedia(query).matches; + }); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const mediaQuery = window.matchMedia(query); + const handleChange = (event: MediaQueryListEvent) => { + setMatches(event.matches); + }; + + // Set initial value + setMatches(mediaQuery.matches); + + // Modern browsers + if (mediaQuery.addEventListener) { + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + } + // Legacy browsers + else { + mediaQuery.addListener(handleChange); + return () => mediaQuery.removeListener(handleChange); + } + }, [query]); + + return matches; +} + +// Convenience hooks for common breakpoints +export function useIsMobile() { + return useMediaQuery('(max-width: 767px)'); +} + +export function useIsTablet() { + return useMediaQuery('(min-width: 768px) and (max-width: 1023px)'); +} + +export function useIsDesktop() { + return useMediaQuery('(min-width: 1024px)'); +} + +export function useIsDarkMode() { + return useMediaQuery('(prefers-color-scheme: dark)'); +} diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..1141eed --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,42 @@ +import { afterEach, vi } from 'vitest'; +import { cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; + +// Cleanup after each test +afterEach(() => { + cleanup(); +}); + +// Mock window.matchMedia +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +// Mock IntersectionObserver +global.IntersectionObserver = class IntersectionObserver { + constructor() {} + disconnect() {} + observe() {} + takeRecords() { + return []; + } + unobserve() {} +} as typeof IntersectionObserver; + +// Mock ResizeObserver +global.ResizeObserver = class ResizeObserver { + constructor() {} + disconnect() {} + observe() {} + unobserve() {} +} as typeof ResizeObserver; diff --git a/src/utils/capturedPieces.test.ts b/src/utils/capturedPieces.test.ts new file mode 100644 index 0000000..c122d57 --- /dev/null +++ b/src/utils/capturedPieces.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect } from 'vitest'; +import { getCapturedPieces } from './capturedPieces'; + +describe('capturedPieces', () => { + const startFen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + + describe('getCapturedPieces', () => { + it('should return empty arrays for starting position', () => { + const captured = getCapturedPieces(startFen, startFen); + expect(captured.white).toEqual([]); + expect(captured.black).toEqual([]); + }); + + it('should detect single black pawn captured by white', () => { + const currentFen = 'rnbqkbnr/ppp1pppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + const captured = getCapturedPieces(startFen, currentFen); + expect(captured.white).toEqual(['p']); + expect(captured.black).toEqual([]); + }); + + it('should detect single white pawn captured by black', () => { + const currentFen = 'rnbqkbnr/pppppppp/8/8/8/8/PPP1PPPP/RNBQKBNR w KQkq - 0 1'; + const captured = getCapturedPieces(startFen, currentFen); + expect(captured.white).toEqual([]); + expect(captured.black).toEqual(['p']); + }); + + it('should detect multiple pawns captured', () => { + const currentFen = 'rnbqkbnr/pp2pppp/8/8/8/8/PPP1PPPP/RNBQKBNR w KQkq - 0 1'; + const captured = getCapturedPieces(startFen, currentFen); + expect(captured.white).toHaveLength(2); + expect(captured.white).toEqual(['p', 'p']); + expect(captured.black).toHaveLength(1); + expect(captured.black).toEqual(['p']); + }); + + it('should detect captured knight', () => { + const currentFen = 'r1bqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + const captured = getCapturedPieces(startFen, currentFen); + expect(captured.white).toEqual(['n']); + expect(captured.black).toEqual([]); + }); + + it('should detect captured bishop', () => { + const currentFen = 'rnbqk1nr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + const captured = getCapturedPieces(startFen, currentFen); + expect(captured.white).toEqual(['b']); + expect(captured.black).toEqual([]); + }); + + it('should detect captured rook', () => { + const currentFen = 'rnbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + const captured = getCapturedPieces(startFen, currentFen); + expect(captured.white).toEqual(['r']); + expect(captured.black).toEqual([]); + }); + + it('should detect captured queen', () => { + const currentFen = 'rnb1kbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + const captured = getCapturedPieces(startFen, currentFen); + expect(captured.white).toEqual(['q']); + expect(captured.black).toEqual([]); + }); + + it('should detect multiple different pieces captured', () => { + const currentFen = 'r1b1kbnr/pp1ppppp/8/8/8/8/PPP1PPPP/RNB1KBNR w KQkq - 0 1'; + const captured = getCapturedPieces(startFen, currentFen); + // White captured: 1 knight, 2 pawns, 1 queen + expect(captured.white).toHaveLength(4); + expect(captured.white).toContain('n'); + expect(captured.white).toContain('p'); + expect(captured.white).toContain('q'); + // Black captured: 1 pawn, 1 queen + expect(captured.black).toHaveLength(2); + expect(captured.black).toContain('p'); + expect(captured.black).toContain('q'); + }); + + it('should handle endgame with many captures', () => { + const currentFen = '4k3/8/8/8/8/8/8/4K3 w - - 0 1'; + const captured = getCapturedPieces(startFen, currentFen); + // All pieces captured except kings + expect(captured.white).toHaveLength(15); // 8 pawns + 2 rooks + 2 knights + 2 bishops + 1 queen + expect(captured.black).toHaveLength(15); + }); + + it('should correctly handle positions with promoted pawns', () => { + // Start with standard position, end with promoted queen + const currentFen = 'rnbqkbnr/ppppppp1/8/8/8/8/PPPPPPPP/RNBQKBNQ w KQkq - 0 1'; + const captured = getCapturedPieces(startFen, currentFen); + // One black pawn is missing (promoted), but we see an extra white queen + expect(captured.white).toEqual(['p']); + expect(captured.black).toEqual([]); + }); + + it('should handle custom starting position', () => { + const customStart = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQ - 0 1'; + const currentFen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R3K3 w Q - 0 1'; + const captured = getCapturedPieces(customStart, currentFen); + expect(captured.white).toEqual([]); + expect(captured.black).toEqual(['r']); + }); + + it('should preserve order based on piece type iteration', () => { + // Multiple pieces of same type should be counted correctly + const currentFen = 'rnbqkb1r/ppppppp1/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + const captured = getCapturedPieces(startFen, currentFen); + expect(captured.white).toEqual(['p', 'n']); + }); + }); +}); diff --git a/src/utils/elo.test.ts b/src/utils/elo.test.ts new file mode 100644 index 0000000..76a2365 --- /dev/null +++ b/src/utils/elo.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from 'vitest'; +import { expectedScore, calculateNewRating, resultToScore } from './elo'; + +describe('elo', () => { + describe('expectedScore', () => { + it('should return 0.5 for equal ratings', () => { + expect(expectedScore(1500, 1500)).toBe(0.5); + }); + + it('should return higher than 0.5 when player rating is higher', () => { + expect(expectedScore(1600, 1500)).toBeGreaterThan(0.5); + }); + + it('should return lower than 0.5 when player rating is lower', () => { + expect(expectedScore(1400, 1500)).toBeLessThan(0.5); + }); + + it('should return approximately 0.76 for 200 point difference', () => { + const result = expectedScore(1700, 1500); + expect(result).toBeCloseTo(0.76, 2); + }); + + it('should return approximately 0.91 for 400 point difference', () => { + const result = expectedScore(1900, 1500); + expect(result).toBeCloseTo(0.91, 2); + }); + }); + + describe('calculateNewRating', () => { + it('should return same rating when result matches expectation', () => { + const newRating = calculateNewRating(1500, 1500, 0.5, 20); + expect(newRating).toBe(1500); + }); + + it('should increase rating when player wins against equal opponent', () => { + const newRating = calculateNewRating(1500, 1500, 1, 20); + expect(newRating).toBeGreaterThan(1500); + expect(newRating).toBe(1510); + }); + + it('should decrease rating when player loses against equal opponent', () => { + const newRating = calculateNewRating(1500, 1500, 0, 20); + expect(newRating).toBeLessThan(1500); + expect(newRating).toBe(1490); + }); + + it('should handle upset victories with larger rating changes', () => { + const newRating = calculateNewRating(1400, 1600, 1, 20); + expect(newRating).toBeGreaterThan(1400); + // Winning against higher rated opponent should give more points + expect(newRating).toBeGreaterThan(1410); + }); + + it('should handle expected victories with smaller rating changes', () => { + const newRating = calculateNewRating(1600, 1400, 1, 20); + expect(newRating).toBeGreaterThan(1600); + // Winning against lower rated opponent should give fewer points + expect(newRating).toBeLessThan(1610); + }); + + it('should respect custom K-factor', () => { + const newRating1 = calculateNewRating(1500, 1500, 1, 10); + const newRating2 = calculateNewRating(1500, 1500, 1, 40); + expect(newRating1).toBe(1505); // K=10: smaller change + expect(newRating2).toBe(1520); // K=40: larger change + }); + + it('should return rounded integer values', () => { + const newRating = calculateNewRating(1500, 1550, 0.5, 32); + expect(Number.isInteger(newRating)).toBe(true); + }); + }); + + describe('resultToScore', () => { + it('should return 1 when white wins and player is white', () => { + expect(resultToScore('1-0', true)).toBe(1); + }); + + it('should return 0 when white wins and player is black', () => { + expect(resultToScore('1-0', false)).toBe(0); + }); + + it('should return 0 when black wins and player is white', () => { + expect(resultToScore('0-1', true)).toBe(0); + }); + + it('should return 1 when black wins and player is black', () => { + expect(resultToScore('0-1', false)).toBe(1); + }); + + it('should return 0.5 for draw regardless of color', () => { + expect(resultToScore('1/2-1/2', true)).toBe(0.5); + expect(resultToScore('1/2-1/2', false)).toBe(0.5); + }); + }); +}); diff --git a/src/utils/elo.ts b/src/utils/elo.ts index 63a0e45..e3b9668 100644 --- a/src/utils/elo.ts +++ b/src/utils/elo.ts @@ -1,7 +1,35 @@ +/** + * Calculate the expected score for a player based on ELO ratings + * Uses the standard ELO formula: E = 1 / (1 + 10^((R_opponent - R_player) / 400)) + * + * @param rating - The player's current ELO rating + * @param opponentRating - The opponent's current ELO rating + * @returns Expected score between 0 and 1 (0.5 means equal chance) + * + * @example + * expectedScore(1500, 1500) // Returns 0.5 (50% expected) + * expectedScore(1600, 1400) // Returns ~0.76 (76% expected to win) + */ export function expectedScore(rating: number, opponentRating: number): number { return 1 / (1 + Math.pow(10, (opponentRating - rating) / 400)); } +/** + * Calculate new ELO rating after a game + * Uses the formula: R_new = R_old + K * (S - E) + * where S is the actual score and E is the expected score + * + * @param rating - Player's current ELO rating + * @param opponentRating - Opponent's current ELO rating + * @param score - Actual game score (1 for win, 0.5 for draw, 0 for loss) + * @param k - K-factor (default: 20) - higher values mean larger rating changes + * @returns New ELO rating (rounded to nearest integer) + * + * @example + * calculateNewRating(1500, 1500, 1) // Returns 1510 (won against equal opponent) + * calculateNewRating(1500, 1500, 0) // Returns 1490 (lost against equal opponent) + * calculateNewRating(1500, 1500, 0.5) // Returns 1500 (draw against equal opponent) + */ export function calculateNewRating( rating: number, opponentRating: number, @@ -12,6 +40,18 @@ export function calculateNewRating( return Math.round(rating + k * (score - exp)); } +/** + * Convert chess game result to numerical score + * + * @param result - Game result in standard chess notation ('1-0', '0-1', or '1/2-1/2') + * @param isWhite - Whether the player was playing as white + * @returns Score value: 1 for win, 0.5 for draw, 0 for loss + * + * @example + * resultToScore('1-0', true) // Returns 1 (white won) + * resultToScore('1-0', false) // Returns 0 (black lost) + * resultToScore('1/2-1/2', true) // Returns 0.5 (draw) + */ export function resultToScore(result: '1-0' | '0-1' | '1/2-1/2', isWhite: boolean): number { if (result === '1/2-1/2') return 0.5; if ((result === '1-0' && isWhite) || (result === '0-1' && !isWhite)) return 1; diff --git a/src/utils/exportFormats.ts b/src/utils/exportFormats.ts new file mode 100644 index 0000000..314be69 --- /dev/null +++ b/src/utils/exportFormats.ts @@ -0,0 +1,258 @@ +import type { ChessGame } from '../data/masterGames'; + +/** + * Export games to JSON format + * @param games - Array of chess games to export + * @param filename - Name of the file to download + */ +export function exportToJSON(games: ChessGame[], filename = 'chess-games.json'): void { + try { + const jsonData = JSON.stringify(games, null, 2); + const blob = new Blob([jsonData], { type: 'application/json' }); + downloadFile(blob, filename); + } catch (error) { + console.error('Error exporting to JSON:', error); + throw new Error('Failed to export games to JSON format'); + } +} + +/** + * Export games to CSV format + * @param games - Array of chess games to export + * @param filename - Name of the file to download + */ +export function exportToCSV(games: ChessGame[], filename = 'chess-games.csv'): void { + try { + if (games.length === 0) { + throw new Error('No games to export'); + } + + // CSV Headers + const headers = [ + 'ID', + 'White', + 'Black', + 'Event', + 'Opening', + 'Result', + 'Year', + 'Moves', + 'Description', + ]; + + // Convert games to CSV rows + const rows = games.map(game => [ + game.id, + escapeCsvValue(game.white), + escapeCsvValue(game.black), + escapeCsvValue(game.event), + escapeCsvValue(game.opening), + game.result, + game.year, + escapeCsvValue(game.moves), + escapeCsvValue(game.description || ''), + ]); + + // Combine headers and rows + const csvContent = [headers.join(','), ...rows.map(row => row.join(','))].join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + downloadFile(blob, filename); + } catch (error) { + console.error('Error exporting to CSV:', error); + throw new Error('Failed to export games to CSV format'); + } +} + +/** + * Escape CSV values containing special characters + * @param value - The value to escape + * @returns Escaped CSV value + */ +function escapeCsvValue(value: string | number): string { + const stringValue = String(value); + + // If value contains comma, quotes, or newlines, wrap in quotes and escape existing quotes + if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) { + return `"${stringValue.replace(/"/g, '""')}"`; + } + + return stringValue; +} + +/** + * Export games to Excel-compatible format (TSV) + * @param games - Array of chess games to export + * @param filename - Name of the file to download + */ +export function exportToExcel(games: ChessGame[], filename = 'chess-games.xlsx'): void { + try { + if (games.length === 0) { + throw new Error('No games to export'); + } + + // Use tab-separated values for better Excel compatibility + const headers = [ + 'ID', + 'White', + 'Black', + 'Event', + 'Opening', + 'Result', + 'Year', + 'Moves', + 'Description', + ]; + + const rows = games.map(game => [ + game.id, + game.white, + game.black, + game.event, + game.opening, + game.result, + game.year, + game.moves, + game.description || '', + ]); + + const tsvContent = [headers.join('\t'), ...rows.map(row => row.join('\t'))].join('\n'); + + // Add BOM for proper UTF-8 encoding in Excel + const BOM = '\uFEFF'; + const blob = new Blob([BOM + tsvContent], { type: 'application/vnd.ms-excel;charset=utf-8;' }); + downloadFile(blob, filename.replace('.xlsx', '.xls')); + } catch (error) { + console.error('Error exporting to Excel:', error); + throw new Error('Failed to export games to Excel format'); + } +} + +/** + * Export a single game to JSON + * @param game - Chess game to export + * @param filename - Name of the file to download + */ +export function exportGameToJSON(game: ChessGame, filename?: string): void { + const name = filename || `chess-game-${game.id}.json`; + exportToJSON([game], name); +} + +/** + * Download a file from a Blob + * @param blob - The blob to download + * @param filename - Name of the file + */ +function downloadFile(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} + +/** + * Parse CSV file and import games + * @param csvContent - The CSV content as string + * @returns Array of parsed games + */ +export function importFromCSV(csvContent: string): ChessGame[] { + const lines = csvContent.split('\n').filter(line => line.trim()); + + if (lines.length < 2) { + throw new Error('CSV file is empty or invalid'); + } + + // Skip header row + const dataLines = lines.slice(1); + + const games: ChessGame[] = dataLines.map((line, index) => { + const values = parseCSVLine(line); + + if (values.length < 8) { + throw new Error(`Invalid CSV format at line ${index + 2}`); + } + + return { + id: parseInt(values[0], 10) || index + 1, + white: values[1], + black: values[2], + event: values[3], + opening: values[4], + result: values[5] as '1-0' | '0-1' | '1/2-1/2', + year: parseInt(values[6], 10), + moves: values[7], + description: values[8] || '', + }; + }); + + return games; +} + +/** + * Parse a single CSV line handling quoted values + * @param line - CSV line to parse + * @returns Array of values + */ +function parseCSVLine(line: string): string[] { + const values: string[] = []; + let currentValue = ''; + let insideQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + const nextChar = line[i + 1]; + + if (char === '"') { + if (insideQuotes && nextChar === '"') { + // Escaped quote + currentValue += '"'; + i++; // Skip next quote + } else { + // Toggle quote state + insideQuotes = !insideQuotes; + } + } else if (char === ',' && !insideQuotes) { + // End of value + values.push(currentValue); + currentValue = ''; + } else { + currentValue += char; + } + } + + // Add last value + values.push(currentValue); + + return values; +} + +/** + * Import games from JSON file + * @param jsonContent - The JSON content as string + * @returns Array of parsed games + */ +export function importFromJSON(jsonContent: string): ChessGame[] { + try { + const games = JSON.parse(jsonContent); + + if (!Array.isArray(games)) { + throw new Error('JSON file must contain an array of games'); + } + + // Validate game structure + games.forEach((game, index) => { + if (!game.white || !game.black || !game.moves) { + throw new Error(`Invalid game structure at index ${index}`); + } + }); + + return games; + } catch (error) { + console.error('Error parsing JSON:', error); + throw new Error('Failed to import games from JSON format'); + } +} diff --git a/src/utils/fenUtils.ts b/src/utils/fenUtils.ts new file mode 100644 index 0000000..e56982c --- /dev/null +++ b/src/utils/fenUtils.ts @@ -0,0 +1,227 @@ +import { Chess } from 'chess.js'; + +/** + * Validate a FEN string + * @param fen - The FEN string to validate + * @returns true if valid, false otherwise + */ +export function isValidFEN(fen: string): boolean { + try { + const chess = new Chess(fen); + return chess.fen() === fen; + } catch { + return false; + } +} + +/** + * Get the starting position FEN + * @returns Standard chess starting position in FEN notation + */ +export function getStartingFEN(): string { + return 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; +} + +/** + * Export current position to FEN string + * @param chess - Chess.js instance + * @returns FEN string of current position + */ +export function exportToFEN(chess: Chess): string { + return chess.fen(); +} + +/** + * Import a position from FEN string + * @param fen - The FEN string to import + * @returns Chess.js instance with the position loaded + * @throws Error if FEN is invalid + */ +export function importFromFEN(fen: string): Chess { + if (!isValidFEN(fen)) { + throw new Error('Invalid FEN notation'); + } + + return new Chess(fen); +} + +/** + * Copy FEN to clipboard + * @param fen - The FEN string to copy + * @returns Promise that resolves when copied + */ +export async function copyFENToClipboard(fen: string): Promise { + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(fen); + } else { + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = fen; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + } + } catch (error) { + console.error('Failed to copy FEN to clipboard:', error); + throw new Error('Failed to copy to clipboard'); + } +} + +/** + * Download FEN as a text file + * @param fen - The FEN string to download + * @param filename - Name of the file (default: position.fen) + */ +export function downloadFEN(fen: string, filename = 'position.fen'): void { + const blob = new Blob([fen], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} + +/** + * Parse FEN to get position information + * @param fen - The FEN string to parse + * @returns Object with position details + */ +export interface FENInfo { + pieces: string; // Piece placement + activeColor: 'w' | 'b'; // Active color + castling: string; // Castling availability + enPassant: string; // En passant target square + halfmove: number; // Halfmove clock + fullmove: number; // Fullmove number +} + +export function parseFEN(fen: string): FENInfo { + if (!isValidFEN(fen)) { + throw new Error('Invalid FEN notation'); + } + + const parts = fen.split(' '); + + return { + pieces: parts[0], + activeColor: parts[1] as 'w' | 'b', + castling: parts[2], + enPassant: parts[3], + halfmove: parseInt(parts[4], 10), + fullmove: parseInt(parts[5], 10), + }; +} + +/** + * Build FEN from components + * @param info - FEN information object + * @returns Complete FEN string + */ +export function buildFEN(info: FENInfo): string { + return `${info.pieces} ${info.activeColor} ${info.castling} ${info.enPassant} ${info.halfmove} ${info.fullmove}`; +} + +/** + * Get a simplified FEN (without move counters) + * Useful for position comparison + * @param fen - The complete FEN string + * @returns Simplified FEN (first 4 fields only) + */ +export function getSimplifiedFEN(fen: string): string { + const parts = fen.split(' '); + return parts.slice(0, 4).join(' '); +} + +/** + * Check if two positions are the same (ignoring move counters) + * @param fen1 - First FEN string + * @param fen2 - Second FEN string + * @returns true if positions are identical + */ +export function arePositionsEqual(fen1: string, fen2: string): boolean { + return getSimplifiedFEN(fen1) === getSimplifiedFEN(fen2); +} + +/** + * Get human-readable description of a position + * @param fen - The FEN string + * @returns Description object + */ +export interface PositionDescription { + toMove: string; + castlingRights: string[]; + enPassantSquare: string | null; + moveNumber: number; +} + +export function describePosition(fen: string): PositionDescription { + const info = parseFEN(fen); + + const castlingRights: string[] = []; + if (info.castling.includes('K')) castlingRights.push('White kingside'); + if (info.castling.includes('Q')) castlingRights.push('White queenside'); + if (info.castling.includes('k')) castlingRights.push('Black kingside'); + if (info.castling.includes('q')) castlingRights.push('Black queenside'); + + return { + toMove: info.activeColor === 'w' ? 'White' : 'Black', + castlingRights: castlingRights.length > 0 ? castlingRights : ['None'], + enPassantSquare: info.enPassant !== '-' ? info.enPassant : null, + moveNumber: info.fullmove, + }; +} + +/** + * Load FEN from a file + * @param file - The file to read + * @returns Promise with the FEN string + */ +export function loadFENFromFile(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = e => { + const content = e.target?.result as string; + const fen = content.trim(); + + if (!isValidFEN(fen)) { + reject(new Error('File does not contain a valid FEN notation')); + return; + } + + resolve(fen); + }; + + reader.onerror = () => { + reject(new Error('Failed to read file')); + }; + + reader.readAsText(file); + }); +} + +/** + * Generate a random legal position + * @returns Chess.js instance with a random position + */ +export function generateRandomPosition(): Chess { + const chess = new Chess(); + const moves = Math.floor(Math.random() * 20) + 5; // 5-25 random moves + + for (let i = 0; i < moves; i++) { + const possibleMoves = chess.moves(); + if (possibleMoves.length === 0) break; + + const randomMove = possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; + chess.move(randomMove); + } + + return chess; +} diff --git a/src/utils/materialEvaluation.test.ts b/src/utils/materialEvaluation.test.ts new file mode 100644 index 0000000..b2f834c --- /dev/null +++ b/src/utils/materialEvaluation.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from 'vitest'; +import { Chess } from 'chess.js'; +import { materialEvaluation } from './materialEvaluation'; + +describe('materialEvaluation', () => { + it('should return 0 for starting position', () => { + const startFen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + expect(materialEvaluation(startFen)).toBe(0); + }); + + it('should work with Chess instance', () => { + const chess = new Chess(); + expect(materialEvaluation(chess)).toBe(0); + }); + + it('should return positive value when white is ahead', () => { + // White has captured a black pawn + const fen = 'rnbqkbnr/ppp1pppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + expect(materialEvaluation(fen)).toBeGreaterThan(0); + }); + + it('should return negative value when black is ahead', () => { + // Black has captured a white pawn + const fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPP1PPPP/RNBQKBNR w KQkq - 0 1'; + expect(materialEvaluation(fen)).toBeLessThan(0); + }); + + it('should correctly evaluate queen vs pawns', () => { + // Only white queen and black pawns + const fen = '4k3/pppppppp/8/8/8/8/8/4K2Q w - - 0 1'; + const evaluation = materialEvaluation(fen); + // Queen is 9 points, 8 pawns is 8 points, so white should be +1 + expect(evaluation).toBe(1); + }); + + it('should correctly evaluate rooks vs knights and bishops', () => { + // White has 2 rooks (10 points), black has 2 knights and 2 bishops (12 points) + const fen = 'rnb1kbnr/8/8/8/8/8/8/R3K2R w - - 0 1'; + const evaluation = materialEvaluation(fen); + // White: 2 rooks = 10, Black: 2n + 2b = 12, difference = -2 + expect(evaluation).toBe(-2); + }); + + it('should give kings value of 0', () => { + // Only kings + const fen = '4k3/8/8/8/8/8/8/4K3 w - - 0 1'; + expect(materialEvaluation(fen)).toBe(0); + }); + + it('should handle empty squares correctly', () => { + // Position with many empty squares + const fen = '4k3/8/8/3Q4/8/8/8/4K3 w - - 0 1'; + const evaluation = materialEvaluation(fen); + expect(evaluation).toBe(9); // Just the white queen + }); + + it('should correctly count piece values', () => { + // Known position: 1 pawn = 1, 1 knight = 3, 1 bishop = 3, 1 rook = 5, 1 queen = 9 + const fen = '4k3/8/8/8/8/8/P7/RNBQK3 w - - 0 1'; + const evaluation = materialEvaluation(fen); + // 1 + 5 + 3 + 9 = 18 + expect(evaluation).toBe(18); + }); + + it('should handle complex middlegame positions', () => { + // After some typical exchanges + const fen = 'r1bqk2r/pppp1ppp/2n2n2/2b1p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R w KQkq - 0 1'; + const evaluation = materialEvaluation(fen); + // Material should be roughly equal + expect(evaluation).toBeGreaterThanOrEqual(-1); + expect(evaluation).toBeLessThanOrEqual(1); + }); + + it('should handle endgame with unbalanced material', () => { + // White has rook and pawn, black has knight and bishop + const fen = '4k3/8/8/8/8/8/P7/R3Knb1 w - - 0 1'; + const evaluation = materialEvaluation(fen); + // White: R(5) + P(1) = 6, Black: N(3) + B(3) = 6 + expect(evaluation).toBe(0); + }); +}); diff --git a/src/utils/materialEvaluation.ts b/src/utils/materialEvaluation.ts index 94b57ac..2505ab5 100644 --- a/src/utils/materialEvaluation.ts +++ b/src/utils/materialEvaluation.ts @@ -1,7 +1,36 @@ import { Chess } from 'chess.js'; +/** + * Standard piece values in pawns + * - Pawn: 1 + * - Knight: 3 + * - Bishop: 3 + * - Rook: 5 + * - Queen: 9 + * - King: 0 (infinite value in gameplay, 0 for material calculation) + */ const VALUE: Record = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 0 }; +/** + * Calculate material evaluation of a chess position + * Counts the total material value difference between white and black + * + * @param input - Either a FEN string or a Chess.js instance + * @returns Material balance in pawns (positive = white ahead, negative = black ahead, 0 = equal) + * + * @example + * // Starting position (equal material) + * materialEvaluation('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1') // Returns 0 + * + * @example + * // White is up a queen (9 points) + * materialEvaluation('4k3/8/8/8/8/8/8/4K2Q w - - 0 1') // Returns 9 + * + * @example + * // Using Chess.js instance + * const chess = new Chess(); + * materialEvaluation(chess) // Returns 0 + */ export function materialEvaluation(input: string | Chess): number { const chess = typeof input === 'string' ? new Chess(input) : input; return chess diff --git a/src/utils/tournament.test.ts b/src/utils/tournament.test.ts new file mode 100644 index 0000000..cc5f4b7 --- /dev/null +++ b/src/utils/tournament.test.ts @@ -0,0 +1,258 @@ +import { describe, it, expect } from 'vitest'; +import { generateRoundRobin, calculateStandings, Tournament } from './tournament'; + +describe('tournament', () => { + describe('generateRoundRobin', () => { + it('should generate correct number of rounds for even number of players', () => { + const players = ['A', 'B', 'C', 'D']; + const rounds = generateRoundRobin(players); + // For n players, there should be n-1 rounds + expect(rounds).toHaveLength(3); + }); + + it('should generate correct number of rounds for odd number of players', () => { + const players = ['A', 'B', 'C', 'D', 'E']; + const rounds = generateRoundRobin(players); + // With BYE added, 6 players -> 5 rounds + expect(rounds).toHaveLength(5); + }); + + it('should ensure each pairing has result field initialized', () => { + const players = ['A', 'B', 'C', 'D']; + const rounds = generateRoundRobin(players); + rounds.forEach(round => { + round.pairings.forEach(pairing => { + expect(pairing.result).toBe(''); + expect(pairing.white).toBeTruthy(); + expect(pairing.black).toBeTruthy(); + }); + }); + }); + + it('should not include BYE in pairings', () => { + const players = ['A', 'B', 'C']; + const rounds = generateRoundRobin(players); + rounds.forEach(round => { + round.pairings.forEach(pairing => { + expect(pairing.white).not.toBe('__BYE__'); + expect(pairing.black).not.toBe('__BYE__'); + }); + }); + }); + + it('should alternate colors correctly', () => { + const players = ['A', 'B', 'C', 'D']; + const rounds = generateRoundRobin(players); + + // Track color assignments for each player + const whiteGames: Record = { A: 0, B: 0, C: 0, D: 0 }; + const blackGames: Record = { A: 0, B: 0, C: 0, D: 0 }; + + rounds.forEach(round => { + round.pairings.forEach(pairing => { + whiteGames[pairing.white]++; + blackGames[pairing.black]++; + }); + }); + + // Each player should play roughly equal games as white and black + players.forEach(player => { + expect(Math.abs(whiteGames[player] - blackGames[player])).toBeLessThanOrEqual(1); + }); + }); + + it('should ensure each player plays every other player exactly once', () => { + const players = ['A', 'B', 'C', 'D']; + const rounds = generateRoundRobin(players); + const matchups: Record> = { + A: new Set(), + B: new Set(), + C: new Set(), + D: new Set(), + }; + + rounds.forEach(round => { + round.pairings.forEach(pairing => { + matchups[pairing.white].add(pairing.black); + matchups[pairing.black].add(pairing.white); + }); + }); + + // Each player should have played against all others + players.forEach(player => { + expect(matchups[player].size).toBe(players.length - 1); + }); + }); + + it('should have correct number of games per round for even players', () => { + const players = ['A', 'B', 'C', 'D']; + const rounds = generateRoundRobin(players); + // With 4 players, each round should have 2 games + rounds.forEach(round => { + expect(round.pairings).toHaveLength(2); + }); + }); + + it('should have correct number of games per round for odd players', () => { + const players = ['A', 'B', 'C', 'D', 'E']; + const rounds = generateRoundRobin(players); + // With 5 players (+ BYE = 6), each round should have 2 games (one player gets BYE) + rounds.forEach(round => { + expect(round.pairings).toHaveLength(2); + }); + }); + + it('should handle 2 players', () => { + const players = ['A', 'B']; + const rounds = generateRoundRobin(players); + expect(rounds).toHaveLength(1); + expect(rounds[0].pairings).toHaveLength(1); + expect(rounds[0].pairings[0].white).toBe('A'); + expect(rounds[0].pairings[0].black).toBe('B'); + }); + + it('should not mutate the original players array', () => { + const players = ['A', 'B', 'C', 'D']; + const originalPlayers = [...players]; + generateRoundRobin(players); + expect(players).toEqual(originalPlayers); + }); + }); + + describe('calculateStandings', () => { + it('should return 0 points for all players with no results', () => { + const tournament: Tournament = { + id: 't1', + name: 'Test Tournament', + players: ['A', 'B', 'C', 'D'], + rounds: generateRoundRobin(['A', 'B', 'C', 'D']), + }; + const standings = calculateStandings(tournament); + expect(standings).toEqual({ A: 0, B: 0, C: 0, D: 0 }); + }); + + it('should correctly calculate wins', () => { + const tournament: Tournament = { + id: 't1', + name: 'Test Tournament', + players: ['A', 'B'], + rounds: [ + { + pairings: [{ white: 'A', black: 'B', result: '1-0' }], + }, + ], + }; + const standings = calculateStandings(tournament); + expect(standings.A).toBe(1); + expect(standings.B).toBe(0); + }); + + it('should correctly calculate losses', () => { + const tournament: Tournament = { + id: 't1', + name: 'Test Tournament', + players: ['A', 'B'], + rounds: [ + { + pairings: [{ white: 'A', black: 'B', result: '0-1' }], + }, + ], + }; + const standings = calculateStandings(tournament); + expect(standings.A).toBe(0); + expect(standings.B).toBe(1); + }); + + it('should correctly calculate draws', () => { + const tournament: Tournament = { + id: 't1', + name: 'Test Tournament', + players: ['A', 'B'], + rounds: [ + { + pairings: [{ white: 'A', black: 'B', result: '1/2-1/2' }], + }, + ], + }; + const standings = calculateStandings(tournament); + expect(standings.A).toBe(0.5); + expect(standings.B).toBe(0.5); + }); + + it('should accumulate points across multiple rounds', () => { + const tournament: Tournament = { + id: 't1', + name: 'Test Tournament', + players: ['A', 'B', 'C'], + rounds: [ + { + pairings: [ + { white: 'A', black: 'B', result: '1-0' }, + { white: 'C', black: 'A', result: '0-1' }, + ], + }, + { + pairings: [{ white: 'B', black: 'C', result: '1/2-1/2' }], + }, + ], + }; + const standings = calculateStandings(tournament); + expect(standings.A).toBe(2); // 2 wins + expect(standings.B).toBe(0.5); // 1 loss, 1 draw + expect(standings.C).toBe(0.5); // 1 loss, 1 draw + }); + + it('should handle empty results', () => { + const tournament: Tournament = { + id: 't1', + name: 'Test Tournament', + players: ['A', 'B', 'C'], + rounds: [ + { + pairings: [ + { white: 'A', black: 'B', result: '' }, + { white: 'B', black: 'C', result: '1-0' }, + ], + }, + ], + }; + const standings = calculateStandings(tournament); + expect(standings.A).toBe(0); + expect(standings.B).toBe(1); + expect(standings.C).toBe(0); + }); + + it('should handle complex tournament with multiple players', () => { + const tournament: Tournament = { + id: 't1', + name: 'Test Tournament', + players: ['A', 'B', 'C', 'D'], + rounds: [ + { + pairings: [ + { white: 'A', black: 'B', result: '1-0' }, + { white: 'C', black: 'D', result: '1/2-1/2' }, + ], + }, + { + pairings: [ + { white: 'A', black: 'C', result: '0-1' }, + { white: 'B', black: 'D', result: '1-0' }, + ], + }, + { + pairings: [ + { white: 'A', black: 'D', result: '1/2-1/2' }, + { white: 'B', black: 'C', result: '0-1' }, + ], + }, + ], + }; + const standings = calculateStandings(tournament); + expect(standings.A).toBe(1.5); // 1W, 1L, 1D + expect(standings.B).toBe(1); // 1W, 1L, 1L + expect(standings.C).toBe(2.5); // 2W, 0L, 1D + expect(standings.D).toBe(1); // 0W, 1L, 2D + }); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..c5badf3 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/test/setup.ts', + css: true, + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'src/test/', + '**/*.spec.ts', + '**/*.test.ts', + '**/*.spec.tsx', + '**/*.test.tsx', + ], + }, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +});