From ea281134b64de91c16b54a0306e9365e141d2dc3 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 23 Aug 2026 17:05:41 +0200 Subject: [PATCH] Archive bucket_catch and poker-stakes to testsAndMisc-archive Two self-contained apps, neither referenced by any other code, systemd unit or symlink -- which is why they go first in the restructure. Full history preserved via git filter-repo and merged into github.com/kuhyx/testsAndMisc-archive at their original repo-relative paths, matching how C/, CPP/, horatio/ and the rest already sit there. Archived commit counts match the monorepo exactly: bucket_catch 10, poker-stakes 2. poker-stakes-ci.yml goes with the app it builds, so no workflow is left pointing at a missing path. bucket_catch had no workflow. Verified before deleting: file counts match (64 and 65), diff -r against the working tree is clean, and the GitHub API lists both units in the archive. Depth violations drop 556 -> 451. billsplit is deliberately NOT part of this commit: it is coupled to python_pkg/billsplit_coverage and an app_icons registry entry that the 100% coverage gate requires be deleted in the same commit as the app. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp --- .github/workflows/poker-stakes-ci.yml | 31 - AGENTS.md | 27 +- bucket_catch/.gitignore | 6 - bucket_catch/package.json | 18 - .../packages/backend/eslint.config.mjs | 26 - bucket_catch/packages/backend/package.json | 29 - .../packages/backend/src/app.module.ts | 9 - .../backend/src/files/files.controller.ts | 38 - .../backend/src/files/files.module.ts | 9 - .../backend/src/files/files.service.ts | 20 - .../packages/backend/src/health.controller.ts | 9 - bucket_catch/packages/backend/src/main.ts | 15 - bucket_catch/packages/backend/tsconfig.json | 23 - .../packages/backend/uploads/.gitkeep | 0 bucket_catch/packages/frontend/.gitignore | 24 - .../packages/frontend/eslint.config.js | 29 - bucket_catch/packages/frontend/index.html | 12 - bucket_catch/packages/frontend/package.json | 37 - .../packages/frontend/src/App.test.tsx | 157 - bucket_catch/packages/frontend/src/App.tsx | 86 - .../src/components/DropZone.module.css | 169 - .../frontend/src/components/DropZone.test.tsx | 229 - .../frontend/src/components/DropZone.tsx | 130 - .../src/components/GameCanvas.module.css | 24 - .../src/components/GameCanvas.test.tsx | 76 - .../frontend/src/components/GameCanvas.tsx | 41 - .../src/components/ModeSelect.module.css | 170 - .../src/components/ModeSelect.test.tsx | 134 - .../frontend/src/components/ModeSelect.tsx | 100 - .../src/components/PuzzleCanvas.module.css | 25 - .../src/components/PuzzleCanvas.test.tsx | 79 - .../frontend/src/components/PuzzleCanvas.tsx | 46 - .../src/components/PuzzleResult.module.css | 88 - .../src/components/PuzzleResult.test.tsx | 180 - .../frontend/src/components/PuzzleResult.tsx | 68 - .../src/components/ScoreScreen.module.css | 157 - .../src/components/ScoreScreen.test.tsx | 208 - .../frontend/src/components/ScoreScreen.tsx | 118 - .../frontend/src/hooks/puzzleGeometry.ts | 127 - .../frontend/src/hooks/puzzleRender.ts | 110 - .../src/hooks/useBasketControl.test.ts | 79 - .../frontend/src/hooks/useBasketControl.ts | 32 - .../frontend/src/hooks/useGameLoop.test.ts | 208 - .../frontend/src/hooks/useGameLoop.ts | 194 - .../src/hooks/usePuzzleGameLoop.test.ts | 125 - .../frontend/src/hooks/usePuzzleGameLoop.ts | 196 - .../src/hooks/usePuzzleGameLoopPlay.test.ts | 222 - bucket_catch/packages/frontend/src/index.css | 85 - .../frontend/src/lib/fileIcon.test.ts | 115 - .../packages/frontend/src/lib/fileIcon.ts | 46 - .../frontend/src/lib/sliceImage.test.ts | 95 - .../packages/frontend/src/lib/sliceImage.ts | 59 - .../frontend/src/lib/uploadFiles.test.ts | 56 - .../packages/frontend/src/lib/uploadFiles.ts | 28 - .../frontend/src/lib/zipDownload.test.ts | 77 - .../packages/frontend/src/lib/zipDownload.ts | 18 - bucket_catch/packages/frontend/src/main.tsx | 12 - .../packages/frontend/src/test/canvasMock.ts | 30 - .../packages/frontend/src/test/domQueries.ts | 21 - .../frontend/src/test/puzzleLoopFixtures.ts | 63 - .../packages/frontend/src/test/setup.ts | 15 - bucket_catch/packages/frontend/src/types.ts | 66 - bucket_catch/packages/frontend/tsconfig.json | 24 - bucket_catch/packages/frontend/vite.config.ts | 28 - bucket_catch/pnpm-lock.yaml | 7260 ----------------- bucket_catch/pnpm-workspace.yaml | 2 - docs/restructure-live-state.md | 10 +- ...-bucket-catch-poker-stakes-2026-08-23.json | 20 + ...-bucket-catch-poker-stakes-2026-08-23.json | 55 + poker-stakes/.gitignore | 3 - poker-stakes/eslint.config.js | 47 - poker-stakes/index.html | 12 - poker-stakes/package-lock.json | 4852 ----------- poker-stakes/package.json | 41 - poker-stakes/src/App.test.tsx | 196 - poker-stakes/src/App.tsx | 58 - poker-stakes/src/engine/bettingRound.test.ts | 244 - poker-stakes/src/engine/bettingRound.ts | 139 - poker-stakes/src/engine/deck.test.ts | 68 - poker-stakes/src/engine/deck.ts | 52 - .../src/engine/handEval.compare.test.ts | 67 - .../src/engine/handEval.rank5.test.ts | 174 - poker-stakes/src/engine/handEval.ts | 182 - poker-stakes/src/engine/handFlow.test.ts | 199 - poker-stakes/src/engine/handFlow.ts | 182 - poker-stakes/src/engine/handFlowBetting.ts | 89 - .../src/engine/interactiveHand.test.ts | 110 - poker-stakes/src/engine/interactiveHand.ts | 64 - poker-stakes/src/engine/invariants.test.ts | 93 - .../src/engine/opponentPolicy.test.ts | 141 - poker-stakes/src/engine/opponentPolicy.ts | 105 - poker-stakes/src/engine/rng.test.ts | 55 - poker-stakes/src/engine/rng.ts | 33 - poker-stakes/src/engine/session.test.ts | 121 - poker-stakes/src/engine/session.ts | 91 - poker-stakes/src/engine/types.ts | 13 - poker-stakes/src/main.test.tsx | 27 - poker-stakes/src/main.tsx | 15 - poker-stakes/src/stakes/settlement.test.ts | 34 - poker-stakes/src/stakes/settlement.ts | 16 - poker-stakes/src/stakes/storage.test.ts | 119 - poker-stakes/src/stakes/storage.ts | 48 - poker-stakes/src/stakes/taskTypes.test.ts | 20 - poker-stakes/src/stakes/taskTypes.ts | 7 - poker-stakes/src/stakes/types.ts | 20 - poker-stakes/src/styles.css | 215 - poker-stakes/src/test/handEvalFixtures.ts | 37 - poker-stakes/src/test/setup.ts | 7 - poker-stakes/src/test/useGameFixtures.tsx | 65 - poker-stakes/src/ui/BettingControls.test.tsx | 63 - poker-stakes/src/ui/BettingControls.tsx | 45 - poker-stakes/src/ui/BuyInScreen.test.tsx | 139 - poker-stakes/src/ui/BuyInScreen.tsx | 91 - poker-stakes/src/ui/Card.test.tsx | 46 - poker-stakes/src/ui/Card.tsx | 47 - poker-stakes/src/ui/CashOutPanel.test.tsx | 54 - poker-stakes/src/ui/CashOutPanel.tsx | 33 - poker-stakes/src/ui/DebtHistoryPanel.test.tsx | 74 - poker-stakes/src/ui/DebtHistoryPanel.tsx | 63 - poker-stakes/src/ui/HandLog.test.tsx | 37 - poker-stakes/src/ui/HandLog.tsx | 24 - .../src/ui/OutstandingDebtRow.test.tsx | 64 - poker-stakes/src/ui/OutstandingDebtRow.tsx | 50 - poker-stakes/src/ui/Table.test.tsx | 115 - poker-stakes/src/ui/Table.tsx | 66 - poker-stakes/src/ui/gameState.ts | 63 - poker-stakes/src/ui/taskLabel.test.ts | 12 - poker-stakes/src/ui/taskLabel.ts | 5 - poker-stakes/src/ui/useGame.debt.test.tsx | 156 - poker-stakes/src/ui/useGame.reducer.test.tsx | 75 - poker-stakes/src/ui/useGame.session.test.tsx | 219 - poker-stakes/src/ui/useGame.ts | 212 - poker-stakes/tsconfig.json | 24 - poker-stakes/vite.config.ts | 18 - 134 files changed, 93 insertions(+), 21888 deletions(-) delete mode 100644 .github/workflows/poker-stakes-ci.yml delete mode 100644 bucket_catch/.gitignore delete mode 100644 bucket_catch/package.json delete mode 100644 bucket_catch/packages/backend/eslint.config.mjs delete mode 100644 bucket_catch/packages/backend/package.json delete mode 100644 bucket_catch/packages/backend/src/app.module.ts delete mode 100644 bucket_catch/packages/backend/src/files/files.controller.ts delete mode 100644 bucket_catch/packages/backend/src/files/files.module.ts delete mode 100644 bucket_catch/packages/backend/src/files/files.service.ts delete mode 100644 bucket_catch/packages/backend/src/health.controller.ts delete mode 100644 bucket_catch/packages/backend/src/main.ts delete mode 100644 bucket_catch/packages/backend/tsconfig.json delete mode 100644 bucket_catch/packages/backend/uploads/.gitkeep delete mode 100644 bucket_catch/packages/frontend/.gitignore delete mode 100644 bucket_catch/packages/frontend/eslint.config.js delete mode 100644 bucket_catch/packages/frontend/index.html delete mode 100644 bucket_catch/packages/frontend/package.json delete mode 100644 bucket_catch/packages/frontend/src/App.test.tsx delete mode 100644 bucket_catch/packages/frontend/src/App.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/DropZone.module.css delete mode 100644 bucket_catch/packages/frontend/src/components/DropZone.test.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/DropZone.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/GameCanvas.module.css delete mode 100644 bucket_catch/packages/frontend/src/components/GameCanvas.test.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/GameCanvas.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/ModeSelect.module.css delete mode 100644 bucket_catch/packages/frontend/src/components/ModeSelect.test.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/ModeSelect.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/PuzzleCanvas.module.css delete mode 100644 bucket_catch/packages/frontend/src/components/PuzzleCanvas.test.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/PuzzleCanvas.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/PuzzleResult.module.css delete mode 100644 bucket_catch/packages/frontend/src/components/PuzzleResult.test.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/PuzzleResult.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/ScoreScreen.module.css delete mode 100644 bucket_catch/packages/frontend/src/components/ScoreScreen.test.tsx delete mode 100644 bucket_catch/packages/frontend/src/components/ScoreScreen.tsx delete mode 100644 bucket_catch/packages/frontend/src/hooks/puzzleGeometry.ts delete mode 100644 bucket_catch/packages/frontend/src/hooks/puzzleRender.ts delete mode 100644 bucket_catch/packages/frontend/src/hooks/useBasketControl.test.ts delete mode 100644 bucket_catch/packages/frontend/src/hooks/useBasketControl.ts delete mode 100644 bucket_catch/packages/frontend/src/hooks/useGameLoop.test.ts delete mode 100644 bucket_catch/packages/frontend/src/hooks/useGameLoop.ts delete mode 100644 bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoop.test.ts delete mode 100644 bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoop.ts delete mode 100644 bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoopPlay.test.ts delete mode 100644 bucket_catch/packages/frontend/src/index.css delete mode 100644 bucket_catch/packages/frontend/src/lib/fileIcon.test.ts delete mode 100644 bucket_catch/packages/frontend/src/lib/fileIcon.ts delete mode 100644 bucket_catch/packages/frontend/src/lib/sliceImage.test.ts delete mode 100644 bucket_catch/packages/frontend/src/lib/sliceImage.ts delete mode 100644 bucket_catch/packages/frontend/src/lib/uploadFiles.test.ts delete mode 100644 bucket_catch/packages/frontend/src/lib/uploadFiles.ts delete mode 100644 bucket_catch/packages/frontend/src/lib/zipDownload.test.ts delete mode 100644 bucket_catch/packages/frontend/src/lib/zipDownload.ts delete mode 100644 bucket_catch/packages/frontend/src/main.tsx delete mode 100644 bucket_catch/packages/frontend/src/test/canvasMock.ts delete mode 100644 bucket_catch/packages/frontend/src/test/domQueries.ts delete mode 100644 bucket_catch/packages/frontend/src/test/puzzleLoopFixtures.ts delete mode 100644 bucket_catch/packages/frontend/src/test/setup.ts delete mode 100644 bucket_catch/packages/frontend/src/types.ts delete mode 100644 bucket_catch/packages/frontend/tsconfig.json delete mode 100644 bucket_catch/packages/frontend/vite.config.ts delete mode 100644 bucket_catch/pnpm-lock.yaml delete mode 100644 bucket_catch/pnpm-workspace.yaml create mode 100644 docs/superpowers/contracts/archive-bucket-catch-poker-stakes-2026-08-23.json create mode 100644 docs/superpowers/evidence/archive-bucket-catch-poker-stakes-2026-08-23.json delete mode 100644 poker-stakes/.gitignore delete mode 100644 poker-stakes/eslint.config.js delete mode 100644 poker-stakes/index.html delete mode 100644 poker-stakes/package-lock.json delete mode 100644 poker-stakes/package.json delete mode 100644 poker-stakes/src/App.test.tsx delete mode 100644 poker-stakes/src/App.tsx delete mode 100644 poker-stakes/src/engine/bettingRound.test.ts delete mode 100644 poker-stakes/src/engine/bettingRound.ts delete mode 100644 poker-stakes/src/engine/deck.test.ts delete mode 100644 poker-stakes/src/engine/deck.ts delete mode 100644 poker-stakes/src/engine/handEval.compare.test.ts delete mode 100644 poker-stakes/src/engine/handEval.rank5.test.ts delete mode 100644 poker-stakes/src/engine/handEval.ts delete mode 100644 poker-stakes/src/engine/handFlow.test.ts delete mode 100644 poker-stakes/src/engine/handFlow.ts delete mode 100644 poker-stakes/src/engine/handFlowBetting.ts delete mode 100644 poker-stakes/src/engine/interactiveHand.test.ts delete mode 100644 poker-stakes/src/engine/interactiveHand.ts delete mode 100644 poker-stakes/src/engine/invariants.test.ts delete mode 100644 poker-stakes/src/engine/opponentPolicy.test.ts delete mode 100644 poker-stakes/src/engine/opponentPolicy.ts delete mode 100644 poker-stakes/src/engine/rng.test.ts delete mode 100644 poker-stakes/src/engine/rng.ts delete mode 100644 poker-stakes/src/engine/session.test.ts delete mode 100644 poker-stakes/src/engine/session.ts delete mode 100644 poker-stakes/src/engine/types.ts delete mode 100644 poker-stakes/src/main.test.tsx delete mode 100644 poker-stakes/src/main.tsx delete mode 100644 poker-stakes/src/stakes/settlement.test.ts delete mode 100644 poker-stakes/src/stakes/settlement.ts delete mode 100644 poker-stakes/src/stakes/storage.test.ts delete mode 100644 poker-stakes/src/stakes/storage.ts delete mode 100644 poker-stakes/src/stakes/taskTypes.test.ts delete mode 100644 poker-stakes/src/stakes/taskTypes.ts delete mode 100644 poker-stakes/src/stakes/types.ts delete mode 100644 poker-stakes/src/styles.css delete mode 100644 poker-stakes/src/test/handEvalFixtures.ts delete mode 100644 poker-stakes/src/test/setup.ts delete mode 100644 poker-stakes/src/test/useGameFixtures.tsx delete mode 100644 poker-stakes/src/ui/BettingControls.test.tsx delete mode 100644 poker-stakes/src/ui/BettingControls.tsx delete mode 100644 poker-stakes/src/ui/BuyInScreen.test.tsx delete mode 100644 poker-stakes/src/ui/BuyInScreen.tsx delete mode 100644 poker-stakes/src/ui/Card.test.tsx delete mode 100644 poker-stakes/src/ui/Card.tsx delete mode 100644 poker-stakes/src/ui/CashOutPanel.test.tsx delete mode 100644 poker-stakes/src/ui/CashOutPanel.tsx delete mode 100644 poker-stakes/src/ui/DebtHistoryPanel.test.tsx delete mode 100644 poker-stakes/src/ui/DebtHistoryPanel.tsx delete mode 100644 poker-stakes/src/ui/HandLog.test.tsx delete mode 100644 poker-stakes/src/ui/HandLog.tsx delete mode 100644 poker-stakes/src/ui/OutstandingDebtRow.test.tsx delete mode 100644 poker-stakes/src/ui/OutstandingDebtRow.tsx delete mode 100644 poker-stakes/src/ui/Table.test.tsx delete mode 100644 poker-stakes/src/ui/Table.tsx delete mode 100644 poker-stakes/src/ui/gameState.ts delete mode 100644 poker-stakes/src/ui/taskLabel.test.ts delete mode 100644 poker-stakes/src/ui/taskLabel.ts delete mode 100644 poker-stakes/src/ui/useGame.debt.test.tsx delete mode 100644 poker-stakes/src/ui/useGame.reducer.test.tsx delete mode 100644 poker-stakes/src/ui/useGame.session.test.tsx delete mode 100644 poker-stakes/src/ui/useGame.ts delete mode 100644 poker-stakes/tsconfig.json delete mode 100644 poker-stakes/vite.config.ts diff --git a/.github/workflows/poker-stakes-ci.yml b/.github/workflows/poker-stakes-ci.yml deleted file mode 100644 index d69c9ecc..00000000 --- a/.github/workflows/poker-stakes-ci.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: poker-stakes CI - -on: - push: - branches: [main] - paths: - - "poker-stakes/**" - - ".github/workflows/poker-stakes-ci.yml" - pull_request: - branches: [main] - paths: - - "poker-stakes/**" - - ".github/workflows/poker-stakes-ci.yml" - -defaults: - run: - working-directory: poker-stakes - -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - cache-dependency-path: poker-stakes/package-lock.json - - run: npm ci - - run: npm run check - - run: npm run build diff --git a/AGENTS.md b/AGENTS.md index c6e3e6a1..576f15e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,20 +4,19 @@ Mixed-language monorepo: Python packages, Bash automation, and several apps. ## Repository Layout -| Path | Description | -| --------------------------- | --------------------------------------------------------------------------------------------------------- | -| `python_pkg/` | Python packages — every `.py` in the repo belongs here or in an allowlisted dir | -| `meta/` | Repo-wide tooling: `pyproject.toml`, `requirements.txt`, `run.sh`, `lint_python.sh`, `.fvmrc`, `scripts/` | -| `linux_configuration/` | Arch setup, i3 config, system maintenance, and the tracked git hooks | -| `phone_focus_mode/` | GPS-based Android focus enforcer (Bash, ADB, Magisk) | -| `billsplit/` | Receipt bill-splitting Flutter app (Dart) | -| `reverse_survivors/` | React/Vite game — you play the horde (TypeScript) | -| `poker-stakes/` | Poker stakes trainer (TypeScript, Vite) | -| `kcd2_dice_solver/` | KCD2 dice solver | -| `focus_owner/` | Android Device-Owner focus enforcer (Kotlin) | -| `bucket_catch/`, `bottles/` | Small standalone projects | -| `docs/` | Reference docs; `docs/superpowers/` holds AI workflow artifacts | -| `third_party/` | Vendored upstream skills/agents | +| Path | Description | +| ---------------------- | --------------------------------------------------------------------------------------------------------- | +| `python_pkg/` | Python packages — every `.py` in the repo belongs here or in an allowlisted dir | +| `meta/` | Repo-wide tooling: `pyproject.toml`, `requirements.txt`, `run.sh`, `lint_python.sh`, `.fvmrc`, `scripts/` | +| `linux_configuration/` | Arch setup, i3 config, system maintenance, and the tracked git hooks | +| `phone_focus_mode/` | GPS-based Android focus enforcer (Bash, ADB, Magisk) | +| `billsplit/` | Receipt bill-splitting Flutter app (Dart) | +| `reverse_survivors/` | React/Vite game — you play the horde (TypeScript) | +| `kcd2_dice_solver/` | KCD2 dice solver | +| `focus_owner/` | Android Device-Owner focus enforcer (Kotlin) | +| `bottles/` | Small standalone project | +| `docs/` | Reference docs; `docs/superpowers/` holds AI workflow artifacts | +| `third_party/` | Vendored upstream skills/agents | Root `pyproject.toml`, `requirements.txt`, `run.sh`, `lint_python.sh` and `.fvmrc` are **symlinks into `meta/`** — edit the files in `meta/`, not the diff --git a/bucket_catch/.gitignore b/bucket_catch/.gitignore deleted file mode 100644 index e77740e5..00000000 --- a/bucket_catch/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -packages/*/node_modules/ -packages/frontend/dist/ -packages/backend/dist/ -packages/backend/uploads/* -!packages/backend/uploads/.gitkeep diff --git a/bucket_catch/package.json b/bucket_catch/package.json deleted file mode 100644 index ab747785..00000000 --- a/bucket_catch/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "bucket-catch", - "private": true, - "scripts": { - "dev": "pnpm --parallel -r dev", - "build": "pnpm -r build", - "lint": "pnpm -r lint" - }, - "engines": { - "node": ">=22", - "pnpm": ">=10" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "esbuild" - ] - } -} diff --git a/bucket_catch/packages/backend/eslint.config.mjs b/bucket_catch/packages/backend/eslint.config.mjs deleted file mode 100644 index ab12726a..00000000 --- a/bucket_catch/packages/backend/eslint.config.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import js from '@eslint/js'; -import tseslint from 'typescript-eslint'; - -export default tseslint.config( - { ignores: ['dist', 'coverage'] }, - js.configs.recommended, - ...tseslint.configs.strictTypeChecked, - ...tseslint.configs.stylisticTypeChecked, - { - languageOptions: { - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: import.meta.dirname, - }, - }, - rules: { - '@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }], - // NestJS modules/controllers are intentionally empty classes decorated with @Module/@Controller - '@typescript-eslint/no-extraneous-class': ['error', { allowWithDecorator: true }], - }, - }, - { - files: ['*.js', '*.mjs', '*.cjs'], - ...tseslint.configs.disableTypeChecked, - }, -); diff --git a/bucket_catch/packages/backend/package.json b/bucket_catch/packages/backend/package.json deleted file mode 100644 index 1950bebc..00000000 --- a/bucket_catch/packages/backend/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "bucket-catch-backend", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "ts-node-dev --respawn --transpile-only src/main.ts", - "build": "tsc", - "start": "node dist/main.js", - "lint": "eslint src" - }, - "dependencies": { - "@nestjs/common": "^11.0.0", - "@nestjs/core": "^11.0.0", - "@nestjs/platform-express": "^11.0.0", - "multer": "^1.4.5-lts.1", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" - }, - "devDependencies": { - "@eslint/js": "^10.0.1", - "@types/express": "^5.0.0", - "@types/multer": "^1.4.12", - "@types/node": "^22.0.0", - "eslint": "^10.6.0", - "ts-node-dev": "^2.0.0", - "typescript": "^5.7.0", - "typescript-eslint": "^8.62.0" - } -} diff --git a/bucket_catch/packages/backend/src/app.module.ts b/bucket_catch/packages/backend/src/app.module.ts deleted file mode 100644 index 7ff3da10..00000000 --- a/bucket_catch/packages/backend/src/app.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from "@nestjs/common"; -import { FilesModule } from "./files/files.module"; -import { HealthController } from "./health.controller"; - -@Module({ - imports: [FilesModule], - controllers: [HealthController], -}) -export class AppModule {} diff --git a/bucket_catch/packages/backend/src/files/files.controller.ts b/bucket_catch/packages/backend/src/files/files.controller.ts deleted file mode 100644 index 75d6f590..00000000 --- a/bucket_catch/packages/backend/src/files/files.controller.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - Controller, - Post, - UploadedFile, - UseInterceptors, - BadRequestException, -} from "@nestjs/common"; -import { FileInterceptor } from "@nestjs/platform-express"; -import { diskStorage } from "multer"; -import { extname } from "path"; -import { FilesService, UploadResult } from "./files.service"; - -@Controller("files") -export class FilesController { - constructor(private readonly filesService: FilesService) {} - - @Post("upload") - @UseInterceptors( - FileInterceptor("file", { - storage: diskStorage({ - destination: "./uploads", - filename: (_req, file, cb) => { - const uniqueSuffix = `${Date.now().toString()}-${Math.round(Math.random() * 1e9).toString()}`; - cb(null, uniqueSuffix + extname(file.originalname)); - }, - }), - limits: { fileSize: 100 * 1024 * 1024 }, // 100 MB - }), - ) - uploadFile( - @UploadedFile() file: Express.Multer.File | undefined, - ): UploadResult { - if (!file) { - throw new BadRequestException("No file provided"); - } - return this.filesService.processUpload(file); - } -} diff --git a/bucket_catch/packages/backend/src/files/files.module.ts b/bucket_catch/packages/backend/src/files/files.module.ts deleted file mode 100644 index 39b548bf..00000000 --- a/bucket_catch/packages/backend/src/files/files.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from "@nestjs/common"; -import { FilesController } from "./files.controller"; -import { FilesService } from "./files.service"; - -@Module({ - controllers: [FilesController], - providers: [FilesService], -}) -export class FilesModule {} diff --git a/bucket_catch/packages/backend/src/files/files.service.ts b/bucket_catch/packages/backend/src/files/files.service.ts deleted file mode 100644 index 1690cdc1..00000000 --- a/bucket_catch/packages/backend/src/files/files.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Injectable } from "@nestjs/common"; - -export interface UploadResult { - filename: string; - originalname: string; - size: number; - savedAt: string; -} - -@Injectable() -export class FilesService { - processUpload(file: Express.Multer.File): UploadResult { - return { - filename: file.filename, - originalname: file.originalname, - size: file.size, - savedAt: new Date().toISOString(), - }; - } -} diff --git a/bucket_catch/packages/backend/src/health.controller.ts b/bucket_catch/packages/backend/src/health.controller.ts deleted file mode 100644 index 252d87a1..00000000 --- a/bucket_catch/packages/backend/src/health.controller.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Controller, Get } from "@nestjs/common"; - -@Controller("health") -export class HealthController { - @Get() - check(): { status: string } { - return { status: "ok" }; - } -} diff --git a/bucket_catch/packages/backend/src/main.ts b/bucket_catch/packages/backend/src/main.ts deleted file mode 100644 index a23946d8..00000000 --- a/bucket_catch/packages/backend/src/main.ts +++ /dev/null @@ -1,15 +0,0 @@ -import "reflect-metadata"; -import { NestFactory } from "@nestjs/core"; -import { AppModule } from "./app.module"; - -async function bootstrap(): Promise { - const app = await NestFactory.create(AppModule); - app.enableCors({ - origin: ["http://localhost:5173", "http://127.0.0.1:5173"], - methods: ["GET", "POST", "OPTIONS"], - }); - await app.listen(3000); - console.log("Backend running on http://localhost:3000"); -} - -void bootstrap(); diff --git a/bucket_catch/packages/backend/tsconfig.json b/bucket_catch/packages/backend/tsconfig.json deleted file mode 100644 index 349c1a6d..00000000 --- a/bucket_catch/packages/backend/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2022", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/bucket_catch/packages/backend/uploads/.gitkeep b/bucket_catch/packages/backend/uploads/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/bucket_catch/packages/frontend/.gitignore b/bucket_catch/packages/frontend/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/bucket_catch/packages/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/bucket_catch/packages/frontend/eslint.config.js b/bucket_catch/packages/frontend/eslint.config.js deleted file mode 100644 index 468b001d..00000000 --- a/bucket_catch/packages/frontend/eslint.config.js +++ /dev/null @@ -1,29 +0,0 @@ -import js from '@eslint/js'; -import reactHooksPlugin from 'eslint-plugin-react-hooks'; -import tseslint from 'typescript-eslint'; - -export default tseslint.config( - { ignores: ['dist', 'coverage'] }, - js.configs.recommended, - ...tseslint.configs.strictTypeChecked, - ...tseslint.configs.stylisticTypeChecked, - { - plugins: { - 'react-hooks': reactHooksPlugin, - }, - languageOptions: { - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: import.meta.dirname, - }, - }, - rules: { - ...reactHooksPlugin.configs.recommended.rules, - '@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }], - }, - }, - { - files: ['*.js', '*.mjs', '*.cjs'], - ...tseslint.configs.disableTypeChecked, - }, -); diff --git a/bucket_catch/packages/frontend/index.html b/bucket_catch/packages/frontend/index.html deleted file mode 100644 index 8b7d54d6..00000000 --- a/bucket_catch/packages/frontend/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Bucket Catch - - -
- - - diff --git a/bucket_catch/packages/frontend/package.json b/bucket_catch/packages/frontend/package.json deleted file mode 100644 index e49464d3..00000000 --- a/bucket_catch/packages/frontend/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "bucket-catch-frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "tsc --noEmit && eslint src", - "preview": "vite preview", - "test": "vitest run", - "coverage": "vitest run --coverage" - }, - "dependencies": { - "jszip": "^3.10.1", - "react": "^19.1.0", - "react-dom": "^19.1.0" - }, - "devDependencies": { - "@eslint/js": "^10.0.1", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.1", - "@types/react": "^19.1.0", - "@types/react-dom": "^19.1.0", - "@vitejs/plugin-react": "^4.5.2", - "@vitest/coverage-v8": "^4.1.9", - "eslint": "^10.6.0", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.1.1", - "jsdom": "^29.1.1", - "typescript": "~5.8.3", - "typescript-eslint": "^8.62.0", - "vite": "^6.3.5", - "vitest": "^4.1.9" - } -} diff --git a/bucket_catch/packages/frontend/src/App.test.tsx b/bucket_catch/packages/frontend/src/App.test.tsx deleted file mode 100644 index 6af3060b..00000000 --- a/bucket_catch/packages/frontend/src/App.test.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { render, screen, fireEvent } from "@testing-library/react"; -import type { FileGameResult, PuzzleGameResult, TransferMode } from "./types"; -import App from "./App"; - -// ---- Minimal component mocks ------------------------------------------------ - -vi.mock("./components/DropZone", () => ({ - DropZone: ({ - onFiles, - onPuzzle, - }: { - onFiles: (f: File[]) => void; - onPuzzle: (f: File, g: number) => void; - }) => ( -
- - -
- ), -})); - -vi.mock("./components/ModeSelect", () => ({ - ModeSelect: ({ - onStart, - }: { - onStart: (m: TransferMode, g?: number) => void; - }) => ( -
- - - -
- ), -})); - -vi.mock("./components/GameCanvas", () => ({ - GameCanvas: ({ onDone }: { onDone: (r: FileGameResult) => void }) => ( - - ), -})); - -vi.mock("./components/PuzzleCanvas", () => ({ - PuzzleCanvas: ({ - onDone, - }: { - onDone: (r: PuzzleGameResult) => void; - }) => ( - - ), -})); - -vi.mock("./components/ScoreScreen", () => ({ - ScoreScreen: ({ onRestart }: { onRestart: () => void }) => ( - - ), -})); - -vi.mock("./components/PuzzleResult", () => ({ - PuzzleResult: ({ onRestart }: { onRestart: () => void }) => ( - - ), -})); - -// ---- Tests ------------------------------------------------------------------ - -describe("App", () => { - it("initially renders DropZone (drop phase)", () => { - render(); - expect(screen.getByTestId("dropzone")).toBeInTheDocument(); - }); - - it("handleFiles transitions from drop to mode phase", () => { - render(); - fireEvent.click(screen.getByText("drop-files")); - expect(screen.getByTestId("modeselect")).toBeInTheDocument(); - }); - - it("handlePuzzleDirect skips mode phase and goes straight to PuzzleCanvas", () => { - render(); - fireEvent.click(screen.getByText("drop-puzzle")); - expect(screen.getByText("puzzle-done")).toBeInTheDocument(); - }); - - it("handleStart('download') transitions from mode to GameCanvas", () => { - render(); - fireEvent.click(screen.getByText("drop-files")); - fireEvent.click(screen.getByText("mode-download")); - expect(screen.getByText("game-done")).toBeInTheDocument(); - }); - - it("handleStart('upload') transitions from mode to GameCanvas", () => { - render(); - fireEvent.click(screen.getByText("drop-files")); - fireEvent.click(screen.getByText("mode-upload")); - expect(screen.getByText("game-done")).toBeInTheDocument(); - }); - - it("handleStart('puzzle', gridSize) sets puzzleGridSize and shows PuzzleCanvas", () => { - render(); - fireEvent.click(screen.getByText("drop-files")); - fireEvent.click(screen.getByText("mode-puzzle")); - expect(screen.getByText("puzzle-done")).toBeInTheDocument(); - }); - - it("handleFileDone transitions to done phase showing ScoreScreen", () => { - render(); - fireEvent.click(screen.getByText("drop-files")); - fireEvent.click(screen.getByText("mode-download")); - fireEvent.click(screen.getByText("game-done")); - expect(screen.getByText("score-restart")).toBeInTheDocument(); - }); - - it("handlePuzzleDone transitions to done phase showing PuzzleResult", () => { - render(); - fireEvent.click(screen.getByText("drop-puzzle")); - fireEvent.click(screen.getByText("puzzle-done")); - expect(screen.getByText("puzzle-restart")).toBeInTheDocument(); - }); - - it("handleRestart from ScoreScreen resets to drop phase", () => { - render(); - fireEvent.click(screen.getByText("drop-files")); - fireEvent.click(screen.getByText("mode-download")); - fireEvent.click(screen.getByText("game-done")); - fireEvent.click(screen.getByText("score-restart")); - expect(screen.getByTestId("dropzone")).toBeInTheDocument(); - }); - - it("handleRestart from PuzzleResult resets to drop phase", () => { - render(); - fireEvent.click(screen.getByText("drop-puzzle")); - fireEvent.click(screen.getByText("puzzle-done")); - fireEvent.click(screen.getByText("puzzle-restart")); - expect(screen.getByTestId("dropzone")).toBeInTheDocument(); - }); -}); diff --git a/bucket_catch/packages/frontend/src/App.tsx b/bucket_catch/packages/frontend/src/App.tsx deleted file mode 100644 index f9a356fa..00000000 --- a/bucket_catch/packages/frontend/src/App.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useCallback, useState } from "react"; -import type { - GamePhase, - FileGameResult, - PuzzleGameResult, - TransferMode, -} from "./types"; -import { DropZone } from "./components/DropZone"; -import { ModeSelect } from "./components/ModeSelect"; -import { GameCanvas } from "./components/GameCanvas"; -import { PuzzleCanvas } from "./components/PuzzleCanvas"; -import { ScoreScreen } from "./components/ScoreScreen"; -import { PuzzleResult } from "./components/PuzzleResult"; - -export default function App(): React.ReactElement { - const [phase, setPhase] = useState({ kind: "drop" }); - const [files, setFiles] = useState([]); - const [mode, setMode] = useState("download"); - const [puzzleGridSize, setPuzzleGridSize] = useState(4); - - const handleFiles = useCallback((incoming: File[]) => { - setFiles(incoming); - setPhase({ kind: "mode" }); - }, []); - - const handlePuzzleDirect = useCallback( - (imageFile: File, gridSize: number) => { - setFiles([imageFile]); - setMode("puzzle"); - setPuzzleGridSize(gridSize); - setPhase({ kind: "playing" }); - }, - [], - ); - - const handleStart = useCallback((selected: TransferMode, gridSize = 4) => { - setMode(selected); - if (selected === "puzzle") setPuzzleGridSize(gridSize); - setPhase({ kind: "playing" }); - }, []); - - const handleFileDone = useCallback((value: FileGameResult) => { - setPhase({ kind: "done", result: { kind: "file", value } }); - }, []); - - const handlePuzzleDone = useCallback((value: PuzzleGameResult) => { - setPhase({ kind: "done", result: { kind: "puzzle", value } }); - }, []); - - const handleRestart = useCallback(() => { - setFiles([]); - setPhase({ kind: "drop" }); - }, []); - - if (phase.kind === "drop") { - return ; - } - if (phase.kind === "mode") { - return ; - } - if (phase.kind === "playing") { - if (mode === "puzzle") { - return ( - - ); - } - return ; - } - // done phase: the phase carries its result, so both arms are non-null. - if (phase.result.kind === "puzzle") { - return ( - - ); - } - return ( - - ); -} diff --git a/bucket_catch/packages/frontend/src/components/DropZone.module.css b/bucket_catch/packages/frontend/src/components/DropZone.module.css deleted file mode 100644 index 33731f89..00000000 --- a/bucket_catch/packages/frontend/src/components/DropZone.module.css +++ /dev/null @@ -1,169 +0,0 @@ -.zone { - display: flex; - align-items: center; - justify-content: center; - min-height: 100dvh; - background: var(--bg-gradient); - transition: background 0.2s; - overflow-y: auto; - padding: var(--space-4) var(--space-3); -} - -.zone.dragging { - background: var(--bg-drag-gradient); - outline: 4px dashed var(--color-indigo); - outline-offset: -12px; -} - -.inner { - text-align: center; - color: var(--color-text); - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-2); -} - -.icon { - font-size: 52px; - line-height: 1; - /* Dimmed relative to the .subtitle text below it (rule 28). */ - opacity: 0.85; -} - -.title { - font-size: 2rem; - font-weight: 800; - margin: 0; - background: var(--gradient-accent); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.subtitle { - font-size: 1rem; - color: var(--muted-text); - margin: 0; -} - -.count { - font-size: 1rem; - color: var(--color-indigo-light); - font-weight: 600; - background: rgba(var(--color-indigo-rgb), 0.15); - padding: var(--space-1) var(--space-3); - border-radius: var(--radius-pill); - margin: 0; -} - -.browseBtn { - padding: 12px var(--space-4); - border-radius: var(--radius-pill); - background: var(--gradient-accent); - color: #fff; - font-weight: 700; - font-size: 0.95rem; - cursor: pointer; - border: none; - transition: opacity 0.15s; -} - -.browseBtn:hover { - opacity: 0.85; -} - -.divider { - color: var(--muted-border); - font-size: 0.8rem; - letter-spacing: 0.1em; - text-transform: uppercase; - margin: 2px 0; -} - -.puzzleSection { - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; -} - -.gridRow { - display: flex; - align-items: center; - gap: 6px; - flex-wrap: wrap; - justify-content: center; -} - -.gridLabel { - font-size: 0.85rem; - color: var(--muted-text); -} - -.preset { - padding: var(--space-1) var(--space-2); - border-radius: var(--radius-sm); - background: transparent; - /* muted-text, not muted-border — the border-only value measured under - 3:1 against the page background (rule 7). */ - border: 1.5px solid var(--muted-text); - color: var(--muted-text); - font-size: 0.8rem; - font-weight: 600; - cursor: pointer; - transition: border-color 0.12s, color 0.12s, background 0.12s; -} - -.preset:hover { - border-color: var(--color-indigo); - color: var(--color-indigo-lighter); -} - -.presetActive { - border-color: var(--color-pink); - color: var(--color-pink-light); - background: rgba(var(--color-pink-rgb), 0.1); -} - -.gridInput { - width: 56px; - padding: var(--space-1) var(--space-2); - border-radius: var(--radius-sm); - background: var(--surface); - border: 1.5px solid var(--muted-border); - color: var(--color-text); - font-size: 0.85rem; - font-weight: 600; - text-align: center; - outline: none; - transition: border-color 0.12s; -} - -.gridInput:focus { - border-color: var(--color-indigo); -} - -.gridEq { - font-size: 0.8rem; - color: var(--muted-faint); - margin: 0; -} - -.puzzleBtn { - padding: 12px var(--space-4); - border-radius: var(--radius-pill); - background: transparent; - border: 2px solid var(--color-pink); - color: var(--color-pink-light); - font-weight: 700; - font-size: 0.95rem; - cursor: pointer; - transition: border-color 0.15s, color 0.15s, background 0.15s; -} - -.puzzleBtn:hover { - background: rgba(var(--color-pink-rgb), 0.12); - border-color: var(--color-pink-lighter); - color: var(--color-pink-lighter); -} diff --git a/bucket_catch/packages/frontend/src/components/DropZone.test.tsx b/bucket_catch/packages/frontend/src/components/DropZone.test.tsx deleted file mode 100644 index d6e31e6b..00000000 --- a/bucket_catch/packages/frontend/src/components/DropZone.test.tsx +++ /dev/null @@ -1,229 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { render, screen, fireEvent } from "@testing-library/react"; -import { DropZone } from "./DropZone"; -import { must } from "../test/domQueries"; - -const makeFile = (name: string, type = "text/plain"): File => - new File(["x"], name, { type }); - -describe("DropZone", () => { - it("renders title and subtitle", () => { - render( undefined} onPuzzle={() => undefined} />); - expect(screen.getByText("Bucket Catch")).toBeInTheDocument(); - expect(screen.getByText(/Drop your files here/)).toBeInTheDocument(); - }); - - it("shows initial grid equation", () => { - render( undefined} onPuzzle={() => undefined} />); - expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument(); - }); - - it("dragOver adds dragging class; dragLeave removes it", () => { - const { container } = render( - undefined} onPuzzle={() => undefined} />, - ); - const zone = must(container.firstChild as Element | null, "the drop zone"); - expect(zone.className).not.toContain("dragging"); - fireEvent.dragOver(zone); - expect(zone.className).toContain("dragging"); - fireEvent.dragLeave(zone); - expect(zone.className).not.toContain("dragging"); - }); - - it("drop with files calls onFiles and shows count (plural)", () => { - const onFiles = vi.fn(); - const { container } = render( - undefined} />, - ); - const zone = must(container.firstChild as Element | null, "the drop zone"); - const f1 = makeFile("a.txt"); - const f2 = makeFile("b.txt"); - fireEvent.drop(zone, { dataTransfer: { files: [f1, f2] } }); - expect(onFiles).toHaveBeenCalledWith([f1, f2]); - expect(screen.getByText("2 files ready")).toBeInTheDocument(); - }); - - it("drop with files shows singular count for 1 file", () => { - const { container } = render( - undefined} onPuzzle={() => undefined} />, - ); - const zone = must(container.firstChild as Element | null, "the drop zone"); - fireEvent.drop(zone, { dataTransfer: { files: [makeFile("a.txt")] } }); - expect(screen.getByText("1 file ready")).toBeInTheDocument(); - }); - - it("drop with empty files does not call onFiles", () => { - const onFiles = vi.fn(); - const { container } = render( - undefined} />, - ); - const zone = must(container.firstChild as Element | null, "the drop zone"); - fireEvent.drop(zone, { dataTransfer: { files: [] } }); - expect(onFiles).not.toHaveBeenCalled(); - }); - - it("file input change with files calls onFiles", () => { - const onFiles = vi.fn(); - const { container } = render( - undefined} />, - ); - const fileInput = must( - container.querySelector("input[type='file'][multiple]"), - "input[type='file'][multiple]", - ); - const file = makeFile("c.txt"); - Object.defineProperty(fileInput, "files", { - value: [file], - configurable: true, - }); - fireEvent.change(fileInput); - expect(onFiles).toHaveBeenCalledWith([file]); - }); - - it("file input change with empty files does not call onFiles", () => { - const onFiles = vi.fn(); - const { container } = render( - undefined} />, - ); - const fileInput = must( - container.querySelector("input[type='file'][multiple]"), - "input[type='file'][multiple]", - ); - Object.defineProperty(fileInput, "files", { - value: [], - configurable: true, - }); - fireEvent.change(fileInput); - expect(onFiles).not.toHaveBeenCalled(); - }); - - it("file input change with null files (nullish-coalesce branch) does not call onFiles", () => { - const onFiles = vi.fn(); - const { container } = render( - undefined} />, - ); - const fileInput = must( - container.querySelector("input[type='file'][multiple]"), - "input[type='file'][multiple]", - ); - Object.defineProperty(fileInput, "files", { - value: null, - configurable: true, - }); - fireEvent.change(fileInput); - expect(onFiles).not.toHaveBeenCalled(); - }); - - it("puzzle input change with file calls onPuzzle with current gridSize", () => { - const onPuzzle = vi.fn(); - const { container } = render( - undefined} onPuzzle={onPuzzle} />, - ); - const puzzleInput = must( - container.querySelector("input[accept='image/*']"), - "input[accept='image/*']", - ); - const imgFile = makeFile("photo.png", "image/png"); - Object.defineProperty(puzzleInput, "files", { - value: [imgFile], - configurable: true, - }); - fireEvent.change(puzzleInput); - expect(onPuzzle).toHaveBeenCalledWith(imgFile, 4); // default gridSize = 4 - }); - - it("puzzle input change with null files does not call onPuzzle (optional-chain null branch)", () => { - const onPuzzle = vi.fn(); - const { container } = render( - undefined} onPuzzle={onPuzzle} />, - ); - const puzzleInput = must( - container.querySelector("input[accept='image/*']"), - "input[accept='image/*']", - ); - Object.defineProperty(puzzleInput, "files", { - value: null, - configurable: true, - }); - fireEvent.change(puzzleInput); - expect(onPuzzle).not.toHaveBeenCalled(); - }); - - it("puzzle input change with empty files does not call onPuzzle (no first element)", () => { - const onPuzzle = vi.fn(); - const { container } = render( - undefined} onPuzzle={onPuzzle} />, - ); - const puzzleInput = must( - container.querySelector("input[accept='image/*']"), - "input[accept='image/*']", - ); - Object.defineProperty(puzzleInput, "files", { - value: [], - configurable: true, - }); - fireEvent.change(puzzleInput); - expect(onPuzzle).not.toHaveBeenCalled(); - }); - - it("Play Puzzle Mode button triggers click on hidden puzzle input", () => { - const { container } = render( - undefined} onPuzzle={() => undefined} />, - ); - const puzzleInput = must( - container.querySelector("input[accept='image/*']"), - "input[accept='image/*']", - ); - const clickSpy = vi - .spyOn(puzzleInput, "click") - .mockImplementation(() => undefined); - fireEvent.click(screen.getByText(/Play Puzzle Mode/)); - expect(clickSpy).toHaveBeenCalled(); - }); - - it("clicking preset updates gridSize display and equation", () => { - render( undefined} onPuzzle={() => undefined} />); - fireEvent.click(screen.getByText("2×2")); - expect(screen.getByText("2×2 = 4 pieces")).toBeInTheDocument(); - }); - - it("custom grid input with valid value updates gridSize", () => { - render( undefined} onPuzzle={() => undefined} />); - const input = screen.getByLabelText("Custom grid size"); - fireEvent.change(input, { target: { value: "7" } }); - expect(screen.getByText("7×7 = 49 pieces")).toBeInTheDocument(); - }); - - it("custom grid input with NaN value does not update gridSize", () => { - render( undefined} onPuzzle={() => undefined} />); - const input = screen.getByLabelText("Custom grid size"); - fireEvent.change(input, { target: { value: "xyz" } }); - expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument(); - }); - - it("custom grid input with value below 2 does not update gridSize", () => { - render( undefined} onPuzzle={() => undefined} />); - const input = screen.getByLabelText("Custom grid size"); - fireEvent.change(input, { target: { value: "1" } }); - expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument(); - }); - - it("onPuzzle uses updated gridSize after preset click", () => { - const onPuzzle = vi.fn(); - const { container } = render( - undefined} onPuzzle={onPuzzle} />, - ); - fireEvent.click(screen.getByText("3×3")); // change gridSize to 3 - const puzzleInput = must( - container.querySelector("input[accept='image/*']"), - "input[accept='image/*']", - ); - const imgFile = makeFile("img.png", "image/png"); - Object.defineProperty(puzzleInput, "files", { - value: [imgFile], - configurable: true, - }); - fireEvent.change(puzzleInput); - expect(onPuzzle).toHaveBeenCalledWith(imgFile, 3); - }); -}); diff --git a/bucket_catch/packages/frontend/src/components/DropZone.tsx b/bucket_catch/packages/frontend/src/components/DropZone.tsx deleted file mode 100644 index bbcf9577..00000000 --- a/bucket_catch/packages/frontend/src/components/DropZone.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useCallback, useRef, useState } from "react"; -import styles from "./DropZone.module.css"; - -const PRESETS = [2, 3, 4, 5, 6] as const; - -interface Props { - onFiles: (files: File[]) => void; - onPuzzle: (imageFile: File, gridSize: number) => void; -} - -export function DropZone({ onFiles, onPuzzle }: Props): React.ReactElement { - const [dragging, setDragging] = useState(false); - const [count, setCount] = useState(0); - const [gridSize, setGridSize] = useState(4); - const puzzleInputRef = useRef(null); - - const handleDrop = useCallback( - (e: React.DragEvent) => { - e.preventDefault(); - setDragging(false); - const files = Array.from(e.dataTransfer.files); - if (files.length === 0) return; - setCount(files.length); - onFiles(files); - }, - [onFiles], - ); - - const handleFileInput = useCallback( - (e: React.ChangeEvent) => { - const files = Array.from(e.target.files ?? []); - if (files.length === 0) return; - setCount(files.length); - onFiles(files); - }, - [onFiles], - ); - - const handlePuzzleInput = useCallback( - (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - onPuzzle(file, gridSize); - }, - [onPuzzle, gridSize], - ); - - const handleGridChange = useCallback( - (e: React.ChangeEvent) => { - const raw = parseInt(e.target.value, 10); - if (!isNaN(raw) && raw >= 2) setGridSize(raw); - }, - [], - ); - - return ( -
{ - e.preventDefault(); - setDragging(true); - }} - onDragLeave={() => { setDragging(false); }} - onDrop={handleDrop} - > -
- 🪣 -

Bucket Catch

-

- Drop your files here — then catch them before they fall! -

- {count > 0 && ( -

- {count} file{count !== 1 ? "s" : ""} ready -

- )} - - -
or
- -
-
- Grid: - {PRESETS.map((n) => ( - - ))} - -
-

{gridSize}×{gridSize} = {gridSize * gridSize} pieces

- - -
-
-
- ); -} diff --git a/bucket_catch/packages/frontend/src/components/GameCanvas.module.css b/bucket_catch/packages/frontend/src/components/GameCanvas.module.css deleted file mode 100644 index 73aa12f9..00000000 --- a/bucket_catch/packages/frontend/src/components/GameCanvas.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.wrapper { - position: relative; - width: 100dvw; - height: 100dvh; - overflow: hidden; - cursor: none; -} - -.canvas { - display: block; - width: 100%; - height: 100%; -} - -.hint { - position: absolute; - top: 16px; - left: 50%; - transform: translateX(-50%); - color: rgba(var(--color-text-rgb), 0.5); - font-size: 0.85rem; - pointer-events: none; - user-select: none; -} diff --git a/bucket_catch/packages/frontend/src/components/GameCanvas.test.tsx b/bucket_catch/packages/frontend/src/components/GameCanvas.test.tsx deleted file mode 100644 index a86c018f..00000000 --- a/bucket_catch/packages/frontend/src/components/GameCanvas.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, waitFor } from "@testing-library/react"; -import { GameCanvas } from "./GameCanvas"; - -vi.mock("../hooks/useBasketControl", () => ({ - useBasketControl: vi.fn(() => ({ current: 400 })), -})); -vi.mock("../hooks/useGameLoop", () => ({ - useGameLoop: vi.fn(() => null), -})); - -import { useGameLoop } from "../hooks/useGameLoop"; - -describe("GameCanvas", () => { - beforeEach(() => { - vi.mocked(useGameLoop).mockReturnValue(null); - }); - - it("renders a canvas element and hint text", () => { - const { container } = render( - undefined} />, - ); - expect(container.querySelector("canvas")).toBeInTheDocument(); - expect( - container.querySelector(".hint") ?? - container.querySelector('[class*="hint"]'), - ).toBeInTheDocument(); - }); - - it("adds resize event listener on mount and removes it on unmount", () => { - const addSpy = vi.spyOn(window, "addEventListener"); - const removeSpy = vi.spyOn(window, "removeEventListener"); - - const { unmount } = render( - undefined} />, - ); - - expect(addSpy).toHaveBeenCalledWith("resize", expect.any(Function)); - unmount(); - expect(removeSpy).toHaveBeenCalledWith("resize", expect.any(Function)); - }); - - it("calls onDone with the result when useGameLoop returns non-null", async () => { - const result = { caught: [], missed: [] }; - vi.mocked(useGameLoop).mockReturnValue(result); - - const onDone = vi.fn(); - render(); - - await waitFor(() => { - expect(onDone).toHaveBeenCalledWith(result); - }); - }); - - it("does not call onDone when result is null", () => { - vi.mocked(useGameLoop).mockReturnValue(null); - const onDone = vi.fn(); - render(); - expect(onDone).not.toHaveBeenCalled(); - }); - - it("resize handler returns safely when canvasRef is null after unmount", () => { - const addSpy = vi.spyOn(window, "addEventListener"); - const { unmount } = render( undefined} />); - - const resizeArgs = addSpy.mock.calls.find((c) => c[0] === "resize"); - const resize = resizeArgs?.[1]; - - unmount(); // React nulls canvasRef.current - - // Call the captured handler — canvas is now null → covers the null-guard branch - if (typeof resize === "function") resize(new Event("resize")); - - addSpy.mockRestore(); - }); -}); diff --git a/bucket_catch/packages/frontend/src/components/GameCanvas.tsx b/bucket_catch/packages/frontend/src/components/GameCanvas.tsx deleted file mode 100644 index fe9cad58..00000000 --- a/bucket_catch/packages/frontend/src/components/GameCanvas.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import type { FileGameResult } from "../types"; -import { useBasketControl } from "../hooks/useBasketControl"; -import { useGameLoop } from "../hooks/useGameLoop"; -import styles from "./GameCanvas.module.css"; - -interface Props { - files: File[]; - onDone: (result: FileGameResult) => void; -} - -export function GameCanvas({ files, onDone }: Props): React.ReactElement { - const canvasRef = useRef(null); - const basketXRef = useBasketControl(canvasRef); - - useEffect(() => { - const resize = (): void => { - const canvas = canvasRef.current; - /* istanbul ignore next */ - if (!canvas) return; - canvas.width = canvas.clientWidth; - canvas.height = canvas.clientHeight; - }; - resize(); - window.addEventListener("resize", resize); - return () => { window.removeEventListener("resize", resize); }; - }, []); - - const result = useGameLoop(canvasRef, basketXRef, files, true); - - useEffect(() => { - if (result) onDone(result); - }, [result, onDone]); - - return ( -
- -
Move mouse to control the basket
-
- ); -} diff --git a/bucket_catch/packages/frontend/src/components/ModeSelect.module.css b/bucket_catch/packages/frontend/src/components/ModeSelect.module.css deleted file mode 100644 index 2b053aa1..00000000 --- a/bucket_catch/packages/frontend/src/components/ModeSelect.module.css +++ /dev/null @@ -1,170 +0,0 @@ -.container { - min-height: 100dvh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--space-4); - background: var(--bg-gradient); - color: var(--color-text); -} - -.heading { - font-size: 2rem; - font-weight: 800; - margin: 0; - background: var(--gradient-accent); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.sub { - font-size: 1.1rem; - color: var(--muted-text); - margin: 0; -} - -.cards { - display: flex; - gap: var(--space-4); - flex-wrap: wrap; - justify-content: center; -} - -.card { - background: var(--surface); - border: 2px solid var(--color-indigo); - border-radius: var(--radius-lg); - padding: var(--space-5) var(--space-6); - width: 220px; - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - cursor: pointer; - transition: border-color 0.15s, transform 0.15s; - color: var(--color-text); -} - -.card:hover { - border-color: var(--color-pink); - transform: translateY(-4px); -} - -.cardIcon { - font-size: 48px; - /* Dimmed relative to .cardDesc below it (rule 28). */ - opacity: 0.85; -} - -.cardTitle { - font-size: 1.3rem; - font-weight: 700; -} - -.cardDesc { - /* Was 0.85rem (13.6px), under the 16px reading floor (rule 20). */ - font-size: 1rem; - color: var(--muted-text); - text-align: center; - line-height: 1.4; -} - -.cardDisabled { - opacity: 0.4; - cursor: not-allowed; - border-color: var(--muted-border); -} - -.cardDisabled:hover { - border-color: var(--muted-border); - transform: none; -} - -.puzzleCard { - cursor: default; -} - -.gridRow { - display: flex; - align-items: center; - gap: var(--space-1); - flex-wrap: wrap; - justify-content: center; - margin-top: 4px; -} - -.preset { - padding: var(--space-1) var(--space-2); - border-radius: var(--radius-sm); - background: transparent; - /* muted-text, not muted-border — same rule 7 contrast fix as DropZone. */ - border: 1.5px solid var(--muted-text); - color: var(--muted-text); - font-size: 0.78rem; - font-weight: 600; - cursor: pointer; - transition: border-color 0.12s, color 0.12s, background 0.12s; -} - -.preset:hover { - border-color: var(--color-indigo); - color: var(--color-indigo-lighter); -} - -.presetActive { - border-color: var(--color-pink); - color: var(--color-pink-light); - /* 0.1, not 0.12 — matches DropZone's identical "active preset chip" - role; the two files had drifted to different alpha values. */ - background: rgba(var(--color-pink-rgb), 0.1); -} - -.gridInput { - width: 52px; - padding: var(--space-1) var(--space-2); - border-radius: var(--radius-sm); - /* --surface, not a raw rgba(15,12,41,...) literal — DropZone's sibling - gridInput already used the surface color; this one had drifted to a - different base (bg-1) for the identical component. */ - background: var(--surface); - border: 1.5px solid var(--muted-border); - color: var(--color-text); - font-size: 0.82rem; - font-weight: 600; - text-align: center; - outline: none; - transition: border-color 0.12s; -} - -.gridInput:focus { - border-color: var(--color-indigo); -} - -.gridEq { - font-size: 0.78rem; - color: var(--muted-faint); - margin: 2px 0 0; -} - -.puzzleStart { - margin-top: 6px; - padding: 10px 20px; - border-radius: var(--radius-pill); - /* --gradient-accent, not the stops written out reversed — this button - was pink->indigo while every other primary gradient in the app is - indigo->pink (same underlying bug the audit flagged in - PuzzleResult.module.css, just not cited by this exact line). */ - background: var(--gradient-accent); - color: #fff; - font-weight: 700; - font-size: 0.9rem; - border: none; - cursor: pointer; - transition: opacity 0.15s; -} - -.puzzleStart:hover { - opacity: 0.85; -} diff --git a/bucket_catch/packages/frontend/src/components/ModeSelect.test.tsx b/bucket_catch/packages/frontend/src/components/ModeSelect.test.tsx deleted file mode 100644 index 2e88e5b0..00000000 --- a/bucket_catch/packages/frontend/src/components/ModeSelect.test.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { render, screen, fireEvent } from "@testing-library/react"; -import { ModeSelect } from "./ModeSelect"; - -const makeFile = (name: string, type = "text/plain"): File => - new File(["x"], name, { type }); - -const makeImageFile = (): File => - new File(["x"], "photo.png", { type: "image/png" }); - -describe("ModeSelect", () => { - it("renders singular file count for 1 file", () => { - render( - undefined} />, - ); - expect(screen.getByText("1 file ready to drop")).toBeInTheDocument(); - }); - - it("renders plural file count for multiple files", () => { - render( - undefined} - />, - ); - expect(screen.getByText("2 files ready to drop")).toBeInTheDocument(); - }); - - it("calls onStart('download') when Download is clicked", () => { - const onStart = vi.fn(); - render(); - fireEvent.click(screen.getByText("Download")); - expect(onStart).toHaveBeenCalledWith("download"); - }); - - it("calls onStart('upload') when Upload is clicked", () => { - const onStart = vi.fn(); - render(); - fireEvent.click(screen.getByText("Upload")); - expect(onStart).toHaveBeenCalledWith("upload"); - }); - - it("shows disabled puzzle message when files is not a single image", () => { - render( - undefined} - />, - ); - expect( - screen.getByText("Drop exactly one image file to play puzzle mode."), - ).toBeInTheDocument(); - expect(screen.queryByText("Start")).not.toBeInTheDocument(); - }); - - it("shows disabled puzzle message for single non-image file", () => { - render( - undefined} />, - ); - expect( - screen.getByText("Drop exactly one image file to play puzzle mode."), - ).toBeInTheDocument(); - }); - - it("shows puzzle controls when a single image file is loaded", () => { - render( - undefined} />, - ); - expect( - screen.getByText("Catch puzzle pieces to assemble your image!"), - ).toBeInTheDocument(); - expect(screen.getByText("Start")).toBeInTheDocument(); - }); - - it("clicking a preset button updates grid size display", () => { - render( - undefined} />, - ); - expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument(); - fireEvent.click(screen.getByText("3×3")); - expect(screen.getByText("3×3 = 9 pieces")).toBeInTheDocument(); - }); - - it("custom grid input with valid value updates grid size", () => { - render( - undefined} />, - ); - const input = screen.getByLabelText("Custom grid size"); - fireEvent.change(input, { target: { value: "6" } }); - expect(screen.getByText("6×6 = 36 pieces")).toBeInTheDocument(); - }); - - it("custom grid input with NaN value does not update grid size", () => { - render( - undefined} />, - ); - const input = screen.getByLabelText("Custom grid size"); - fireEvent.change(input, { target: { value: "abc" } }); - expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument(); - }); - - it("custom grid input with value < 2 does not update grid size", () => { - render( - undefined} />, - ); - const input = screen.getByLabelText("Custom grid size"); - fireEvent.change(input, { target: { value: "1" } }); - expect(screen.getByText("4×4 = 16 pieces")).toBeInTheDocument(); - }); - - it("Start button calls onStart('puzzle', gridSize)", () => { - const onStart = vi.fn(); - render(); - // Default gridSize is 4 - fireEvent.click(screen.getByText("Start")); - expect(onStart).toHaveBeenCalledWith("puzzle", 4); - }); - - it("clicking number input stops propagation (onClick handler covered)", () => { - render( - undefined} />, - ); - const input = screen.getByLabelText("Custom grid size"); - fireEvent.click(input); // covers onClick={(e) => { e.stopPropagation(); }} - }); - - it("Start button uses updated grid size after preset click", () => { - const onStart = vi.fn(); - render(); - fireEvent.click(screen.getByText("5×5")); - fireEvent.click(screen.getByText("Start")); - expect(onStart).toHaveBeenCalledWith("puzzle", 5); - }); -}); diff --git a/bucket_catch/packages/frontend/src/components/ModeSelect.tsx b/bucket_catch/packages/frontend/src/components/ModeSelect.tsx deleted file mode 100644 index 5e5f81d7..00000000 --- a/bucket_catch/packages/frontend/src/components/ModeSelect.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { useCallback, useState } from "react"; -import type { TransferMode } from "../types"; -import styles from "./ModeSelect.module.css"; - -const PRESETS = [2, 3, 4, 5, 6] as const; - -interface Props { - files: File[]; - onStart: (mode: TransferMode, gridSize?: number) => void; -} - -export function ModeSelect({ files, onStart }: Props): React.ReactElement { - const [gridSize, setGridSize] = useState(4); - const fileCount = files.length; - const isSingleImage = - files.length === 1 && files[0].type.startsWith("image/"); - - const handleGridChange = useCallback( - (e: React.ChangeEvent) => { - const raw = parseInt(e.target.value, 10); - if (!isNaN(raw) && raw >= 2) setGridSize(raw); - }, - [], - ); - - return ( -
-

- {fileCount} file{fileCount !== 1 ? "s" : ""} ready to drop -

-

What happens to the files you catch?

-
- - -
- 🧩 - Puzzle - - {isSingleImage - ? "Catch puzzle pieces to assemble your image!" - : "Drop exactly one image file to play puzzle mode."} - - {isSingleImage && ( - <> -
- {PRESETS.map((n) => ( - - ))} - { e.stopPropagation(); }} - className={styles.gridInput} - aria-label="Custom grid size" - /> -
-

{gridSize}×{gridSize} = {gridSize * gridSize} pieces

- - - )} -
-
-
- ); -} diff --git a/bucket_catch/packages/frontend/src/components/PuzzleCanvas.module.css b/bucket_catch/packages/frontend/src/components/PuzzleCanvas.module.css deleted file mode 100644 index d1e5c586..00000000 --- a/bucket_catch/packages/frontend/src/components/PuzzleCanvas.module.css +++ /dev/null @@ -1,25 +0,0 @@ -.wrapper { - position: relative; - width: 100dvw; - height: 100dvh; - overflow: hidden; - cursor: none; -} - -.canvas { - display: block; - width: 100%; - height: 100%; -} - -.hint { - position: absolute; - top: 16px; - left: 50%; - transform: translateX(-50%); - color: rgba(var(--color-pink-rgb), 0.7); - font-size: 0.9rem; - pointer-events: none; - user-select: none; - white-space: nowrap; -} diff --git a/bucket_catch/packages/frontend/src/components/PuzzleCanvas.test.tsx b/bucket_catch/packages/frontend/src/components/PuzzleCanvas.test.tsx deleted file mode 100644 index 915b0be2..00000000 --- a/bucket_catch/packages/frontend/src/components/PuzzleCanvas.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, waitFor } from "@testing-library/react"; -import { PuzzleCanvas } from "./PuzzleCanvas"; - -vi.mock("../hooks/useBasketControl", () => ({ - useBasketControl: vi.fn(() => ({ current: 400 })), -})); -vi.mock("../hooks/usePuzzleGameLoop", () => ({ - usePuzzleGameLoop: vi.fn(() => null), -})); - -import { usePuzzleGameLoop } from "../hooks/usePuzzleGameLoop"; - -const imageFile = new File(["img"], "test.png", { type: "image/png" }); - -describe("PuzzleCanvas", () => { - beforeEach(() => { - vi.mocked(usePuzzleGameLoop).mockReturnValue(null); - }); - - it("renders a canvas element and puzzle hint text", () => { - const { container } = render( - undefined} />, - ); - expect(container.querySelector("canvas")).toBeInTheDocument(); - expect( - container.querySelector(".hint") ?? - container.querySelector('[class*="hint"]'), - ).toBeInTheDocument(); - }); - - it("adds resize event listener on mount and removes it on unmount", () => { - const addSpy = vi.spyOn(window, "addEventListener"); - const removeSpy = vi.spyOn(window, "removeEventListener"); - - const { unmount } = render( - undefined} />, - ); - - expect(addSpy).toHaveBeenCalledWith("resize", expect.any(Function)); - unmount(); - expect(removeSpy).toHaveBeenCalledWith("resize", expect.any(Function)); - }); - - it("calls onDone with the result when usePuzzleGameLoop returns non-null", async () => { - const result = { caughtPieces: [], missedPieces: [], gridSize: 2 }; - vi.mocked(usePuzzleGameLoop).mockReturnValue(result); - - const onDone = vi.fn(); - render(); - - await waitFor(() => { - expect(onDone).toHaveBeenCalledWith(result); - }); - }); - - it("does not call onDone when result is null", () => { - vi.mocked(usePuzzleGameLoop).mockReturnValue(null); - const onDone = vi.fn(); - render(); - expect(onDone).not.toHaveBeenCalled(); - }); - - it("resize handler returns safely when canvasRef is null after unmount", () => { - const addSpy = vi.spyOn(window, "addEventListener"); - const { unmount } = render( - undefined} />, - ); - - const resizeArgs = addSpy.mock.calls.find((c) => c[0] === "resize"); - const resize = resizeArgs?.[1]; - - unmount(); // React nulls canvasRef.current - - if (typeof resize === "function") resize(new Event("resize")); - - addSpy.mockRestore(); - }); -}); diff --git a/bucket_catch/packages/frontend/src/components/PuzzleCanvas.tsx b/bucket_catch/packages/frontend/src/components/PuzzleCanvas.tsx deleted file mode 100644 index b9205423..00000000 --- a/bucket_catch/packages/frontend/src/components/PuzzleCanvas.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import type { PuzzleGameResult } from "../types"; -import { useBasketControl } from "../hooks/useBasketControl"; -import { usePuzzleGameLoop } from "../hooks/usePuzzleGameLoop"; -import styles from "./PuzzleCanvas.module.css"; - -interface Props { - imageFile: File; - gridSize: number; - onDone: (result: PuzzleGameResult) => void; -} - -export function PuzzleCanvas({ - imageFile, - gridSize, - onDone, -}: Props): React.ReactElement { - const canvasRef = useRef(null); - const basketXRef = useBasketControl(canvasRef); - - useEffect(() => { - const resize = (): void => { - const canvas = canvasRef.current; - /* istanbul ignore next */ - if (!canvas) return; - canvas.width = canvas.clientWidth; - canvas.height = canvas.clientHeight; - }; - resize(); - window.addEventListener("resize", resize); - return () => { window.removeEventListener("resize", resize); }; - }, []); - - const result = usePuzzleGameLoop(canvasRef, basketXRef, imageFile, gridSize, true); - - useEffect(() => { - if (result) onDone(result); - }, [result, onDone]); - - return ( -
- -
Move mouse to catch the puzzle pieces!
-
- ); -} diff --git a/bucket_catch/packages/frontend/src/components/PuzzleResult.module.css b/bucket_catch/packages/frontend/src/components/PuzzleResult.module.css deleted file mode 100644 index a4a66254..00000000 --- a/bucket_catch/packages/frontend/src/components/PuzzleResult.module.css +++ /dev/null @@ -1,88 +0,0 @@ -.container { - min-height: 100dvh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 28px; - padding: var(--space-5) var(--space-3); - background: var(--bg-gradient); - color: var(--color-text); -} - -.scoreBox { - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-2); -} - -.grade { - font-size: 6rem; - font-weight: 900; - line-height: 1; - /* Was reversed (pink->indigo) vs every other heading gradient in the - app (indigo->pink) — the bug DESIGN_AUDIT_TODO.md flagged. */ - background: var(--gradient-accent); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.pct { - font-size: 2rem; - font-weight: 700; - color: var(--color-indigo-light); -} - -.stats { - font-size: 1.1rem; - font-weight: 600; -} - -.caught { - color: var(--color-success); -} - -.grid { - display: grid; - gap: 4px; - max-width: min(480px, 90vw); - width: 100%; - border: 3px solid rgba(var(--color-indigo-rgb), 0.4); - border-radius: var(--radius-md); - overflow: hidden; - background: var(--surface-solid); -} - -.piece { - display: block; - width: 100%; - aspect-ratio: 1; - object-fit: cover; -} - -.hole { - width: 100%; - aspect-ratio: 1; - background: rgba(var(--surface-rgb), 0.9); - border: 1px dashed rgba(var(--color-indigo-rgb), 0.2); -} - -.restartBtn { - padding: 14px 28px; - border-radius: var(--radius-pill); - background: transparent; - border: 2px solid var(--color-pink); - color: var(--color-pink-light); - font-weight: 600; - font-size: 1rem; - cursor: pointer; - transition: border-color 0.15s, color 0.15s; -} - -.restartBtn:hover { - border-color: var(--color-indigo); - color: var(--color-indigo-light); -} diff --git a/bucket_catch/packages/frontend/src/components/PuzzleResult.test.tsx b/bucket_catch/packages/frontend/src/components/PuzzleResult.test.tsx deleted file mode 100644 index 7c24dfdd..00000000 --- a/bucket_catch/packages/frontend/src/components/PuzzleResult.test.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { render, screen, fireEvent } from "@testing-library/react"; -import { PuzzleResult } from "./PuzzleResult"; -import { must } from "../test/domQueries"; -import type { FallingPuzzleItem, PuzzlePiece } from "../types"; - -const makePiece = (row: number, col: number): PuzzlePiece => ({ - row, - col, - gridSize: 2, - imageUrl: `data:image/png;base64,${row}-${col}`, - pieceWidth: 50, - pieceHeight: 50, -}); - -const makeFalling = (row: number, col: number): FallingPuzzleItem => ({ - kind: "puzzle", - id: `${row}-${col}`, - piece: makePiece(row, col), - x: 0, - y: 0, - speed: 3, - startFrame: 0, - status: "caught", -}); - -describe("PuzzleResult", () => { - it("renders grade A and percentage for 75% completion", () => { - // gridSize=2 → total=4; 3 caught → 75% → A - render( - undefined} - />, - ); - expect(screen.getByText("A")).toBeInTheDocument(); - expect(screen.getByText("75%")).toBeInTheDocument(); - expect(screen.getByText(/3 \/ 4 pieces caught/)).toBeInTheDocument(); - }); - - it("renders grade S at 100%", () => { - render( - undefined} - />, - ); - expect(screen.getByText("S")).toBeInTheDocument(); - expect(screen.getByText("100%")).toBeInTheDocument(); - }); - - it("renders grade B at exactly 50%", () => { - render( - undefined} - />, - ); - expect(screen.getByText("B")).toBeInTheDocument(); - expect(screen.getByText("50%")).toBeInTheDocument(); - }); - - it("renders grade C at 25%", () => { - render( - undefined} - />, - ); - expect(screen.getByText("C")).toBeInTheDocument(); - expect(screen.getByText("25%")).toBeInTheDocument(); - }); - - it("renders grade D at 0%", () => { - render( - undefined} - />, - ); - expect(screen.getByText("D")).toBeInTheDocument(); - expect(screen.getByText("0%")).toBeInTheDocument(); - }); - - it("renders img elements for caught pieces and div holes for missed", () => { - const { container } = render( - undefined} - />, - ); - // 2 img elements for caught pieces - const imgs = container.querySelectorAll("img"); - expect(imgs).toHaveLength(2); - // The grid div has inline style with gridTemplateColumns; its non-img children are holes - const gridEl = must( - container.querySelector('[style*="repeat"]'), - "the puzzle grid element", - ); - const holeDivs = Array.from(gridEl.children).filter( - (c) => c.tagName === "DIV", - ); - expect(holeDivs).toHaveLength(2); - }); - - it("img src matches imageUrl of caught piece", () => { - render( - undefined} - />, - ); - const img = screen.getByRole("img", { name: "Piece 0-0" }); - expect(img).toHaveAttribute("src", "data:image/png;base64,0-0"); - }); - - it("calls onRestart when Play again is clicked", () => { - const onRestart = vi.fn(); - render( - , - ); - fireEvent.click(screen.getByText("Play again")); - expect(onRestart).toHaveBeenCalledOnce(); - }); - - it("handles gridSize=0 without errors (0% fallback)", () => { - render( - undefined} - />, - ); - expect(screen.getByText("D")).toBeInTheDocument(); - expect(screen.getByText("0%")).toBeInTheDocument(); - }); -}); diff --git a/bucket_catch/packages/frontend/src/components/PuzzleResult.tsx b/bucket_catch/packages/frontend/src/components/PuzzleResult.tsx deleted file mode 100644 index 57211206..00000000 --- a/bucket_catch/packages/frontend/src/components/PuzzleResult.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from "react"; -import type { PuzzleGameResult } from "../types"; -import styles from "./PuzzleResult.module.css"; - -interface Props { - result: PuzzleGameResult; - onRestart: () => void; -} - -function calcGrade(pct: number): string { - if (pct >= 90) return "S"; - if (pct >= 75) return "A"; - if (pct >= 50) return "B"; - if (pct >= 25) return "C"; - return "D"; -} - -export function PuzzleResult({ result, onRestart }: Props): React.ReactElement { - const total = result.gridSize * result.gridSize; - const caught = result.caughtPieces.length; - const pct = total === 0 ? 0 : Math.round((caught / total) * 100); - const grade = calcGrade(pct); - - const cells = Array.from({ length: total }, (_, index) => { - const row = Math.floor(index / result.gridSize); - const col = index % result.gridSize; - const piece = result.caughtPieces.find( - (p) => p.piece.row === row && p.piece.col === col, - ); - return { row, col, piece }; - }); - - return ( -
-
-
{grade}
-
{pct}%
-
- - ✅ {caught} / {total} pieces caught - -
-
- -
- {cells.map(({ row, col, piece }) => - piece ? ( - {`Piece - ) : ( -
- ), - )} -
- - -
- ); -} diff --git a/bucket_catch/packages/frontend/src/components/ScoreScreen.module.css b/bucket_catch/packages/frontend/src/components/ScoreScreen.module.css deleted file mode 100644 index 7257f118..00000000 --- a/bucket_catch/packages/frontend/src/components/ScoreScreen.module.css +++ /dev/null @@ -1,157 +0,0 @@ -.container { - min-height: 100dvh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 28px; - padding: var(--space-5) var(--space-3); - background: var(--bg-gradient); - color: var(--color-text); -} - -.scoreBox { - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-2); -} - -.grade { - font-size: 6rem; - font-weight: 900; - line-height: 1; - background: var(--gradient-accent); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.pct { - font-size: 2rem; - font-weight: 700; - color: var(--color-indigo-light); -} - -.stats { - display: flex; - gap: var(--space-4); - font-size: 1.1rem; - font-weight: 600; -} - -.caught { - color: var(--color-success); -} - -.missed { - color: var(--color-error); -} - -.lists { - display: flex; - gap: var(--space-5); - flex-wrap: wrap; - justify-content: center; - max-width: 640px; - width: 100%; -} - -.listTitle { - font-size: 1rem; - font-weight: 700; - color: var(--muted-text); - text-transform: uppercase; - letter-spacing: 0.08em; - margin: 0 0 8px; -} - -.fileList { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 6px; -} - -.fileItem { - display: flex; - align-items: center; - gap: var(--space-2); - background: var(--surface); - border-radius: var(--radius-md); - padding: var(--space-2) var(--space-3); - font-size: 0.9rem; -} - -.missedItem { - opacity: 0.5; -} - -.fname { - flex: 1; - font-family: monospace; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 180px; -} - -.fsize { - color: var(--muted-faint); - font-size: 0.8rem; -} - -.actions { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-3); -} - -.actionBtn { - padding: 16px var(--space-5); - border-radius: var(--radius-pill); - background: var(--gradient-accent); - color: #fff; - font-weight: 700; - font-size: 1rem; - border: none; - cursor: pointer; - transition: opacity 0.15s; -} - -.actionBtn:hover { - opacity: 0.85; -} - -.restartBtn { - padding: 14px 28px; - border-radius: var(--radius-pill); - background: transparent; - border: 2px solid var(--color-indigo); - color: var(--color-indigo-light); - font-weight: 600; - font-size: 1rem; - cursor: pointer; - transition: border-color 0.15s, color 0.15s; -} - -.restartBtn:hover { - border-color: var(--color-pink); - color: var(--color-pink-light); -} - -.status { - color: var(--color-success); - font-weight: 600; - margin: 0; -} - -.error { - color: var(--color-error); - font-weight: 600; - margin: 0; -} diff --git a/bucket_catch/packages/frontend/src/components/ScoreScreen.test.tsx b/bucket_catch/packages/frontend/src/components/ScoreScreen.test.tsx deleted file mode 100644 index fcc22a4f..00000000 --- a/bucket_catch/packages/frontend/src/components/ScoreScreen.test.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, fireEvent, waitFor } from "@testing-library/react"; -import { ScoreScreen } from "./ScoreScreen"; - -vi.mock("../lib/zipDownload", () => ({ - zipDownload: vi.fn(), -})); -vi.mock("../lib/uploadFiles", () => ({ - uploadFiles: vi.fn(), -})); - -import { zipDownload } from "../lib/zipDownload"; -import { uploadFiles } from "../lib/uploadFiles"; - -const makeFile = (name: string, size = 10): File => { - const f = new File(["x".repeat(size)], name, { type: "text/plain" }); - return f; -}; - -const result1 = { - caught: [makeFile("a.txt"), makeFile("b.txt")], - missed: [makeFile("c.txt")], -}; - -describe("ScoreScreen", () => { - beforeEach(() => { - vi.mocked(zipDownload).mockResolvedValue(undefined); - vi.mocked(uploadFiles).mockResolvedValue([]); - }); - - it("renders grade, percentage, and file lists", () => { - render( - undefined} />, - ); - // 2 caught / 3 total = 67% → grade B - expect(screen.getByText("B")).toBeInTheDocument(); - expect(screen.getByText("67%")).toBeInTheDocument(); - expect(screen.getByText("Caught")).toBeInTheDocument(); - expect(screen.getByText("Missed")).toBeInTheDocument(); - }); - - it("shows download button for download mode with multiple files", () => { - render( - undefined} />, - ); - expect( - screen.getByText(/Download 2 caught files/), - ).toBeInTheDocument(); - }); - - it("shows singular 'file' for exactly 1 caught file", () => { - render( - undefined} - />, - ); - expect(screen.getByText(/Download 1 caught file$/)).toBeInTheDocument(); - }); - - it("triggers zip download and shows done state", async () => { - render( - undefined} />, - ); - fireEvent.click(screen.getByText(/Download/)); - await waitFor(() => { - expect(screen.getByText("Downloaded!")).toBeInTheDocument(); - }); - expect(zipDownload).toHaveBeenCalledWith(result1.caught); - }); - - it("shows upload button for upload mode", () => { - render( - undefined} />, - ); - expect(screen.getByText(/Upload 2 caught files to server/)).toBeInTheDocument(); - }); - - it("shows singular upload text for exactly 1 caught file", () => { - render( - undefined} - />, - ); - expect(screen.getByText(/Upload 1 caught file to server$/)).toBeInTheDocument(); - }); - - it("triggers upload and shows done state", async () => { - render( - undefined} />, - ); - fireEvent.click(screen.getByText(/Upload/)); - await waitFor(() => { - expect(screen.getByText("Uploaded to server!")).toBeInTheDocument(); - }); - expect(uploadFiles).toHaveBeenCalledWith(result1.caught); - }); - - it("shows error state when action throws an Error", async () => { - vi.mocked(zipDownload).mockRejectedValue(new Error("Network error")); - render( - undefined} />, - ); - fireEvent.click(screen.getByText(/Download/)); - await waitFor(() => { - expect(screen.getByText(/Error: Network error/)).toBeInTheDocument(); - }); - }); - - it("shows error state when action throws a non-Error value", async () => { - vi.mocked(uploadFiles).mockRejectedValue("server unavailable"); - render( - undefined} />, - ); - fireEvent.click(screen.getByText(/Upload/)); - await waitFor(() => { - expect(screen.getByText(/Error: server unavailable/)).toBeInTheDocument(); - }); - }); - - it("calls onRestart when play again is clicked", () => { - const onRestart = vi.fn(); - render( - , - ); - fireEvent.click(screen.getByText("Play again")); - expect(onRestart).toHaveBeenCalledOnce(); - }); - - it("hides action button when no files were caught", () => { - render( - undefined} - />, - ); - expect(screen.queryByText(/Download/)).not.toBeInTheDocument(); - }); - - it("grades S at >= 90%", () => { - const files = Array.from({ length: 10 }, (_, i) => makeFile(`f${i}.txt`)); - render( - undefined} - />, - ); - expect(screen.getByText("S")).toBeInTheDocument(); - }); - - it("grades A at >= 75%", () => { - const files = Array.from({ length: 4 }, (_, i) => makeFile(`f${i}.txt`)); - render( - undefined} - />, - ); - expect(screen.getByText("A")).toBeInTheDocument(); - }); - - it("grades C at >= 25%", () => { - const files = Array.from({ length: 4 }, (_, i) => makeFile(`f${i}.txt`)); - render( - undefined} - />, - ); - expect(screen.getByText("C")).toBeInTheDocument(); - }); - - it("grades D below 25%", () => { - const files = Array.from({ length: 5 }, (_, i) => makeFile(`f${i}.txt`)); - render( - undefined} - />, - ); - expect(screen.getByText("D")).toBeInTheDocument(); - expect(screen.getByText("0%")).toBeInTheDocument(); - }); - - it("handles zero total files (0%)", () => { - render( - undefined} - />, - ); - expect(screen.getByText("D")).toBeInTheDocument(); - expect(screen.getByText("0%")).toBeInTheDocument(); - }); -}); diff --git a/bucket_catch/packages/frontend/src/components/ScoreScreen.tsx b/bucket_catch/packages/frontend/src/components/ScoreScreen.tsx deleted file mode 100644 index a771344f..00000000 --- a/bucket_catch/packages/frontend/src/components/ScoreScreen.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, { useState } from "react"; -import type { FileGameResult, TransferMode } from "../types"; -import { fileIcon, formatSize } from "../lib/fileIcon"; -import { zipDownload } from "../lib/zipDownload"; -import { uploadFiles } from "../lib/uploadFiles"; -import styles from "./ScoreScreen.module.css"; - -interface Props { - result: FileGameResult; - mode: TransferMode; - onRestart: () => void; -} - -type ActionState = "idle" | "working" | "done" | "error"; - -function calcGrade(pct: number): string { - if (pct >= 90) return "S"; - if (pct >= 75) return "A"; - if (pct >= 50) return "B"; - if (pct >= 25) return "C"; - return "D"; -} - -export function ScoreScreen({ - result, - mode, - onRestart, -}: Props): React.ReactElement { - const [state, setState] = useState("idle"); - const [errorMsg, setErrorMsg] = useState(""); - const total = result.caught.length + result.missed.length; - const pct = - total === 0 ? 0 : Math.round((result.caught.length / total) * 100); - const grade = calcGrade(pct); - - const handleAction = async (): Promise => { - setState("working"); - try { - if (mode === "download") { - await zipDownload(result.caught); - } else { - await uploadFiles(result.caught); - } - setState("done"); - } catch (e) { - setErrorMsg(e instanceof Error ? e.message : String(e)); - setState("error"); - } - }; - - return ( -
-
-
{grade}
-
{pct}%
-
- ✅ {result.caught.length} caught - ❌ {result.missed.length} missed -
-
- -
- {result.caught.length > 0 && ( -
-

Caught

-
    - {result.caught.map((f, i) => ( -
  • - {fileIcon(f)} - {f.name} - {formatSize(f.size)} -
  • - ))} -
-
- )} - {result.missed.length > 0 && ( -
-

Missed

-
    - {result.missed.map((f, i) => ( -
  • - {fileIcon(f)} - {f.name} - {formatSize(f.size)} -
  • - ))} -
-
- )} -
- -
- {result.caught.length > 0 && state === "idle" && ( - - )} - {state === "working" && ( -

Working…

- )} - {state === "done" && ( -

- {mode === "download" ? "Downloaded!" : "Uploaded to server!"} -

- )} - {state === "error" && ( -

Error: {errorMsg}

- )} - -
-
- ); -} diff --git a/bucket_catch/packages/frontend/src/hooks/puzzleGeometry.ts b/bucket_catch/packages/frontend/src/hooks/puzzleGeometry.ts deleted file mode 100644 index 491988bb..00000000 --- a/bucket_catch/packages/frontend/src/hooks/puzzleGeometry.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { PuzzlePiece } from "../types"; - -/** - * Board geometry and piece scheduling for the puzzle game. - * - * The interesting part is `assignXPositions`, which is what makes a 100% score - * always achievable: pieces that arrive at the basket at overlapping times are - * clustered so one basket position covers all of them. - */ - -export const BASKET_HALF_WIDTH = 60; -export const BASKET_HEIGHT = 40; -export const PIECE_HALF_W = 48; -export const PIECE_HALF_H = 48; -export const BASKET_Y_OFFSET = 80; -export const MIN_SPEED = 3; -export const SPEED_RANGE = 3; -/** y-half-extent of the catch zone: BASKET_HEIGHT/2 + PIECE_HALF_H */ -export const CATCH_HALF = BASKET_HEIGHT / 2 + PIECE_HALF_H; // 68 px -/** x-half-extent: basket catches a piece within this horizontal distance */ -export const CATCH_RANGE = BASKET_HALF_WIDTH + PIECE_HALF_W; // 108 px -/** Frames between consecutive piece spawn times — controls pacing. */ -export const SPAWN_GAP = 20; - -export interface ScheduledPiece { - piece: PuzzlePiece; - spawnFrame: number; - speed: number; - x: number; -} - -/** - * Computes when each piece enters/exits the catch zone, groups pieces - * whose windows overlap (Union-Find), and assigns x positions so that - * every group fits within one basket-width — guaranteeing 100% is achievable. - */ -export function assignXPositions( - scheduled: ScheduledPiece[], - basketY: number, - canvasW: number, -): void { - const n = scheduled.length; - const parent = Array.from({ length: n }, (_, i) => i); - - const find = (start: number): number => { - let root = start; - while (parent[root] !== root) root = parent[root]; - let i = start; - while (parent[i] !== root) { - const next = parent[i]; - parent[i] = root; - i = next; - } - return root; - }; - - const union = (a: number, b: number): void => { - parent[find(a)] = find(b); - }; - - // Catch window for each piece: the frame range when it's at basket height. - const enters = scheduled.map( - (s) => s.spawnFrame + (basketY - CATCH_HALF + PIECE_HALF_H) / s.speed, - ); - const exits = scheduled.map( - (s) => s.spawnFrame + (basketY + CATCH_HALF + PIECE_HALF_H) / s.speed, - ); - - // Union all pairs whose windows overlap — they arrive simultaneously. - for (let i = 0; i < n; i++) { - for (let j = i + 1; j < n; j++) { - if (enters[i] <= exits[j] && enters[j] <= exits[i]) { - union(i, j); - } - } - } - - // Collect groups. - const groups = new Map(); - for (let i = 0; i < n; i++) { - const root = find(i); - const g = groups.get(root); - if (g !== undefined) { - g.push(i); - } else { - groups.set(root, [i]); - } - } - - // maxOffset: pieces within ±maxOffset of cluster center are all reachable - // from a single basket position (|piece.x - basketX| < CATCH_RANGE iff - // basketX = centerX and |piece.x - centerX| ≤ maxOffset < CATCH_RANGE). - const maxOffset = BASKET_HALF_WIDTH; // 60 px - const minX = PIECE_HALF_W; - const maxX = canvasW - PIECE_HALF_W; - - for (const indices of groups.values()) { - if (indices.length === 1) { - // Solo piece: any position across the full canvas width. - scheduled[indices[0]].x = minX + Math.random() * (maxX - minX); - } else { - // Multi-piece group: cluster within ±maxOffset of a shared center. - const centerMin = PIECE_HALF_W + maxOffset; - const centerMax = canvasW - PIECE_HALF_W - maxOffset; - const centerX = - centerMin < centerMax - ? centerMin + Math.random() * (centerMax - centerMin) - : canvasW / 2; - for (const idx of indices) { - scheduled[idx].x = - centerX + (Math.random() * 2 - 1) * maxOffset; - } - } - } -} - -export function aabbCollision( - fx: number, - fy: number, - bx: number, - by: number, -): boolean { - return ( - Math.abs(fx - bx) < CATCH_RANGE && - Math.abs(fy - by) < BASKET_HEIGHT / 2 + PIECE_HALF_H - ); -} diff --git a/bucket_catch/packages/frontend/src/hooks/puzzleRender.ts b/bucket_catch/packages/frontend/src/hooks/puzzleRender.ts deleted file mode 100644 index 8193303a..00000000 --- a/bucket_catch/packages/frontend/src/hooks/puzzleRender.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { FallingPuzzleItem } from "../types"; -import { - BASKET_HALF_WIDTH, - BASKET_HEIGHT, - PIECE_HALF_H, - PIECE_HALF_W, -} from "./puzzleGeometry"; - -/** - * Canvas drawing for the puzzle game. - * - * Nothing here reads or writes game state — each function takes what it needs - * and paints. Keeping the rendering separate from the tick means the loop in - * `usePuzzleGameLoop` reads as scheduling and collision, not as drawing calls. - */ - -export function drawBasket( - ctx: CanvasRenderingContext2D, - x: number, - y: number, -): void { - const h = BASKET_HEIGHT; - ctx.save(); - ctx.strokeStyle = "#f472b6"; - ctx.lineWidth = 4; - ctx.lineJoin = "round"; - ctx.beginPath(); - ctx.moveTo(x - BASKET_HALF_WIDTH, y - h / 2); - ctx.lineTo(x - BASKET_HALF_WIDTH, y + h / 2); - ctx.lineTo(x + BASKET_HALF_WIDTH, y + h / 2); - ctx.lineTo(x + BASKET_HALF_WIDTH, y - h / 2); - ctx.stroke(); - ctx.strokeStyle = "#e879f9"; - ctx.lineWidth = 6; - ctx.beginPath(); - ctx.moveTo(x - BASKET_HALF_WIDTH - 6, y - h / 2); - ctx.lineTo(x + BASKET_HALF_WIDTH + 6, y - h / 2); - ctx.stroke(); - ctx.restore(); -} - -export function drawPiece( - ctx: CanvasRenderingContext2D, - item: FallingPuzzleItem, - img: HTMLImageElement | undefined, - caught: boolean, -): void { - const w = PIECE_HALF_W * 2; - const h = PIECE_HALF_H * 2; - const x = item.x - PIECE_HALF_W; - const y = item.y - PIECE_HALF_H; - - ctx.save(); - ctx.globalAlpha = caught ? 0.35 : 1; - - if (img?.complete && img.naturalWidth > 0) { - ctx.drawImage(img, x, y, w, h); - } else { - ctx.fillStyle = "rgba(129, 140, 248, 0.5)"; - ctx.fillRect(x, y, w, h); - } - - ctx.strokeStyle = caught ? "#34d399" : "#f472b6"; - ctx.lineWidth = caught ? 3 : 2; - ctx.strokeRect(x, y, w, h); - ctx.restore(); -} - -export function drawHUD( - ctx: CanvasRenderingContext2D, - canvas: HTMLCanvasElement, - caught: number, - total: number, - piecesDone: number, -): void { - ctx.save(); - ctx.fillStyle = "rgba(0,0,0,0.45)"; - ctx.roundRect(canvas.width - 160, 12, 148, 36, 8); - ctx.fill(); - ctx.fillStyle = "#a5b4fc"; - ctx.font = "bold 14px monospace"; - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - ctx.fillText(`✅ ${caught} / ${total}`, canvas.width - 20, 30); - ctx.restore(); - - const barW = 200; - const barX = (canvas.width - barW) / 2; - ctx.save(); - ctx.fillStyle = "rgba(0,0,0,0.4)"; - ctx.roundRect(barX, 12, barW, 8, 4); - ctx.fill(); - ctx.fillStyle = "#818cf8"; - ctx.roundRect(barX, 12, barW * (piecesDone / total), 8, 4); - ctx.fill(); - ctx.restore(); -} - -/** Paints the backdrop gradient the pieces fall against. */ -export function drawBackdrop( - ctx: CanvasRenderingContext2D, - canvas: HTMLCanvasElement, -): void { - ctx.clearRect(0, 0, canvas.width, canvas.height); - const grad = ctx.createLinearGradient(0, 0, 0, canvas.height); - grad.addColorStop(0, "#0f0c29"); - grad.addColorStop(1, "#302b63"); - ctx.fillStyle = grad; - ctx.fillRect(0, 0, canvas.width, canvas.height); -} diff --git a/bucket_catch/packages/frontend/src/hooks/useBasketControl.test.ts b/bucket_catch/packages/frontend/src/hooks/useBasketControl.test.ts deleted file mode 100644 index 8803f799..00000000 --- a/bucket_catch/packages/frontend/src/hooks/useBasketControl.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { renderHook } from "@testing-library/react"; -import { fireEvent } from "@testing-library/react"; -import type React from "react"; -import { useBasketControl } from "./useBasketControl"; - -const makeRef = (val: T): React.RefObject => ({ - current: val, -}); - -describe("useBasketControl", () => { - it("initialises basket x to half of window.innerWidth", () => { - const { result } = renderHook(() => - useBasketControl(makeRef(null)), - ); - expect(result.current.current).toBe(window.innerWidth / 2); - }); - - it("does not attach listener when canvas is null", () => { - const canvasRef = makeRef(null); - // If the effect returned early (canvas null), no error and value unchanged. - const { result } = renderHook(() => useBasketControl(canvasRef)); - expect(result.current.current).toBe(window.innerWidth / 2); - }); - - it("updates basket x on mouse move within canvas", () => { - const canvas = document.createElement("canvas"); - canvas.width = 800; - canvas.getBoundingClientRect = () => - ({ left: 0, top: 0 }) as DOMRect; - - const { result } = renderHook(() => - useBasketControl(makeRef(canvas)), - ); - - fireEvent.mouseMove(canvas, { clientX: 400 }); - expect(result.current.current).toBe(400); - }); - - it("clamps basket x to minimum (BASKET_HALF_WIDTH = 60)", () => { - const canvas = document.createElement("canvas"); - canvas.width = 800; - canvas.getBoundingClientRect = () => ({ left: 0 }) as DOMRect; - - const { result } = renderHook(() => - useBasketControl(makeRef(canvas)), - ); - - fireEvent.mouseMove(canvas, { clientX: 5 }); // x = 5 < 60 - expect(result.current.current).toBe(60); - }); - - it("clamps basket x to maximum (canvas.width - 60)", () => { - const canvas = document.createElement("canvas"); - canvas.width = 800; - canvas.getBoundingClientRect = () => ({ left: 0 }) as DOMRect; - - const { result } = renderHook(() => - useBasketControl(makeRef(canvas)), - ); - - fireEvent.mouseMove(canvas, { clientX: 790 }); // x = 790 > 800 - 60 = 740 - expect(result.current.current).toBe(740); - }); - - it("removes event listener on unmount", () => { - const canvas = document.createElement("canvas"); - canvas.width = 200; - canvas.getBoundingClientRect = () => ({ left: 0 }) as DOMRect; - const removeSpy = vi.spyOn(canvas, "removeEventListener"); - - const { unmount } = renderHook(() => - useBasketControl(makeRef(canvas)), - ); - - unmount(); - expect(removeSpy).toHaveBeenCalledWith("mousemove", expect.any(Function)); - }); -}); diff --git a/bucket_catch/packages/frontend/src/hooks/useBasketControl.ts b/bucket_catch/packages/frontend/src/hooks/useBasketControl.ts deleted file mode 100644 index 7d2638a6..00000000 --- a/bucket_catch/packages/frontend/src/hooks/useBasketControl.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect, useRef } from "react"; - -const BASKET_HALF_WIDTH = 60; - -/** - * Tracks mouse X on the canvas and keeps a ref updated with the clamped basket - * centre X. Uses a ref (not state) to avoid re-renders inside the game loop. - */ -export function useBasketControl( - canvasRef: React.RefObject, -): React.RefObject { - const basketXRef = useRef(window.innerWidth / 2); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - const onMouseMove = (e: MouseEvent): void => { - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - basketXRef.current = Math.max( - BASKET_HALF_WIDTH, - Math.min(canvas.width - BASKET_HALF_WIDTH, x), - ); - }; - - canvas.addEventListener("mousemove", onMouseMove); - return () => { canvas.removeEventListener("mousemove", onMouseMove); }; - }, [canvasRef]); - - return basketXRef; -} diff --git a/bucket_catch/packages/frontend/src/hooks/useGameLoop.test.ts b/bucket_catch/packages/frontend/src/hooks/useGameLoop.test.ts deleted file mode 100644 index 5bdc477d..00000000 --- a/bucket_catch/packages/frontend/src/hooks/useGameLoop.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { renderHook, act, waitFor } from "@testing-library/react"; -import type React from "react"; -import { useGameLoop } from "./useGameLoop"; - -const makeRef = (val: T): React.RefObject => ({ current: val }); - -describe("useGameLoop", () => { - let rafCallbacks: FrameRequestCallback[]; - - beforeEach(() => { - rafCallbacks = []; - vi.stubGlobal( - "requestAnimationFrame", - vi.fn((cb: FrameRequestCallback) => { - rafCallbacks.push(cb); - return rafCallbacks.length; - }), - ); - vi.stubGlobal("cancelAnimationFrame", vi.fn()); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - vi.restoreAllMocks(); - }); - - const flushTick = (): void => { - const cb = rafCallbacks.pop(); - if (cb) act(() => { cb(0); }); - }; - - it("returns null when active=false and does not start RAF", () => { - const { result } = renderHook(() => - useGameLoop( - makeRef(null), - makeRef(400), - [], - false, - ), - ); - expect(result.current).toBeNull(); - expect(requestAnimationFrame).not.toHaveBeenCalled(); - }); - - it("returns null when canvasRef.current is null (active=true)", () => { - renderHook(() => - useGameLoop( - makeRef(null), - makeRef(400), - [new File(["x"], "a.txt")], - true, - ), - ); - expect(requestAnimationFrame).not.toHaveBeenCalled(); - }); - - it("cancels RAF on unmount", () => { - const canvas = document.createElement("canvas"); - canvas.width = 800; - canvas.height = 600; - const { unmount } = renderHook(() => - useGameLoop( - makeRef(canvas), - makeRef(400), - [], - true, - ), - ); - unmount(); - expect(cancelAnimationFrame).toHaveBeenCalled(); - }); - - it("resolves immediately with empty result when files is empty", async () => { - const canvas = document.createElement("canvas"); - canvas.width = 800; - canvas.height = 600; - - const { result } = renderHook(() => - useGameLoop( - makeRef(canvas), - makeRef(400), - [], - true, - ), - ); - - flushTick(); // allDone=true on first frame → setResult - await waitFor(() => { expect(result.current).not.toBeNull(); }); - const settled = result.current; - if (settled === null) { - throw new Error('result never settled'); - } - expect(settled.caught).toHaveLength(0); - expect(settled.missed).toHaveLength(0); - }); - - it("catches file and resolves result (caught branch + caught-draw branch)", async () => { - // Math.random()=0 → file x=48, speed=2.5 - // basketXRef=48 → |fx-bx|=0<108, |fy-by|=35.5<48 → caught on tick 0 - vi.spyOn(Math, "random").mockReturnValue(0); - - const canvas = document.createElement("canvas"); - canvas.width = 800; - canvas.height = 90; // basketY = 90-80 = 10 - - const { result } = renderHook(() => - useGameLoop( - makeRef(canvas), - makeRef(48), - [new File(["x"], "a.txt")], - true, - ), - ); - - flushTick(); // tick 0: caught - flushTick(); // tick 1: draw caught + allDone=true → setResult - - await waitFor(() => { expect(result.current).not.toBeNull(); }); - const settled = result.current; - if (settled === null) { - throw new Error('result never settled'); - } - expect(settled.caught).toHaveLength(1); - expect(settled.missed).toHaveLength(0); - }); - - it("misses file (still-falling + missed branches)", async () => { - // Math.random()=0 → file x=48, speed=2.5; basketXRef=400 - // |48-400|=352 > 108 → no collision; off-bottom at y>118, ~60 ticks - vi.spyOn(Math, "random").mockReturnValue(0); - - const canvas = document.createElement("canvas"); - canvas.width = 800; - canvas.height = 90; - - const { result } = renderHook(() => - useGameLoop( - makeRef(canvas), - makeRef(400), - [new File(["x"], "a.txt")], - true, - ), - ); - - for (let i = 0; i < 65; i++) { - if (result.current !== null) break; - flushTick(); - } - - await waitFor(() => { expect(result.current).not.toBeNull(); }); - const settled = result.current; - if (settled === null) { - throw new Error('result never settled'); - } - expect(settled.caught).toHaveLength(0); - expect(settled.missed).toHaveLength(1); - }); - - it("does not start RAF when canvas.getContext returns null", () => { - // A plain fake canvas whose getContext returns null avoids prototype contamination - const fakeCanvas = { - width: 800, - height: 600, - clientWidth: 800, - clientHeight: 600, - getContext: () => null, - } as unknown as HTMLCanvasElement; - - renderHook(() => - useGameLoop(makeRef(fakeCanvas), makeRef(400), [], true), - ); - - expect(requestAnimationFrame).not.toHaveBeenCalled(); - }); - - it("handles not-yet-spawned branch with two files (startFrames 0 and 50)", async () => { - // Both files: x=48, speed=2.5; basket at 48 → both caught - // File 1 has startFrame=50 → "frame < ff.startFrame" branch hit for ticks 1-49 - vi.spyOn(Math, "random").mockReturnValue(0); - - const canvas = document.createElement("canvas"); - canvas.width = 800; - canvas.height = 90; - - const { result } = renderHook(() => - useGameLoop( - makeRef(canvas), - makeRef(48), - [new File(["a"], "a.txt"), new File(["b"], "b.txt")], - true, - ), - ); - - for (let i = 0; i < 60; i++) { - if (result.current !== null) break; - flushTick(); - } - - await waitFor(() => { expect(result.current).not.toBeNull(); }); - const settled = result.current; - if (settled === null) { - throw new Error('result never settled'); - } - expect(settled.caught).toHaveLength(2); - expect(settled.missed).toHaveLength(0); - }); -}); diff --git a/bucket_catch/packages/frontend/src/hooks/useGameLoop.ts b/bucket_catch/packages/frontend/src/hooks/useGameLoop.ts deleted file mode 100644 index 9974b786..00000000 --- a/bucket_catch/packages/frontend/src/hooks/useGameLoop.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import type { FallingFileItem, FileGameResult } from "../types"; -import { fileIcon, truncateFilename, formatSize } from "../lib/fileIcon"; - -const BASKET_HALF_WIDTH = 60; -const BASKET_HEIGHT = 40; -const FILE_HALF_W = 48; -const FILE_HALF_H = 28; -const SPAWN_INTERVAL = 50; -const BASKET_Y_OFFSET = 80; - -function buildFallingFiles(files: File[], canvasWidth: number): FallingFileItem[] { - return files.map((file, i) => ({ - kind: "file" as const, - id: `${file.name}-${i}`, - file, - x: FILE_HALF_W + Math.random() * (canvasWidth - FILE_HALF_W * 2), - y: -FILE_HALF_H, - speed: 2.5 + Math.random() * 3, - startFrame: i * SPAWN_INTERVAL, - status: "falling" as const, - })); -} - -function aabbCollision( - fx: number, - fy: number, - bx: number, - by: number, -): boolean { - return ( - Math.abs(fx - bx) < BASKET_HALF_WIDTH + FILE_HALF_W && - Math.abs(fy - by) < BASKET_HEIGHT / 2 + FILE_HALF_H - ); -} - -function drawBasket( - ctx: CanvasRenderingContext2D, - x: number, - y: number, -): void { - const h = BASKET_HEIGHT; - ctx.save(); - ctx.strokeStyle = "#f472b6"; - ctx.lineWidth = 4; - ctx.lineJoin = "round"; - ctx.beginPath(); - ctx.moveTo(x - BASKET_HALF_WIDTH, y - h / 2); - ctx.lineTo(x - BASKET_HALF_WIDTH, y + h / 2); - ctx.lineTo(x + BASKET_HALF_WIDTH, y + h / 2); - ctx.lineTo(x + BASKET_HALF_WIDTH, y - h / 2); - ctx.stroke(); - // Rim line above basket opening - ctx.strokeStyle = "#e879f9"; - ctx.lineWidth = 6; - ctx.beginPath(); - ctx.moveTo(x - BASKET_HALF_WIDTH - 6, y - h / 2); - ctx.lineTo(x + BASKET_HALF_WIDTH + 6, y - h / 2); - ctx.stroke(); - ctx.restore(); -} - -function drawFile( - ctx: CanvasRenderingContext2D, - ff: FallingFileItem, - caught: boolean, -): void { - ctx.save(); - ctx.globalAlpha = caught ? 0.4 : 1; - const w = FILE_HALF_W * 2; - const h = FILE_HALF_H * 2; - const x = ff.x - FILE_HALF_W; - const y = ff.y - FILE_HALF_H; - const r = 8; - - ctx.fillStyle = "rgba(30, 27, 75, 0.85)"; - ctx.beginPath(); - ctx.roundRect(x, y, w, h, r); - ctx.fill(); - - ctx.strokeStyle = caught ? "#6b7280" : "#818cf8"; - ctx.lineWidth = 1.5; - ctx.beginPath(); - ctx.roundRect(x, y, w, h, r); - ctx.stroke(); - - ctx.font = "18px sans-serif"; - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - ctx.fillText(fileIcon(ff.file), x + 4, ff.y); - - ctx.fillStyle = caught ? "#6b7280" : "#e0e7ff"; - ctx.font = "11px monospace"; - ctx.textAlign = "left"; - ctx.textBaseline = "top"; - ctx.fillText(truncateFilename(ff.file.name, 10), x + 26, y + 4); - - ctx.fillStyle = "#94a3b8"; - ctx.font = "10px monospace"; - ctx.textBaseline = "bottom"; - ctx.fillText(formatSize(ff.file.size), x + 26, y + h - 4); - - ctx.restore(); -} - -/** - * Runs the osu!catch game loop on the provided canvas. - * Returns the game result when all files are resolved, or null while playing. - */ -export function useGameLoop( - canvasRef: React.RefObject, - basketXRef: React.RefObject, - files: File[], - active: boolean, -): FileGameResult | null { - const [result, setResult] = useState(null); - const stateRef = useRef([]); - const frameRef = useRef(0); - const rafRef = useRef(0); - - const startLoop = useCallback(() => { - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext("2d"); - /* istanbul ignore next */ - if (!ctx) return; - - stateRef.current = buildFallingFiles(files, canvas.width); - frameRef.current = 0; - - const tick = (): void => { - const frame = frameRef.current; - frameRef.current = frame + 1; - - ctx.clearRect(0, 0, canvas.width, canvas.height); - - const grad = ctx.createLinearGradient(0, 0, 0, canvas.height); - grad.addColorStop(0, "#0f0c29"); - grad.addColorStop(1, "#302b63"); - ctx.fillStyle = grad; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - const basketX = basketXRef.current; - const basketY = canvas.height - BASKET_Y_OFFSET; - - drawBasket(ctx, basketX, basketY); - - let allDone = true; - for (const ff of stateRef.current) { - if (ff.status !== "falling") { - if (ff.status === "caught") drawFile(ctx, ff, true); - continue; - } - if (frame < ff.startFrame) { - allDone = false; - continue; - } - allDone = false; - ff.y += ff.speed; - - if (aabbCollision(ff.x, ff.y, basketX, basketY)) { - ff.status = "caught"; - } else if (ff.y > canvas.height + FILE_HALF_H) { - ff.status = "missed"; - } else { - drawFile(ctx, ff, false); - } - } - - if (allDone) { - const caught = stateRef.current - .filter((f) => f.status === "caught") - .map((f) => f.file); - const missed = stateRef.current - .filter((f) => f.status === "missed") - .map((f) => f.file); - setResult({ caught, missed }); - return; - } - - rafRef.current = requestAnimationFrame(tick); - }; - - rafRef.current = requestAnimationFrame(tick); - }, [canvasRef, basketXRef, files]); - - useEffect(() => { - if (!active) return; - startLoop(); - return () => { cancelAnimationFrame(rafRef.current); }; - }, [active, startLoop]); - - return result; -} diff --git a/bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoop.test.ts b/bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoop.test.ts deleted file mode 100644 index 8696e230..00000000 --- a/bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoop.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** Lifecycle and guard paths: when the loop declines to start, and teardown. */ - -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { renderHook, act } from "@testing-library/react"; -import type { PuzzlePiece } from "../types"; -import { usePuzzleGameLoop } from "./usePuzzleGameLoop"; - -vi.mock("../lib/sliceImage"); -import { sliceImage } from "../lib/sliceImage"; -import { - flushPromises, - installRafHarness, - makeCanvas, - makePiece, - makeRef, -} from "../test/puzzleLoopFixtures"; - -describe("usePuzzleGameLoop lifecycle", () => { - beforeEach(() => { - installRafHarness(); - vi.mocked(sliceImage).mockResolvedValue([makePiece(0, 0)]); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - vi.restoreAllMocks(); - }); - - it("returns null when active=false", () => { - const { result } = renderHook(() => - usePuzzleGameLoop( - makeRef(makeCanvas()), - makeRef(400), - new File([""], "img.png"), - 1, - false, - ), - ); - expect(result.current).toBeNull(); - expect(sliceImage).not.toHaveBeenCalled(); - }); - - it("returns null when canvas is null", async () => { - const { result } = renderHook(() => - usePuzzleGameLoop( - makeRef(null), - makeRef(400), - new File([""], "img.png"), - 1, - true, - ), - ); - await flushPromises(); - expect(result.current).toBeNull(); - expect(requestAnimationFrame).not.toHaveBeenCalled(); - }); - - it("does nothing when unmounted before sliceImage resolves (cancelled branch)", async () => { - let resolveSlice!: (pieces: PuzzlePiece[]) => void; - vi.mocked(sliceImage).mockReturnValue( - new Promise((res) => { resolveSlice = res; }), - ); - - const { unmount } = renderHook(() => - usePuzzleGameLoop( - makeRef(makeCanvas()), - makeRef(400), - new File([""], "img.png"), - 1, - true, - ), - ); - - unmount(); // sets cancelled=true before sliceImage resolves - await act(async () => { - resolveSlice([makePiece(0, 0)]); - await Promise.resolve(); - }); - - expect(requestAnimationFrame).not.toHaveBeenCalled(); - }); - - it("cancels RAF on unmount after game started", async () => { - vi.spyOn(Math, "random").mockReturnValue(0); - - const { unmount } = renderHook(() => - usePuzzleGameLoop( - makeRef(makeCanvas()), - makeRef(400), - new File([""], "img.png"), - 1, - true, - ), - ); - - await flushPromises(); - unmount(); - expect(cancelAnimationFrame).toHaveBeenCalled(); - }); - - it("does not start RAF when startLoop canvas.getContext returns null", async () => { - // A plain fake canvas whose getContext returns null avoids prototype contamination - const fakeCanvas = { - width: 800, - height: 300, - clientWidth: 800, - clientHeight: 300, - getContext: () => null, - } as unknown as HTMLCanvasElement; - - renderHook(() => - usePuzzleGameLoop( - makeRef(fakeCanvas), - makeRef(400), - new File([""], "img.png"), - 1, - true, - ), - ); - - await flushPromises(); // sliceImage resolves → startLoop called → null ctx → early return - - expect(requestAnimationFrame).not.toHaveBeenCalled(); - }); -}); diff --git a/bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoop.ts b/bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoop.ts deleted file mode 100644 index b6f2197d..00000000 --- a/bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoop.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { useCallback, useEffect, useRef, useState, type RefObject } from "react"; -import type { FallingPuzzleItem, PuzzlePiece, PuzzleGameResult } from "../types"; -import { sliceImage } from "../lib/sliceImage"; -import { - BASKET_Y_OFFSET, - MIN_SPEED, - PIECE_HALF_H, - SPAWN_GAP, - SPEED_RANGE, - aabbCollision, - assignXPositions, - type ScheduledPiece, -} from "./puzzleGeometry"; -import { drawBackdrop, drawBasket, drawHUD, drawPiece } from "./puzzleRender"; - -/** - * Puzzle game loop. - * - * All pieces are scheduled up-front. Their x positions are assigned via - * interval-graph clustering so that pieces arriving simultaneously share a - * spatial cluster the basket can cover in one position — guaranteeing 100% - * is always achievable. Multiple pieces fall simultaneously, creating an - * exciting hectic visual while remaining fair. - * - * The clustering itself lives in `puzzleGeometry.ts` and the canvas drawing in - * `puzzleRender.ts`; what remains here is the per-frame tick. - */ -export function usePuzzleGameLoop( - canvasRef: RefObject, - basketXRef: RefObject, - imageFile: File, - gridSize: number, - active: boolean, -): PuzzleGameResult | null { - const [result, setResult] = useState(null); - const scheduleRef = useRef([]); - const activeItemsRef = useRef([]); - const resolvedRef = useRef([]); - const resolvedFrameMapRef = useRef>(new Map()); - const frameRef = useRef(0); - const rafRef = useRef(0); - const imgsRef = useRef>(new Map()); - const totalRef = useRef(0); - - const startLoop = useCallback( - (pieces: PuzzlePiece[], canvas: HTMLCanvasElement) => { - const ctx = canvas.getContext("2d"); - /* istanbul ignore next */ - if (!ctx) return; - - const basketY = canvas.height - BASKET_Y_OFFSET; - - // Build schedule: staggered spawn times, random speeds. - const scheduled: ScheduledPiece[] = pieces.map((piece, i) => ({ - piece, - spawnFrame: i * SPAWN_GAP, - speed: MIN_SPEED + Math.random() * SPEED_RANGE, - x: 0, // will be filled by assignXPositions - })); - - // Assign x positions with the spatial clustering guarantee. - assignXPositions(scheduled, basketY, canvas.width); - - // Sort by spawn time (speeds vary, so original order isn't strictly sorted). - scheduled.sort((a, b) => a.spawnFrame - b.spawnFrame); - - scheduleRef.current = scheduled; - activeItemsRef.current = []; - resolvedRef.current = []; - resolvedFrameMapRef.current = new Map(); - frameRef.current = 0; - totalRef.current = pieces.length; - - const tick = (): void => { - const frame = frameRef.current; - frameRef.current = frame + 1; - - drawBackdrop(ctx, canvas); - - const basketX = basketXRef.current; - drawBasket(ctx, basketX, basketY); - - // Spawn pieces whose time has come. - while ( - scheduleRef.current.length > 0 && - frame >= scheduleRef.current[0].spawnFrame - ) { - // The while condition already indexes [0] unasserted, so this is - // the same non-null value; shift() only advances the queue. - const s = scheduleRef.current[0]; - scheduleRef.current.shift(); - activeItemsRef.current.push({ - kind: "puzzle", - id: `puzzle-${s.piece.row}-${s.piece.col}`, - piece: s.piece, - x: s.x, - y: -PIECE_HALF_H, - speed: s.speed, - startFrame: frame, - status: "falling", - }); - } - - // Update all falling pieces. - const stillActive: FallingPuzzleItem[] = []; - for (const item of activeItemsRef.current) { - const img = imgsRef.current.get(`${item.piece.row}-${item.piece.col}`); - item.y += item.speed; - - if (aabbCollision(item.x, item.y, basketX, basketY)) { - item.status = "caught"; - resolvedRef.current.push(item); - resolvedFrameMapRef.current.set(item.id, frame); - } else if (item.y > canvas.height + PIECE_HALF_H) { - item.status = "missed"; - resolvedRef.current.push(item); - resolvedFrameMapRef.current.set(item.id, frame); - } else { - drawPiece(ctx, item, img, false); - stillActive.push(item); - } - } - activeItemsRef.current = stillActive; - - // Flash caught pieces briefly at their catch position. - for (const item of resolvedRef.current) { - if (item.status === "caught") { - // id is always set in the map when piece is resolved (lines above) - const rf = resolvedFrameMapRef.current.get(item.id)!; - if (frame - rf < 30) { - const img = imgsRef.current.get( - `${item.piece.row}-${item.piece.col}`, - ); - drawPiece(ctx, item, img, true); - } - } - } - - const caughtCount = resolvedRef.current.filter( - (p) => p.status === "caught", - ).length; - drawHUD( - ctx, - canvas, - caughtCount, - totalRef.current, - resolvedRef.current.length, - ); - - if ( - activeItemsRef.current.length === 0 && - scheduleRef.current.length === 0 - ) { - const caught = resolvedRef.current.filter( - (p) => p.status === "caught", - ); - const missed = resolvedRef.current.filter( - (p) => p.status === "missed", - ); - setResult({ caughtPieces: caught, missedPieces: missed, gridSize }); - return; - } - - rafRef.current = requestAnimationFrame(tick); - }; - - rafRef.current = requestAnimationFrame(tick); - }, - [basketXRef, gridSize], - ); - - useEffect(() => { - if (!active) return; - const canvas = canvasRef.current; - if (!canvas) return; - - let cancelled = false; - - void sliceImage(imageFile, gridSize).then((pieces) => { - if (cancelled) return; - pieces.forEach((piece) => { - const img = new Image(); - img.src = piece.imageUrl; - imgsRef.current.set(`${piece.row}-${piece.col}`, img); - }); - startLoop(pieces, canvas); - }); - - return () => { - cancelled = true; - cancelAnimationFrame(rafRef.current); - }; - }, [active, imageFile, gridSize, canvasRef, startLoop]); - - return result; -} diff --git a/bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoopPlay.test.ts b/bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoopPlay.test.ts deleted file mode 100644 index 80457ad6..00000000 --- a/bucket_catch/packages/frontend/src/hooks/usePuzzleGameLoopPlay.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Gameplay paths: catching, missing, the catch flash, piece rendering, and the - * two x-position clustering branches in assignXPositions. - */ - -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { renderHook, waitFor } from "@testing-library/react"; -import { usePuzzleGameLoop } from "./usePuzzleGameLoop"; - -vi.mock("../lib/sliceImage"); -import { sliceImage } from "../lib/sliceImage"; -import { - flushPromises, - installRafHarness, - makeCanvas, - makePiece, - makeRef, - type FlushTick, -} from "../test/puzzleLoopFixtures"; - -describe("usePuzzleGameLoop play", () => { - let flushTick: FlushTick; - - beforeEach(() => { - flushTick = installRafHarness(); - vi.mocked(sliceImage).mockResolvedValue([makePiece(0, 0)]); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - vi.restoreAllMocks(); - }); - - it("catches single piece and resolves result (solo x branch, drawPiece no-img branch)", async () => { - // Math.random()=0 → speed=3, x=48 (solo: minX + 0*(maxX-minX) = 48) - // basketXRef=48 → caught on tick 0; game ends on same tick - vi.spyOn(Math, "random").mockReturnValue(0); - vi.mocked(sliceImage).mockResolvedValue([makePiece(0, 0)]); - - const { result } = renderHook(() => - usePuzzleGameLoop( - makeRef(makeCanvas(800, 90)), - makeRef(48), - new File([""], "img.png"), - 1, - true, - ), - ); - - await flushPromises(); // let sliceImage resolve and startLoop run - flushTick(); // tick 0: spawn, move, caught, flash, allDone → setResult - - await waitFor(() => { expect(result.current).not.toBeNull(); }); - const settled = result.current; - if (settled === null) { - throw new Error('result never settled'); - } - expect(settled.caughtPieces).toHaveLength(1); - expect(settled.missedPieces).toHaveLength(0); - }); - - it("misses piece (still-falling + missed branches)", async () => { - // Math.random()=0 → speed=3, x=48; basket at 500 → no collision - // piece off bottom at y>90+48=138 → ~62 ticks - vi.spyOn(Math, "random").mockReturnValue(0); - - const { result } = renderHook(() => - usePuzzleGameLoop( - makeRef(makeCanvas(800, 90)), - makeRef(500), - new File([""], "img.png"), - 1, - true, - ), - ); - - await flushPromises(); - for (let i = 0; i < 70; i++) { - if (result.current !== null) break; - flushTick(); - } - - await waitFor(() => { expect(result.current).not.toBeNull(); }); - const settled = result.current; - if (settled === null) { - throw new Error('result never settled'); - } - expect(settled.caughtPieces).toHaveLength(0); - expect(settled.missedPieces).toHaveLength(1); - }); - - it("tests flash-during and flash-expired branches with 2 pieces", async () => { - // Piece 0: x=48, speed=3, spawnFrame=0 → caught by basket@48 on tick 0 - // Piece 1: x=168, speed=3, spawnFrame=20 → |168-48|=120>108 → missed at tick~82 - // Flash: piece 0 caught at frame 0; at frame 30+ flash expires - vi.spyOn(Math, "random") - .mockReturnValueOnce(0) // speed[0] = 3 - .mockReturnValueOnce(0) // speed[1] = 3 - .mockReturnValueOnce(0) // centerX = 108 (108 + 0*(692-108)) - .mockReturnValueOnce(0) // x[0] = 108 + (0*2-1)*60 = 48 - .mockReturnValueOnce(1); // x[1] = 108 + (1*2-1)*60 = 168 - - vi.mocked(sliceImage).mockResolvedValue([ - makePiece(0, 0), - { ...makePiece(0, 1), gridSize: 2 }, - ]); - - const { result } = renderHook(() => - usePuzzleGameLoop( - makeRef(makeCanvas(800, 90)), - makeRef(48), - new File([""], "img.png"), - 2, - true, - ), - ); - - await flushPromises(); - for (let i = 0; i < 90; i++) { - if (result.current !== null) break; - flushTick(); - } - - await waitFor(() => { expect(result.current).not.toBeNull(); }); - const settled = result.current; - if (settled === null) { - throw new Error('result never settled'); - } - expect(settled.caughtPieces).toHaveLength(1); - expect(settled.missedPieces).toHaveLength(1); - }); - - it("uses drawImage when piece image is loaded (img.complete=true branch)", async () => { - // Class constructor — arrow functions can't be used with `new` - class FakeImage { - complete = true; - naturalWidth = 100; - src = ""; - } - vi.stubGlobal("Image", FakeImage); - - vi.spyOn(Math, "random").mockReturnValue(0); - - // Use large canvas so piece doesn't collide on first tick - const canvas = makeCanvas(800, 600); // basketY=520; piece reaches it at tick~190 - renderHook(() => - usePuzzleGameLoop( - makeRef(canvas), - makeRef(500), // far from x=48 → piece will miss - new File([""], "img.png"), - 1, - true, - ), - ); - - await flushPromises(); - - // Run one tick — piece is still falling, drawPiece called with loaded img - flushTick(); - - // Verify drawImage was called (piece has complete+naturalWidth>0) - const { mockCtx } = await import("../test/canvasMock"); - expect(mockCtx.drawImage).toHaveBeenCalled(); - }); - - it("triggers Union-Find path compression with 4 overlapping pieces", async () => { - vi.spyOn(Math, "random").mockReturnValue(0); - vi.mocked(sliceImage).mockResolvedValue([ - makePiece(0, 0), - makePiece(0, 1), - makePiece(1, 0), - makePiece(1, 1), - ]); - - // basketY = 300-80 = 220; all 4 pieces overlap (SPAWN_GAP=20 and speed=3) - const canvas = makeCanvas(800, 300); - const { unmount } = renderHook(() => - usePuzzleGameLoop( - makeRef(canvas), - makeRef(48), - new File([""], "img.png"), - 2, - true, - ), - ); - - await flushPromises(); // sliceImage resolves → assignXPositions (path compression) runs - flushTick(); // one tick to confirm loop is running - - unmount(); - }); - - it("uses canvasW/2 for centerX when canvas is too narrow", async () => { - // canvas.width=200: centerMin=108 >= centerMax=92 → uses 200/2=100 - vi.spyOn(Math, "random").mockReturnValue(0.5); - - vi.mocked(sliceImage).mockResolvedValue([ - makePiece(0, 0), - makePiece(0, 1), - ]); - - const canvas = makeCanvas(200, 90); // basketY=10 - // With centerX=100, both pieces at x=100 (offset = 0); basket@100 → caught - const { result } = renderHook(() => - usePuzzleGameLoop( - makeRef(canvas), - makeRef(100), - new File([""], "img.png"), - 2, - true, - ), - ); - - await flushPromises(); - for (let i = 0; i < 30; i++) { - if (result.current !== null) break; - flushTick(); - } - - await waitFor(() => { expect(result.current).not.toBeNull(); }); - }); -}); diff --git a/bucket_catch/packages/frontend/src/index.css b/bucket_catch/packages/frontend/src/index.css deleted file mode 100644 index 8fb90b6b..00000000 --- a/bucket_catch/packages/frontend/src/index.css +++ /dev/null @@ -1,85 +0,0 @@ -/* Design tokens — the shared source of truth every *.module.css file - should reference instead of its own hardcoded hex literals. Previously - absent entirely (rule 4), which is how the same page-background/heading - gradients ended up copy-pasted verbatim across 4 files, one of them with - the gradient stops reversed (PuzzleResult.module.css, and ModeSelect's - puzzleStart button — same bug, two spots). */ -:root { - /* Page background (3-stop gradient) */ - --bg-1: #0f0c29; - --bg-2: #302b63; - --bg-3: #24243e; - --bg-gradient: linear-gradient(135deg, var(--bg-1), var(--bg-2), var(--bg-3)); - - /* Drag-over state (DropZone only) */ - --bg-drag-1: #1a1560; - --bg-drag-2: #4a3f9e; - --bg-drag-3: #3a3868; - --bg-drag-gradient: linear-gradient( - 135deg, - var(--bg-drag-1), - var(--bg-drag-2), - var(--bg-drag-3) - ); - - /* Accent pair. --gradient-accent is the ONE canonical direction - (indigo -> pink) for every heading/primary-button gradient in the - app — always use this token, never write the stops out by hand. */ - --color-indigo: #818cf8; - --color-indigo-rgb: 129, 140, 248; - --color-pink: #f472b6; - --color-pink-rgb: 244, 114, 182; - --gradient-accent: linear-gradient(90deg, var(--color-indigo), var(--color-pink)); - - --color-indigo-light: #a5b4fc; - --color-indigo-lighter: #c7d2fe; - --color-pink-light: #f9a8d4; - --color-pink-lighter: #e879f9; - - --color-text: #e0e7ff; - --color-text-rgb: 224, 231, 255; - - /* Raised-surface fill (cards, file rows, the puzzle grid) — one base - color at different opacities/solidity, not independent literals. */ - --surface-rgb: 30, 27, 75; - --surface: rgba(var(--surface-rgb), 0.7); - --surface-solid: rgb(var(--surface-rgb)); - - --color-success: #34d399; - --color-error: #f87171; - - /* Muted grays — was 4 unrelated shades (rule 2); collapsed to 2 named, - semantically distinct roles: body-adjacent secondary text, and a - slightly darker tertiary/caption tone. (Borders get their own token - below, since one of them needed a contrast fix rule 7 flagged.) */ - --muted-text: #94a3b8; - --muted-faint: #6b7280; - --muted-border: #4b5563; - - /* Spacing scale (4px base) */ - --space-1: 4px; - --space-2: 8px; - --space-3: 16px; - --space-4: 24px; - --space-5: 32px; - --space-6: 40px; - - /* Corner radius scale */ - --radius-sm: 6px; - --radius-md: 8px; - --radius-lg: 16px; - --radius-pill: 999px; -} - -*, *::before, *::after { - box-sizing: border-box; -} - -html, body, #root { - margin: 0; - padding: 0; - height: 100%; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - background: var(--bg-1); - color: var(--color-text); -} diff --git a/bucket_catch/packages/frontend/src/lib/fileIcon.test.ts b/bucket_catch/packages/frontend/src/lib/fileIcon.test.ts deleted file mode 100644 index de3a7412..00000000 --- a/bucket_catch/packages/frontend/src/lib/fileIcon.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { fileIcon, truncateFilename, formatSize } from "./fileIcon"; - -const makeFile = (name: string, type = "") => - new File([""], name, { type }); - -describe("fileIcon", () => { - it("returns image emoji for image/* MIME", () => { - expect(fileIcon(makeFile("a.png", "image/png"))).toBe("🖼️"); - }); - it("returns video emoji for video/* MIME", () => { - expect(fileIcon(makeFile("a.mp4", "video/mp4"))).toBe("🎬"); - }); - it("returns audio emoji for audio/* MIME", () => { - expect(fileIcon(makeFile("a.mp3", "audio/mpeg"))).toBe("🎵"); - }); - it("returns pdf emoji for application/pdf", () => { - expect(fileIcon(makeFile("a.pdf", "application/pdf"))).toBe("📄"); - }); - it("returns archive emoji for application/zip", () => { - expect(fileIcon(makeFile("a.zip", "application/zip"))).toBe("📦"); - }); - it("returns archive emoji for application/x-zip-compressed", () => { - expect(fileIcon(makeFile("a.zip", "application/x-zip-compressed"))).toBe("📦"); - }); - it("returns archive emoji for .zip extension", () => { - expect(fileIcon(makeFile("a.zip"))).toBe("📦"); - }); - it("returns archive emoji for .tar.gz extension", () => { - expect(fileIcon(makeFile("a.tar.gz"))).toBe("📦"); - }); - it("returns archive emoji for .rar extension", () => { - expect(fileIcon(makeFile("a.rar"))).toBe("📦"); - }); - it("returns code emoji for javascript MIME", () => { - expect(fileIcon(makeFile("a.js", "application/javascript"))).toBe("💻"); - }); - it("returns code emoji for typescript MIME", () => { - expect(fileIcon(makeFile("a.ts", "application/typescript"))).toBe("💻"); - }); - it("returns code emoji for .ts extension", () => { - expect(fileIcon(makeFile("a.ts"))).toBe("💻"); - }); - it("returns code emoji for .tsx extension", () => { - expect(fileIcon(makeFile("a.tsx"))).toBe("💻"); - }); - it("returns code emoji for .py extension", () => { - expect(fileIcon(makeFile("a.py"))).toBe("💻"); - }); - it("returns code emoji for .go extension", () => { - expect(fileIcon(makeFile("a.go"))).toBe("💻"); - }); - it("returns code emoji for .rs extension", () => { - expect(fileIcon(makeFile("a.rs"))).toBe("💻"); - }); - it("returns code emoji for .c extension", () => { - expect(fileIcon(makeFile("a.c"))).toBe("💻"); - }); - it("returns code emoji for .cpp extension", () => { - expect(fileIcon(makeFile("a.cpp"))).toBe("💻"); - }); - it("returns code emoji for .java extension", () => { - expect(fileIcon(makeFile("a.java"))).toBe("💻"); - }); - it("returns code emoji for .sh extension", () => { - expect(fileIcon(makeFile("a.sh"))).toBe("💻"); - }); - it("returns text emoji for text/* MIME", () => { - expect(fileIcon(makeFile("a.txt", "text/plain"))).toBe("📝"); - }); - it("returns text emoji for .md extension", () => { - expect(fileIcon(makeFile("a.md"))).toBe("📝"); - }); - it("returns text emoji for .csv extension", () => { - expect(fileIcon(makeFile("a.csv"))).toBe("📝"); - }); - it("returns text emoji for .log extension", () => { - expect(fileIcon(makeFile("a.log"))).toBe("📝"); - }); - it("returns generic emoji for unknown type", () => { - expect(fileIcon(makeFile("a.xyz"))).toBe("📎"); - }); -}); - -describe("truncateFilename", () => { - it("returns name unchanged when short enough", () => { - expect(truncateFilename("hello.txt", 18)).toBe("hello.txt"); - }); - it("uses default max of 18", () => { - expect(truncateFilename("short")).toBe("short"); - }); - it("truncates long name with extension", () => { - const result = truncateFilename("averylongfilename.txt", 18); - expect(result.length).toBeLessThanOrEqual(18); - expect(result).toContain("…"); - expect(result).toContain(".txt"); - }); - it("truncates long name without extension", () => { - const result = truncateFilename("averylongnamenoext", 10); - expect(result.length).toBeLessThanOrEqual(10); - expect(result).toContain("…"); - }); -}); - -describe("formatSize", () => { - it("formats bytes under 1 KB", () => { - expect(formatSize(512)).toBe("512 B"); - }); - it("formats bytes under 1 MB as KB", () => { - expect(formatSize(2048)).toBe("2.0 KB"); - }); - it("formats bytes 1 MB and over as MB", () => { - expect(formatSize(2 * 1024 * 1024)).toBe("2.0 MB"); - }); -}); diff --git a/bucket_catch/packages/frontend/src/lib/fileIcon.ts b/bucket_catch/packages/frontend/src/lib/fileIcon.ts deleted file mode 100644 index fb87ec1a..00000000 --- a/bucket_catch/packages/frontend/src/lib/fileIcon.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** Returns an emoji representing the file type based on MIME or extension. */ -export function fileIcon(file: File): string { - const mime = file.type; - if (mime.startsWith("image/")) return "🖼️"; - if (mime.startsWith("video/")) return "🎬"; - if (mime.startsWith("audio/")) return "🎵"; - if (mime === "application/pdf") return "📄"; - if ( - mime === "application/zip" || - mime === "application/x-zip-compressed" || - file.name.endsWith(".zip") || - file.name.endsWith(".tar.gz") || - file.name.endsWith(".rar") - ) - return "📦"; - if ( - mime.includes("javascript") || - mime.includes("typescript") || - (/\.(ts|tsx|js|jsx|py|go|rs|c|cpp|java|sh)$/.exec(file.name)) - ) - return "💻"; - if ( - mime.includes("text") || - (/\.(txt|md|csv|log)$/.exec(file.name)) - ) - return "📝"; - return "📎"; -} - -/** Truncate a filename to at most `max` characters, keeping extension. */ -export function truncateFilename(name: string, max = 18): string { - if (name.length <= max) return name; - const dot = name.lastIndexOf("."); - if (dot > 0) { - const ext = name.slice(dot); - return name.slice(0, max - ext.length - 1) + "…" + ext; - } - return name.slice(0, max - 1) + "…"; -} - -/** Human-readable file size. */ -export function formatSize(bytes: number): string { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; -} diff --git a/bucket_catch/packages/frontend/src/lib/sliceImage.test.ts b/bucket_catch/packages/frontend/src/lib/sliceImage.test.ts deleted file mode 100644 index 87e0fe4f..00000000 --- a/bucket_catch/packages/frontend/src/lib/sliceImage.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import type { MockInstance } from "vitest"; -import { sliceImage } from "./sliceImage"; - -// Captures the last Image instance created so tests can trigger onload/onerror. -let capturedImg: { - onload: (() => void) | null; - onerror: (() => void) | null; - src: string; - width: number; - height: number; -}; - -class FakeImage { - onload: (() => void) | null = null; - onerror: (() => void) | null = null; - src = ""; - width = 200; - height = 100; - constructor() { - capturedImg = this; - } -} - -describe("sliceImage", () => { - let revokeUrlSpy: MockInstance; - - beforeEach(() => { - vi.stubGlobal("Image", FakeImage); - vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:mock"); - revokeUrlSpy = vi - .spyOn(URL, "revokeObjectURL") - .mockImplementation(() => undefined); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - vi.restoreAllMocks(); - }); - - it("resolves with pieces when image loads", async () => { - const file = new File([""], "img.png", { type: "image/png" }); - const promise = sliceImage(file, 2); - - // Trigger onload synchronously — sliceImage has already assigned it. - capturedImg.onload?.(); - - const pieces = await promise; - expect(pieces).toHaveLength(4); // 2×2 grid - expect(pieces[0]).toMatchObject({ row: 0, col: 0, gridSize: 2 }); - expect(pieces[3]).toMatchObject({ row: 1, col: 1, gridSize: 2 }); - expect(revokeUrlSpy).toHaveBeenCalledWith("blob:mock"); - }); - - it("includes pieceWidth and pieceHeight from image dimensions", async () => { - const file = new File([""], "img.png"); - const promise = sliceImage(file, 2); - capturedImg.onload?.(); - const pieces = await promise; - // FakeImage.width=200, height=100, gridSize=2 → pieceWidth=100, pieceHeight=50 - expect(pieces[0].pieceWidth).toBe(100); - expect(pieces[0].pieceHeight).toBe(50); - }); - - it("resolves with 1 piece for 1×1 grid", async () => { - const file = new File([""], "img.png"); - const promise = sliceImage(file, 1); - capturedImg.onload?.(); - const pieces = await promise; - expect(pieces).toHaveLength(1); - }); - - it("rejects when offscreen canvas getContext returns null", async () => { - // Save and override the prototype mock to return null for this test only - const savedImpl = HTMLCanvasElement.prototype.getContext; - HTMLCanvasElement.prototype.getContext = (() => null) as typeof HTMLCanvasElement.prototype.getContext; - - const file = new File([""], "img.png"); - const promise = sliceImage(file, 1); - capturedImg.onload?.(); - await expect(promise).rejects.toThrow("Canvas 2D context not available"); - expect(revokeUrlSpy).toHaveBeenCalledWith("blob:mock"); - - // Restore so subsequent tests in this file have the mock context - HTMLCanvasElement.prototype.getContext = savedImpl; - }); - - it("rejects when image fails to load", async () => { - const file = new File([""], "broken.jpg"); - const promise = sliceImage(file, 2); - capturedImg.onerror?.(); - await expect(promise).rejects.toThrow("Failed to load image for slicing"); - expect(revokeUrlSpy).toHaveBeenCalledWith("blob:mock"); - }); -}); diff --git a/bucket_catch/packages/frontend/src/lib/sliceImage.ts b/bucket_catch/packages/frontend/src/lib/sliceImage.ts deleted file mode 100644 index 52780ec3..00000000 --- a/bucket_catch/packages/frontend/src/lib/sliceImage.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { PuzzlePiece } from "../types"; - -/** Slice an image file into a gridSize×gridSize grid of data-URL pieces. */ -export function sliceImage(file: File, gridSize: number): Promise { - return new Promise((resolve, reject) => { - const img = new Image(); - const url = URL.createObjectURL(file); - - img.onload = () => { - const pieceWidth = Math.floor(img.width / gridSize); - const pieceHeight = Math.floor(img.height / gridSize); - const pieces: PuzzlePiece[] = []; - - for (let row = 0; row < gridSize; row++) { - for (let col = 0; col < gridSize; col++) { - const offscreen = document.createElement("canvas"); - offscreen.width = pieceWidth; - offscreen.height = pieceHeight; - const ctx = offscreen.getContext("2d"); - /* istanbul ignore next */ - if (!ctx) { - URL.revokeObjectURL(url); - reject(new Error("Canvas 2D context not available")); - return; - } - ctx.drawImage( - img, - col * pieceWidth, - row * pieceHeight, - pieceWidth, - pieceHeight, - 0, - 0, - pieceWidth, - pieceHeight, - ); - pieces.push({ - row, - col, - gridSize, - imageUrl: offscreen.toDataURL(), - pieceWidth, - pieceHeight, - }); - } - } - - URL.revokeObjectURL(url); - resolve(pieces); - }; - - img.onerror = () => { - URL.revokeObjectURL(url); - reject(new Error("Failed to load image for slicing")); - }; - - img.src = url; - }); -} diff --git a/bucket_catch/packages/frontend/src/lib/uploadFiles.test.ts b/bucket_catch/packages/frontend/src/lib/uploadFiles.test.ts deleted file mode 100644 index b0c8d9c4..00000000 --- a/bucket_catch/packages/frontend/src/lib/uploadFiles.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { uploadFiles } from "./uploadFiles"; - -describe("uploadFiles", () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - it("uploads multiple files sequentially and returns results", async () => { - const mockResult = { - filename: "stored.txt", - originalname: "test.txt", - size: 5, - savedAt: "2024-01-01", - }; - vi.stubGlobal( - "fetch", - vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve(mockResult), - }), - ); - - const files = [ - new File(["hello"], "a.txt"), - new File(["world"], "b.txt"), - ]; - const results = await uploadFiles(files); - - expect(fetch).toHaveBeenCalledTimes(2); - expect(results).toHaveLength(2); - expect(results[0]).toEqual(mockResult); - }); - - it("throws when server returns non-ok response", async () => { - vi.stubGlobal( - "fetch", - vi.fn().mockResolvedValue({ - ok: false, - statusText: "Bad Request", - }), - ); - - const files = [new File(["x"], "fail.txt")]; - await expect(uploadFiles(files)).rejects.toThrow( - "Upload failed for fail.txt: Bad Request", - ); - }); - - it("returns empty array for empty input", async () => { - vi.stubGlobal("fetch", vi.fn()); - const results = await uploadFiles([]); - expect(results).toEqual([]); - expect(fetch).not.toHaveBeenCalled(); - }); -}); diff --git a/bucket_catch/packages/frontend/src/lib/uploadFiles.ts b/bucket_catch/packages/frontend/src/lib/uploadFiles.ts deleted file mode 100644 index 6d910ba6..00000000 --- a/bucket_catch/packages/frontend/src/lib/uploadFiles.ts +++ /dev/null @@ -1,28 +0,0 @@ -const BACKEND_URL = "http://localhost:3000/files/upload"; - -export interface UploadResult { - filename: string; - originalname: string; - size: number; - savedAt: string; -} - -/** Upload a single file to the backend. */ -async function uploadOne(file: File): Promise { - const form = new FormData(); - form.append("file", file); - const res = await fetch(BACKEND_URL, { method: "POST", body: form }); - if (!res.ok) { - throw new Error(`Upload failed for ${file.name}: ${res.statusText}`); - } - return res.json() as Promise; -} - -/** Upload all caught files sequentially to the backend. */ -export async function uploadFiles(files: File[]): Promise { - const results: UploadResult[] = []; - for (const file of files) { - results.push(await uploadOne(file)); - } - return results; -} diff --git a/bucket_catch/packages/frontend/src/lib/zipDownload.test.ts b/bucket_catch/packages/frontend/src/lib/zipDownload.test.ts deleted file mode 100644 index 9d2568bd..00000000 --- a/bucket_catch/packages/frontend/src/lib/zipDownload.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import type { MockInstance } from "vitest"; -import { zipDownload } from "./zipDownload"; - -describe("zipDownload", () => { - let createUrlSpy: MockInstance; - let revokeUrlSpy: MockInstance; - let clickSpy: MockInstance; - - beforeEach(() => { - createUrlSpy = vi - .spyOn(URL, "createObjectURL") - .mockReturnValue("blob:mock-url"); - revokeUrlSpy = vi - .spyOn(URL, "revokeObjectURL") - .mockImplementation(() => undefined); - clickSpy = vi - .spyOn(HTMLAnchorElement.prototype, "click") - .mockImplementation(() => undefined); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("creates a zip blob, triggers download, and cleans up", async () => { - const appendSpy = vi.spyOn(document.body, "appendChild"); - const removeSpy = vi.spyOn(document.body, "removeChild"); - - const file = new File(["hello"], "hello.txt"); - await zipDownload([file]); - - expect(createUrlSpy).toHaveBeenCalled(); - expect(clickSpy).toHaveBeenCalled(); - expect(revokeUrlSpy).toHaveBeenCalledWith("blob:mock-url"); - expect(appendSpy).toHaveBeenCalled(); - expect(removeSpy).toHaveBeenCalled(); - }); - - it("sets download attribute and href on the anchor", async () => { - const anchors: HTMLAnchorElement[] = []; - // Bound before the spy replaces it, so the implementation below can build - // a real element without recursing into its own mock. - const realCreateElement = document.createElement.bind(document); - vi.spyOn(document, "createElement").mockImplementation((tag: string) => { - const el = realCreateElement(tag); - if (el instanceof HTMLAnchorElement) anchors.push(el); - return el; - }); - // Restore after spy above since we used bind trick. That discards the - // handles from beforeEach, so re-spy and keep the new ones. - vi.restoreAllMocks(); - vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:mock-url"); - const localRevokeSpy = vi - .spyOn(URL, "revokeObjectURL") - .mockImplementation(() => undefined); - vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation( - () => undefined, - ); - - const file = new File(["x"], "data.bin"); - await zipDownload([file]); - - expect(localRevokeSpy).toHaveBeenCalledWith("blob:mock-url"); - }); - - it("works with multiple files", async () => { - const files = [ - new File(["a"], "a.txt"), - new File(["b"], "b.txt"), - new File(["c"], "c.png", { type: "image/png" }), - ]; - await expect(zipDownload(files)).resolves.toBeUndefined(); - expect(createUrlSpy).toHaveBeenCalledTimes(1); - expect(revokeUrlSpy).toHaveBeenCalledTimes(1); - }); -}); diff --git a/bucket_catch/packages/frontend/src/lib/zipDownload.ts b/bucket_catch/packages/frontend/src/lib/zipDownload.ts deleted file mode 100644 index 86064781..00000000 --- a/bucket_catch/packages/frontend/src/lib/zipDownload.ts +++ /dev/null @@ -1,18 +0,0 @@ -import JSZip from "jszip"; - -/** Bundle the given files into a zip and trigger a browser download. */ -export async function zipDownload(files: File[]): Promise { - const zip = new JSZip(); - for (const file of files) { - zip.file(file.name, file); - } - const blob = await zip.generateAsync({ type: "blob" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "caught-files.zip"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} diff --git a/bucket_catch/packages/frontend/src/main.tsx b/bucket_catch/packages/frontend/src/main.tsx deleted file mode 100644 index a721b862..00000000 --- a/bucket_catch/packages/frontend/src/main.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App"; -import "./index.css"; - -const root = document.getElementById("root"); -if (!root) throw new Error("Missing #root element"); -ReactDOM.createRoot(root).render( - - - , -); diff --git a/bucket_catch/packages/frontend/src/test/canvasMock.ts b/bucket_catch/packages/frontend/src/test/canvasMock.ts deleted file mode 100644 index 2e2cd0db..00000000 --- a/bucket_catch/packages/frontend/src/test/canvasMock.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { vi } from "vitest"; - -export const mockGradient = { addColorStop: vi.fn() }; - -export const mockCtx = { - clearRect: vi.fn(), - fillRect: vi.fn(), - strokeRect: vi.fn(), - drawImage: vi.fn(), - fillText: vi.fn(), - measureText: vi.fn(() => ({ width: 0 })), - save: vi.fn(), - restore: vi.fn(), - beginPath: vi.fn(), - arc: vi.fn(), - fill: vi.fn(), - stroke: vi.fn(), - moveTo: vi.fn(), - lineTo: vi.fn(), - roundRect: vi.fn(), - createLinearGradient: vi.fn(() => mockGradient), - fillStyle: "" as string | CanvasGradient | CanvasPattern, - strokeStyle: "" as string | CanvasGradient | CanvasPattern, - lineWidth: 1, - lineJoin: "miter" as CanvasLineJoin, - font: "", - textAlign: "start" as CanvasTextAlign, - textBaseline: "alphabetic" as CanvasTextBaseline, - globalAlpha: 1, -}; diff --git a/bucket_catch/packages/frontend/src/test/domQueries.ts b/bucket_catch/packages/frontend/src/test/domQueries.ts deleted file mode 100644 index ab2a96f8..00000000 --- a/bucket_catch/packages/frontend/src/test/domQueries.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * DOM query helpers shared by the component tests. - * - * Lives under src/test/ because the coverage config excludes that path — a - * helper placed beside a component would be counted as production code and - * break the 100% gate. - */ - -/** - * Query one element and narrow it, failing the test if it is absent. - * - * Both `as HTMLElement` and `!` are rejected here — the former by - * non-nullable-type-assertion-style, the latter by no-non-null-assertion — so - * the narrowing is done with a real check that also gives a useful message. - */ -export const must = (el: Element | null, what: string): HTMLElement => { - if (!(el instanceof HTMLElement)) { - throw new Error(`expected to find ${what}`); - } - return el; -}; diff --git a/bucket_catch/packages/frontend/src/test/puzzleLoopFixtures.ts b/bucket_catch/packages/frontend/src/test/puzzleLoopFixtures.ts deleted file mode 100644 index d58b76bd..00000000 --- a/bucket_catch/packages/frontend/src/test/puzzleLoopFixtures.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { act } from "@testing-library/react"; -import type React from "react"; -import type { PuzzlePiece } from "../types"; - -/** - * Shared fixtures for the usePuzzleGameLoop tests. - * - * Lives under src/test/ because the coverage config excludes that path — a - * fixture placed beside the hook would be counted as production code and break - * the 100% gate. - */ - -export const makeRef = (val: T): React.RefObject => ({ - current: val, -}); - -export const makePiece = (row: number, col: number): PuzzlePiece => ({ - row, - col, - gridSize: 1, - imageUrl: "data:image/png;base64,mock", - pieceWidth: 50, - pieceHeight: 50, -}); - -export const makeCanvas = (w = 800, h = 90): HTMLCanvasElement => { - const c = document.createElement("canvas"); - c.width = w; - c.height = h; - return c; -}; - -/** Drives one queued animation frame, if any is pending. */ -export type FlushTick = () => void; - -/** - * Installs the requestAnimationFrame harness the loop tests drive by hand. - * - * Returns a `flushTick` bound to this suite's own queue, so each describe block - * gets an isolated set of callbacks rather than sharing module state. - * - * @returns A function that runs the most recently queued frame. - */ -export function installRafHarness(): FlushTick { - const rafCallbacks: FrameRequestCallback[] = []; - vi.stubGlobal( - "requestAnimationFrame", - vi.fn((cb: FrameRequestCallback) => { - rafCallbacks.push(cb); - return rafCallbacks.length; - }), - ); - vi.stubGlobal("cancelAnimationFrame", vi.fn()); - return () => { - const cb = rafCallbacks.pop(); - if (cb) act(() => { cb(0); }); - }; -} - -/** Lets pending promise callbacks settle inside act(). */ -export const flushPromises = async (): Promise => { - await act(async () => { await Promise.resolve(); }); -}; diff --git a/bucket_catch/packages/frontend/src/test/setup.ts b/bucket_catch/packages/frontend/src/test/setup.ts deleted file mode 100644 index 19e1784d..00000000 --- a/bucket_catch/packages/frontend/src/test/setup.ts +++ /dev/null @@ -1,15 +0,0 @@ -import "@testing-library/jest-dom"; -import { vi, beforeEach } from "vitest"; -import { mockCtx } from "./canvasMock"; - -beforeEach(() => { - vi.clearAllMocks(); -}); - -HTMLCanvasElement.prototype.getContext = vi.fn( - () => mockCtx, -) as unknown as typeof HTMLCanvasElement.prototype.getContext; - -HTMLCanvasElement.prototype.toDataURL = vi.fn( - () => "data:image/png;base64,mock", -); diff --git a/bucket_catch/packages/frontend/src/types.ts b/bucket_catch/packages/frontend/src/types.ts deleted file mode 100644 index 796ac8df..00000000 --- a/bucket_catch/packages/frontend/src/types.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Where the app is. The done phase carries its result, so "finished with no - * result" cannot be represented and the render never has to assert one away. - */ -export type GamePhase = - | { kind: "drop" } - | { kind: "mode" } - | { kind: "playing" } - | { kind: "done"; result: GameResult }; - -export type TransferMode = "download" | "upload" | "puzzle"; - -export interface FallingFileItem { - readonly kind: "file"; - readonly id: string; - readonly file: File; - x: number; - y: number; - readonly speed: number; - readonly startFrame: number; - status: "falling" | "caught" | "missed"; -} - -export interface PuzzlePiece { - readonly row: number; - readonly col: number; - readonly gridSize: number; - readonly imageUrl: string; - readonly pieceWidth: number; - readonly pieceHeight: number; -} - -export interface FallingPuzzleItem { - readonly kind: "puzzle"; - readonly id: string; - readonly piece: PuzzlePiece; - x: number; - y: number; - readonly speed: number; - readonly startFrame: number; - status: "falling" | "caught" | "missed"; -} - -export type FallingItem = FallingFileItem | FallingPuzzleItem; - -export interface BasketState { - x: number; - width: number; - height: number; -} - -export interface FileGameResult { - caught: File[]; - missed: File[]; -} - -export interface PuzzleGameResult { - caughtPieces: FallingPuzzleItem[]; - missedPieces: FallingPuzzleItem[]; - gridSize: number; -} - -/** The outcome of a finished game, tagged by which mode produced it. */ -export type GameResult = - | { kind: "file"; value: FileGameResult } - | { kind: "puzzle"; value: PuzzleGameResult }; diff --git a/bucket_catch/packages/frontend/tsconfig.json b/bucket_catch/packages/frontend/tsconfig.json deleted file mode 100644 index da1d9892..00000000 --- a/bucket_catch/packages/frontend/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "esnext", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "jsx": "react-jsx", - "types": ["vite/client", "vitest/globals"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src", "vite.config.ts"] -} diff --git a/bucket_catch/packages/frontend/vite.config.ts b/bucket_catch/packages/frontend/vite.config.ts deleted file mode 100644 index 2a3148bc..00000000 --- a/bucket_catch/packages/frontend/vite.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -// vitest 4 no longer contributes the `test` key to vite's own defineConfig, so -// the config is typed by importing defineConfig from vitest/config instead of -// the (now inert) triple-slash reference. -import { defineConfig } from "vitest/config"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], - server: { - port: 5173, - }, - test: { - environment: "jsdom", - globals: true, - setupFiles: ["./src/test/setup.ts"], - coverage: { - provider: "v8", - include: ["src/**/*.{ts,tsx}"], - exclude: ["src/main.tsx", "src/test/**"], - thresholds: { - statements: 100, - branches: 100, - functions: 100, - lines: 100, - }, - }, - }, -}); diff --git a/bucket_catch/pnpm-lock.yaml b/bucket_catch/pnpm-lock.yaml deleted file mode 100644 index bd90b2ee..00000000 --- a/bucket_catch/pnpm-lock.yaml +++ /dev/null @@ -1,7260 +0,0 @@ -lockfileVersion: "9.0" - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - .: {} - - packages/backend: - dependencies: - "@nestjs/common": - specifier: ^11.0.0 - version: 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2) - "@nestjs/core": - specifier: ^11.0.0 - version: 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) - "@nestjs/platform-express": - specifier: ^11.0.0 - version: 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27) - multer: - specifier: ^1.4.5-lts.1 - version: 1.4.5-lts.2 - reflect-metadata: - specifier: ^0.2.2 - version: 0.2.2 - rxjs: - specifier: ^7.8.1 - version: 7.8.2 - devDependencies: - "@eslint/js": - specifier: ^10.0.1 - version: 10.0.1(eslint@10.6.0) - "@types/express": - specifier: ^5.0.0 - version: 5.0.6 - "@types/multer": - specifier: ^1.4.12 - version: 1.4.13 - "@types/node": - specifier: ^22.0.0 - version: 22.20.0 - eslint: - specifier: ^10.6.0 - version: 10.6.0 - ts-node-dev: - specifier: ^2.0.0 - version: 2.0.0(@types/node@22.20.0)(typescript@5.9.3) - typescript: - specifier: ^5.7.0 - version: 5.9.3 - typescript-eslint: - specifier: ^8.62.0 - version: 8.62.0(eslint@10.6.0)(typescript@5.9.3) - - packages/frontend: - dependencies: - jszip: - specifier: ^3.10.1 - version: 3.10.1 - react: - specifier: ^19.1.0 - version: 19.2.7 - react-dom: - specifier: ^19.1.0 - version: 19.2.7(react@19.2.7) - devDependencies: - "@eslint/js": - specifier: ^10.0.1 - version: 10.0.1(eslint@10.6.0) - "@testing-library/jest-dom": - specifier: ^6.9.1 - version: 6.9.1 - "@testing-library/react": - specifier: ^16.3.2 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@testing-library/user-event": - specifier: ^14.6.1 - version: 14.6.1(@testing-library/dom@10.4.1) - "@types/react": - specifier: ^19.1.0 - version: 19.2.17 - "@types/react-dom": - specifier: ^19.1.0 - version: 19.2.3(@types/react@19.2.17) - "@vitejs/plugin-react": - specifier: ^4.5.2 - version: 4.7.0(vite@6.4.3(@types/node@22.20.0)(lightningcss@1.32.0)) - "@vitest/coverage-v8": - specifier: ^4.1.9 - version: 4.1.9(vitest@4.1.9) - eslint: - specifier: ^10.6.0 - version: 10.6.0 - eslint-plugin-react: - specifier: ^7.37.5 - version: 7.37.5(eslint@10.6.0) - eslint-plugin-react-hooks: - specifier: ^7.1.1 - version: 7.1.1(eslint@10.6.0) - jsdom: - specifier: ^29.1.1 - version: 29.1.1 - typescript: - specifier: ~5.8.3 - version: 5.8.3 - typescript-eslint: - specifier: ^8.62.0 - version: 8.62.0(eslint@10.6.0)(typescript@5.8.3) - vite: - specifier: ^6.3.5 - version: 6.4.3(@types/node@22.20.0)(lightningcss@1.32.0) - vitest: - specifier: ^4.1.9 - version: 4.1.9(@types/node@22.20.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@6.4.3(@types/node@22.20.0)(lightningcss@1.32.0)) - -packages: - "@adobe/css-tools@4.5.0": - resolution: - { - integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==, - } - - "@asamuzakjp/css-color@5.1.11": - resolution: - { - integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==, - } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } - - "@asamuzakjp/dom-selector@7.1.1": - resolution: - { - integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==, - } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } - - "@asamuzakjp/generational-cache@1.0.1": - resolution: - { - integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==, - } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } - - "@asamuzakjp/nwsapi@2.3.9": - resolution: - { - integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==, - } - - "@babel/code-frame@7.29.7": - resolution: - { - integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==, - } - engines: { node: ">=6.9.0" } - - "@babel/compat-data@7.29.7": - resolution: - { - integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==, - } - engines: { node: ">=6.9.0" } - - "@babel/core@7.29.7": - resolution: - { - integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==, - } - engines: { node: ">=6.9.0" } - - "@babel/generator@7.29.7": - resolution: - { - integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==, - } - engines: { node: ">=6.9.0" } - - "@babel/helper-compilation-targets@7.29.7": - resolution: - { - integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==, - } - engines: { node: ">=6.9.0" } - - "@babel/helper-globals@7.29.7": - resolution: - { - integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==, - } - engines: { node: ">=6.9.0" } - - "@babel/helper-module-imports@7.29.7": - resolution: - { - integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==, - } - engines: { node: ">=6.9.0" } - - "@babel/helper-module-transforms@7.29.7": - resolution: - { - integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==, - } - engines: { node: ">=6.9.0" } - peerDependencies: - "@babel/core": ^7.0.0 - - "@babel/helper-plugin-utils@7.29.7": - resolution: - { - integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==, - } - engines: { node: ">=6.9.0" } - - "@babel/helper-string-parser@7.29.7": - resolution: - { - integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==, - } - engines: { node: ">=6.9.0" } - - "@babel/helper-validator-identifier@7.29.7": - resolution: - { - integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==, - } - engines: { node: ">=6.9.0" } - - "@babel/helper-validator-option@7.29.7": - resolution: - { - integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==, - } - engines: { node: ">=6.9.0" } - - "@babel/helpers@7.29.7": - resolution: - { - integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==, - } - engines: { node: ">=6.9.0" } - - "@babel/parser@7.29.7": - resolution: - { - integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==, - } - engines: { node: ">=6.0.0" } - hasBin: true - - "@babel/plugin-transform-react-jsx-self@7.29.7": - resolution: - { - integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==, - } - engines: { node: ">=6.9.0" } - peerDependencies: - "@babel/core": ^7.0.0-0 - - "@babel/plugin-transform-react-jsx-source@7.29.7": - resolution: - { - integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==, - } - engines: { node: ">=6.9.0" } - peerDependencies: - "@babel/core": ^7.0.0-0 - - "@babel/runtime@7.29.7": - resolution: - { - integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==, - } - engines: { node: ">=6.9.0" } - - "@babel/template@7.29.7": - resolution: - { - integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==, - } - engines: { node: ">=6.9.0" } - - "@babel/traverse@7.29.7": - resolution: - { - integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==, - } - engines: { node: ">=6.9.0" } - - "@babel/types@7.29.7": - resolution: - { - integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==, - } - engines: { node: ">=6.9.0" } - - "@bcoe/v8-coverage@1.0.2": - resolution: - { - integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==, - } - engines: { node: ">=18" } - - "@borewit/text-codec@0.2.2": - resolution: - { - integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==, - } - - "@bramus/specificity@2.4.2": - resolution: - { - integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==, - } - hasBin: true - - "@cspotcode/source-map-support@0.8.1": - resolution: - { - integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==, - } - engines: { node: ">=12" } - - "@csstools/color-helpers@6.1.0": - resolution: - { - integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==, - } - engines: { node: ">=20.19.0" } - - "@csstools/css-calc@3.2.1": - resolution: - { - integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==, - } - engines: { node: ">=20.19.0" } - peerDependencies: - "@csstools/css-parser-algorithms": ^4.0.0 - "@csstools/css-tokenizer": ^4.0.0 - - "@csstools/css-color-parser@4.1.9": - resolution: - { - integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==, - } - engines: { node: ">=20.19.0" } - peerDependencies: - "@csstools/css-parser-algorithms": ^4.0.0 - "@csstools/css-tokenizer": ^4.0.0 - - "@csstools/css-parser-algorithms@4.0.0": - resolution: - { - integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==, - } - engines: { node: ">=20.19.0" } - peerDependencies: - "@csstools/css-tokenizer": ^4.0.0 - - "@csstools/css-syntax-patches-for-csstree@1.1.5": - resolution: - { - integrity: sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==, - } - peerDependencies: - css-tree: ^3.2.1 - peerDependenciesMeta: - css-tree: - optional: true - - "@csstools/css-tokenizer@4.0.0": - resolution: - { - integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==, - } - engines: { node: ">=20.19.0" } - - "@esbuild/aix-ppc64@0.25.12": - resolution: - { - integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [aix] - - "@esbuild/android-arm64@0.25.12": - resolution: - { - integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [android] - - "@esbuild/android-arm@0.25.12": - resolution: - { - integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [android] - - "@esbuild/android-x64@0.25.12": - resolution: - { - integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [android] - - "@esbuild/darwin-arm64@0.25.12": - resolution: - { - integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [darwin] - - "@esbuild/darwin-x64@0.25.12": - resolution: - { - integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [darwin] - - "@esbuild/freebsd-arm64@0.25.12": - resolution: - { - integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [freebsd] - - "@esbuild/freebsd-x64@0.25.12": - resolution: - { - integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [freebsd] - - "@esbuild/linux-arm64@0.25.12": - resolution: - { - integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [linux] - - "@esbuild/linux-arm@0.25.12": - resolution: - { - integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [linux] - - "@esbuild/linux-ia32@0.25.12": - resolution: - { - integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [linux] - - "@esbuild/linux-loong64@0.25.12": - resolution: - { - integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==, - } - engines: { node: ">=18" } - cpu: [loong64] - os: [linux] - - "@esbuild/linux-mips64el@0.25.12": - resolution: - { - integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==, - } - engines: { node: ">=18" } - cpu: [mips64el] - os: [linux] - - "@esbuild/linux-ppc64@0.25.12": - resolution: - { - integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [linux] - - "@esbuild/linux-riscv64@0.25.12": - resolution: - { - integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==, - } - engines: { node: ">=18" } - cpu: [riscv64] - os: [linux] - - "@esbuild/linux-s390x@0.25.12": - resolution: - { - integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==, - } - engines: { node: ">=18" } - cpu: [s390x] - os: [linux] - - "@esbuild/linux-x64@0.25.12": - resolution: - { - integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [linux] - - "@esbuild/netbsd-arm64@0.25.12": - resolution: - { - integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [netbsd] - - "@esbuild/netbsd-x64@0.25.12": - resolution: - { - integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [netbsd] - - "@esbuild/openbsd-arm64@0.25.12": - resolution: - { - integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openbsd] - - "@esbuild/openbsd-x64@0.25.12": - resolution: - { - integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [openbsd] - - "@esbuild/openharmony-arm64@0.25.12": - resolution: - { - integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openharmony] - - "@esbuild/sunos-x64@0.25.12": - resolution: - { - integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [sunos] - - "@esbuild/win32-arm64@0.25.12": - resolution: - { - integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [win32] - - "@esbuild/win32-ia32@0.25.12": - resolution: - { - integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [win32] - - "@esbuild/win32-x64@0.25.12": - resolution: - { - integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [win32] - - "@eslint-community/eslint-utils@4.9.1": - resolution: - { - integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - "@eslint-community/regexpp@4.12.2": - resolution: - { - integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==, - } - engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } - - "@eslint/config-array@0.23.5": - resolution: - { - integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - - "@eslint/config-helpers@0.6.0": - resolution: - { - integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - - "@eslint/core@1.2.1": - resolution: - { - integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - - "@eslint/js@10.0.1": - resolution: - { - integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - peerDependencies: - eslint: ^10.0.0 - peerDependenciesMeta: - eslint: - optional: true - - "@eslint/object-schema@3.0.5": - resolution: - { - integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - - "@eslint/plugin-kit@0.7.2": - resolution: - { - integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - - "@exodus/bytes@1.15.1": - resolution: - { - integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==, - } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } - peerDependencies: - "@noble/hashes": ^1.8.0 || ^2.0.0 - peerDependenciesMeta: - "@noble/hashes": - optional: true - - "@humanfs/core@0.19.2": - resolution: - { - integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==, - } - engines: { node: ">=18.18.0" } - - "@humanfs/node@0.16.8": - resolution: - { - integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==, - } - engines: { node: ">=18.18.0" } - - "@humanfs/types@0.15.0": - resolution: - { - integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==, - } - engines: { node: ">=18.18.0" } - - "@humanwhocodes/module-importer@1.0.1": - resolution: - { - integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, - } - engines: { node: ">=12.22" } - - "@humanwhocodes/retry@0.4.3": - resolution: - { - integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==, - } - engines: { node: ">=18.18" } - - "@jridgewell/gen-mapping@0.3.13": - resolution: - { - integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, - } - - "@jridgewell/remapping@2.3.5": - resolution: - { - integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, - } - - "@jridgewell/resolve-uri@3.1.2": - resolution: - { - integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, - } - engines: { node: ">=6.0.0" } - - "@jridgewell/sourcemap-codec@1.5.5": - resolution: - { - integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, - } - - "@jridgewell/trace-mapping@0.3.31": - resolution: - { - integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, - } - - "@jridgewell/trace-mapping@0.3.9": - resolution: - { - integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==, - } - - "@lukeed/csprng@1.1.0": - resolution: - { - integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==, - } - engines: { node: ">=8" } - - "@nestjs/common@11.1.27": - resolution: - { - integrity: sha512-kEGSzqM2lWr4whh4Ubflw+oPZSEzxvRMu9WL+LveZploJWTjec5bBlCiRVlVzTPg2kIwBiLwWSvCCW7Wnin1gg==, - } - peerDependencies: - class-transformer: ">=0.4.1" - class-validator: ">=0.13.2" - reflect-metadata: ^0.1.12 || ^0.2.0 - rxjs: ^7.1.0 - peerDependenciesMeta: - class-transformer: - optional: true - class-validator: - optional: true - - "@nestjs/core@11.1.27": - resolution: - { - integrity: sha512-K6DX7hcqmZdeXkv7tsPakKBRCgqL19a4mtbX4FluY0hWtFdtPKp6lbe+lb8gWPfvLdbOWr/CPScn7BSjBX+Ecg==, - } - engines: { node: ">= 20" } - peerDependencies: - "@nestjs/common": ^11.0.0 - "@nestjs/microservices": ^11.0.0 - "@nestjs/platform-express": ^11.0.0 - "@nestjs/websockets": ^11.0.0 - reflect-metadata: ^0.1.12 || ^0.2.0 - rxjs: ^7.1.0 - peerDependenciesMeta: - "@nestjs/microservices": - optional: true - "@nestjs/platform-express": - optional: true - "@nestjs/websockets": - optional: true - - "@nestjs/platform-express@11.1.27": - resolution: - { - integrity: sha512-0ZFhz6H6EdGh4xQVbUNwjoAwBuz73P7FvUAl67h9CTdMqQlJDaQYJApBv8pKfVZ1fGjMCbl0m9DcC6pXaZPWSQ==, - } - peerDependencies: - "@nestjs/common": ^11.0.0 - "@nestjs/core": ^11.0.0 - - "@rolldown/pluginutils@1.0.0-beta.27": - resolution: - { - integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==, - } - - "@rollup/rollup-android-arm-eabi@4.62.2": - resolution: - { - integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==, - } - cpu: [arm] - os: [android] - - "@rollup/rollup-android-arm64@4.62.2": - resolution: - { - integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==, - } - cpu: [arm64] - os: [android] - - "@rollup/rollup-darwin-arm64@4.62.2": - resolution: - { - integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==, - } - cpu: [arm64] - os: [darwin] - - "@rollup/rollup-darwin-x64@4.62.2": - resolution: - { - integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==, - } - cpu: [x64] - os: [darwin] - - "@rollup/rollup-freebsd-arm64@4.62.2": - resolution: - { - integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==, - } - cpu: [arm64] - os: [freebsd] - - "@rollup/rollup-freebsd-x64@4.62.2": - resolution: - { - integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==, - } - cpu: [x64] - os: [freebsd] - - "@rollup/rollup-linux-arm-gnueabihf@4.62.2": - resolution: - { - integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==, - } - cpu: [arm] - os: [linux] - - "@rollup/rollup-linux-arm-musleabihf@4.62.2": - resolution: - { - integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==, - } - cpu: [arm] - os: [linux] - - "@rollup/rollup-linux-arm64-gnu@4.62.2": - resolution: - { - integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==, - } - cpu: [arm64] - os: [linux] - - "@rollup/rollup-linux-arm64-musl@4.62.2": - resolution: - { - integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==, - } - cpu: [arm64] - os: [linux] - - "@rollup/rollup-linux-loong64-gnu@4.62.2": - resolution: - { - integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==, - } - cpu: [loong64] - os: [linux] - - "@rollup/rollup-linux-loong64-musl@4.62.2": - resolution: - { - integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==, - } - cpu: [loong64] - os: [linux] - - "@rollup/rollup-linux-ppc64-gnu@4.62.2": - resolution: - { - integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==, - } - cpu: [ppc64] - os: [linux] - - "@rollup/rollup-linux-ppc64-musl@4.62.2": - resolution: - { - integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==, - } - cpu: [ppc64] - os: [linux] - - "@rollup/rollup-linux-riscv64-gnu@4.62.2": - resolution: - { - integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==, - } - cpu: [riscv64] - os: [linux] - - "@rollup/rollup-linux-riscv64-musl@4.62.2": - resolution: - { - integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==, - } - cpu: [riscv64] - os: [linux] - - "@rollup/rollup-linux-s390x-gnu@4.62.2": - resolution: - { - integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==, - } - cpu: [s390x] - os: [linux] - - "@rollup/rollup-linux-x64-gnu@4.62.2": - resolution: - { - integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==, - } - cpu: [x64] - os: [linux] - - "@rollup/rollup-linux-x64-musl@4.62.2": - resolution: - { - integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==, - } - cpu: [x64] - os: [linux] - - "@rollup/rollup-openbsd-x64@4.62.2": - resolution: - { - integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==, - } - cpu: [x64] - os: [openbsd] - - "@rollup/rollup-openharmony-arm64@4.62.2": - resolution: - { - integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==, - } - cpu: [arm64] - os: [openharmony] - - "@rollup/rollup-win32-arm64-msvc@4.62.2": - resolution: - { - integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==, - } - cpu: [arm64] - os: [win32] - - "@rollup/rollup-win32-ia32-msvc@4.62.2": - resolution: - { - integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==, - } - cpu: [ia32] - os: [win32] - - "@rollup/rollup-win32-x64-gnu@4.62.2": - resolution: - { - integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==, - } - cpu: [x64] - os: [win32] - - "@rollup/rollup-win32-x64-msvc@4.62.2": - resolution: - { - integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==, - } - cpu: [x64] - os: [win32] - - "@standard-schema/spec@1.1.0": - resolution: - { - integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, - } - - "@testing-library/dom@10.4.1": - resolution: - { - integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==, - } - engines: { node: ">=18" } - - "@testing-library/jest-dom@6.9.1": - resolution: - { - integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==, - } - engines: { node: ">=14", npm: ">=6", yarn: ">=1" } - - "@testing-library/react@16.3.2": - resolution: - { - integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==, - } - engines: { node: ">=18" } - peerDependencies: - "@testing-library/dom": ^10.0.0 - "@types/react": ^18.0.0 || ^19.0.0 - "@types/react-dom": ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - "@types/react-dom": - optional: true - - "@testing-library/user-event@14.6.1": - resolution: - { - integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==, - } - engines: { node: ">=12", npm: ">=6" } - peerDependencies: - "@testing-library/dom": ">=7.21.4" - - "@tokenizer/inflate@0.4.1": - resolution: - { - integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==, - } - engines: { node: ">=18" } - - "@tokenizer/token@0.3.0": - resolution: - { - integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==, - } - - "@tsconfig/node10@1.0.12": - resolution: - { - integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==, - } - - "@tsconfig/node12@1.0.11": - resolution: - { - integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==, - } - - "@tsconfig/node14@1.0.3": - resolution: - { - integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==, - } - - "@tsconfig/node16@1.0.4": - resolution: - { - integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==, - } - - "@types/aria-query@5.0.4": - resolution: - { - integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==, - } - - "@types/babel__core@7.20.5": - resolution: - { - integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, - } - - "@types/babel__generator@7.27.0": - resolution: - { - integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==, - } - - "@types/babel__template@7.4.4": - resolution: - { - integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==, - } - - "@types/babel__traverse@7.28.0": - resolution: - { - integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==, - } - - "@types/body-parser@1.19.6": - resolution: - { - integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==, - } - - "@types/chai@5.2.3": - resolution: - { - integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==, - } - - "@types/connect@3.4.38": - resolution: - { - integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==, - } - - "@types/deep-eql@4.0.2": - resolution: - { - integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==, - } - - "@types/esrecurse@4.3.1": - resolution: - { - integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==, - } - - "@types/estree@1.0.9": - resolution: - { - integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==, - } - - "@types/express-serve-static-core@5.1.1": - resolution: - { - integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==, - } - - "@types/express@5.0.6": - resolution: - { - integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==, - } - - "@types/http-errors@2.0.5": - resolution: - { - integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==, - } - - "@types/json-schema@7.0.15": - resolution: - { - integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, - } - - "@types/multer@1.4.13": - resolution: - { - integrity: sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==, - } - - "@types/node@22.20.0": - resolution: - { - integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==, - } - - "@types/qs@6.15.1": - resolution: - { - integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==, - } - - "@types/range-parser@1.2.7": - resolution: - { - integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==, - } - - "@types/react-dom@19.2.3": - resolution: - { - integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==, - } - peerDependencies: - "@types/react": ^19.2.0 - - "@types/react@19.2.17": - resolution: - { - integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==, - } - - "@types/send@1.2.1": - resolution: - { - integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==, - } - - "@types/serve-static@2.2.0": - resolution: - { - integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==, - } - - "@types/strip-bom@3.0.0": - resolution: - { - integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==, - } - - "@types/strip-json-comments@0.0.30": - resolution: - { - integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==, - } - - "@typescript-eslint/eslint-plugin@8.62.0": - resolution: - { - integrity: sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - "@typescript-eslint/parser": ^8.62.0 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.1.0" - - "@typescript-eslint/parser@8.62.0": - resolution: - { - integrity: sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.1.0" - - "@typescript-eslint/project-service@8.62.0": - resolution: - { - integrity: sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - typescript: ">=4.8.4 <6.1.0" - - "@typescript-eslint/scope-manager@8.62.0": - resolution: - { - integrity: sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - "@typescript-eslint/tsconfig-utils@8.62.0": - resolution: - { - integrity: sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - typescript: ">=4.8.4 <6.1.0" - - "@typescript-eslint/type-utils@8.62.0": - resolution: - { - integrity: sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.1.0" - - "@typescript-eslint/types@8.62.0": - resolution: - { - integrity: sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - "@typescript-eslint/typescript-estree@8.62.0": - resolution: - { - integrity: sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - typescript: ">=4.8.4 <6.1.0" - - "@typescript-eslint/utils@8.62.0": - resolution: - { - integrity: sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.1.0" - - "@typescript-eslint/visitor-keys@8.62.0": - resolution: - { - integrity: sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - - "@vitejs/plugin-react@4.7.0": - resolution: - { - integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==, - } - engines: { node: ^14.18.0 || >=16.0.0 } - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - - "@vitest/coverage-v8@4.1.9": - resolution: - { - integrity: sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==, - } - peerDependencies: - "@vitest/browser": 4.1.9 - vitest: 4.1.9 - peerDependenciesMeta: - "@vitest/browser": - optional: true - - "@vitest/expect@4.1.9": - resolution: - { - integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==, - } - - "@vitest/mocker@4.1.9": - resolution: - { - integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==, - } - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - "@vitest/pretty-format@4.1.9": - resolution: - { - integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==, - } - - "@vitest/runner@4.1.9": - resolution: - { - integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==, - } - - "@vitest/snapshot@4.1.9": - resolution: - { - integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==, - } - - "@vitest/spy@4.1.9": - resolution: - { - integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==, - } - - "@vitest/utils@4.1.9": - resolution: - { - integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==, - } - - accepts@2.0.0: - resolution: - { - integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==, - } - engines: { node: ">= 0.6" } - - acorn-jsx@5.3.2: - resolution: - { - integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, - } - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn-walk@8.3.5: - resolution: - { - integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==, - } - engines: { node: ">=0.4.0" } - - acorn@8.17.0: - resolution: - { - integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==, - } - engines: { node: ">=0.4.0" } - hasBin: true - - ajv@6.15.0: - resolution: - { - integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==, - } - - ansi-regex@5.0.1: - resolution: - { - integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, - } - engines: { node: ">=8" } - - ansi-styles@5.2.0: - resolution: - { - integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==, - } - engines: { node: ">=10" } - - anymatch@3.1.3: - resolution: - { - integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==, - } - engines: { node: ">= 8" } - - append-field@1.0.0: - resolution: - { - integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==, - } - - arg@4.1.3: - resolution: - { - integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==, - } - - aria-query@5.3.0: - resolution: - { - integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==, - } - - array-buffer-byte-length@1.0.2: - resolution: - { - integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==, - } - engines: { node: ">= 0.4" } - - array-includes@3.1.9: - resolution: - { - integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==, - } - engines: { node: ">= 0.4" } - - array.prototype.findlast@1.2.5: - resolution: - { - integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==, - } - engines: { node: ">= 0.4" } - - array.prototype.flat@1.3.3: - resolution: - { - integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==, - } - engines: { node: ">= 0.4" } - - array.prototype.flatmap@1.3.3: - resolution: - { - integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==, - } - engines: { node: ">= 0.4" } - - array.prototype.tosorted@1.1.4: - resolution: - { - integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==, - } - engines: { node: ">= 0.4" } - - arraybuffer.prototype.slice@1.0.4: - resolution: - { - integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==, - } - engines: { node: ">= 0.4" } - - assertion-error@2.0.1: - resolution: - { - integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==, - } - engines: { node: ">=12" } - - ast-v8-to-istanbul@1.0.4: - resolution: - { - integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==, - } - - async-function@1.0.0: - resolution: - { - integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==, - } - engines: { node: ">= 0.4" } - - available-typed-arrays@1.0.7: - resolution: - { - integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==, - } - engines: { node: ">= 0.4" } - - balanced-match@1.0.2: - resolution: - { - integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, - } - - balanced-match@4.0.4: - resolution: - { - integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, - } - engines: { node: 18 || 20 || >=22 } - - baseline-browser-mapping@2.10.40: - resolution: - { - integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==, - } - engines: { node: ">=6.0.0" } - hasBin: true - - bidi-js@1.0.3: - resolution: - { - integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==, - } - - binary-extensions@2.3.0: - resolution: - { - integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==, - } - engines: { node: ">=8" } - - body-parser@2.3.0: - resolution: - { - integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==, - } - engines: { node: ">=18" } - - brace-expansion@1.1.15: - resolution: - { - integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==, - } - - brace-expansion@5.0.6: - resolution: - { - integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==, - } - engines: { node: 18 || 20 || >=22 } - - braces@3.0.3: - resolution: - { - integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, - } - engines: { node: ">=8" } - - browserslist@4.28.4: - resolution: - { - integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==, - } - engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } - hasBin: true - - buffer-from@1.1.2: - resolution: - { - integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==, - } - - busboy@1.6.0: - resolution: - { - integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==, - } - engines: { node: ">=10.16.0" } - - bytes@3.1.2: - resolution: - { - integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==, - } - engines: { node: ">= 0.8" } - - call-bind-apply-helpers@1.0.2: - resolution: - { - integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, - } - engines: { node: ">= 0.4" } - - call-bind@1.0.9: - resolution: - { - integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==, - } - engines: { node: ">= 0.4" } - - call-bound@1.0.4: - resolution: - { - integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, - } - engines: { node: ">= 0.4" } - - caniuse-lite@1.0.30001799: - resolution: - { - integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==, - } - - chai@6.2.2: - resolution: - { - integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==, - } - engines: { node: ">=18" } - - chokidar@3.6.0: - resolution: - { - integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==, - } - engines: { node: ">= 8.10.0" } - - concat-map@0.0.1: - resolution: - { - integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, - } - - concat-stream@1.6.2: - resolution: - { - integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==, - } - engines: { "0": node >= 0.8 } - - concat-stream@2.0.0: - resolution: - { - integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==, - } - engines: { "0": node >= 6.0 } - - content-disposition@1.1.0: - resolution: - { - integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==, - } - engines: { node: ">=18" } - - content-type@1.0.5: - resolution: - { - integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==, - } - engines: { node: ">= 0.6" } - - content-type@2.0.0: - resolution: - { - integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==, - } - engines: { node: ">=18" } - - convert-source-map@2.0.0: - resolution: - { - integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, - } - - cookie-signature@1.2.2: - resolution: - { - integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==, - } - engines: { node: ">=6.6.0" } - - cookie@0.7.2: - resolution: - { - integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==, - } - engines: { node: ">= 0.6" } - - core-util-is@1.0.3: - resolution: - { - integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==, - } - - cors@2.8.6: - resolution: - { - integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==, - } - engines: { node: ">= 0.10" } - - create-require@1.1.1: - resolution: - { - integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==, - } - - cross-spawn@7.0.6: - resolution: - { - integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, - } - engines: { node: ">= 8" } - - css-tree@3.2.1: - resolution: - { - integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==, - } - engines: { node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0 } - - css.escape@1.5.1: - resolution: - { - integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==, - } - - csstype@3.2.3: - resolution: - { - integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, - } - - data-urls@7.0.0: - resolution: - { - integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==, - } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } - - data-view-buffer@1.0.2: - resolution: - { - integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==, - } - engines: { node: ">= 0.4" } - - data-view-byte-length@1.0.2: - resolution: - { - integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==, - } - engines: { node: ">= 0.4" } - - data-view-byte-offset@1.0.1: - resolution: - { - integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==, - } - engines: { node: ">= 0.4" } - - debug@4.4.3: - resolution: - { - integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, - } - engines: { node: ">=6.0" } - peerDependencies: - supports-color: "*" - peerDependenciesMeta: - supports-color: - optional: true - - decimal.js@10.6.0: - resolution: - { - integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==, - } - - deep-is@0.1.4: - resolution: - { - integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, - } - - define-data-property@1.1.4: - resolution: - { - integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==, - } - engines: { node: ">= 0.4" } - - define-properties@1.2.1: - resolution: - { - integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==, - } - engines: { node: ">= 0.4" } - - depd@2.0.0: - resolution: - { - integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, - } - engines: { node: ">= 0.8" } - - dequal@2.0.3: - resolution: - { - integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==, - } - engines: { node: ">=6" } - - detect-libc@2.1.2: - resolution: - { - integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, - } - engines: { node: ">=8" } - - diff@4.0.4: - resolution: - { - integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==, - } - engines: { node: ">=0.3.1" } - - doctrine@2.1.0: - resolution: - { - integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==, - } - engines: { node: ">=0.10.0" } - - dom-accessibility-api@0.5.16: - resolution: - { - integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==, - } - - dom-accessibility-api@0.6.3: - resolution: - { - integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==, - } - - dunder-proto@1.0.1: - resolution: - { - integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, - } - engines: { node: ">= 0.4" } - - dynamic-dedupe@0.3.0: - resolution: - { - integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==, - } - - ee-first@1.1.1: - resolution: - { - integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, - } - - electron-to-chromium@1.5.379: - resolution: - { - integrity: sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==, - } - - encodeurl@2.0.0: - resolution: - { - integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, - } - engines: { node: ">= 0.8" } - - entities@8.0.0: - resolution: - { - integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==, - } - engines: { node: ">=20.19.0" } - - es-abstract-get@1.0.0: - resolution: - { - integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==, - } - engines: { node: ">= 0.4" } - - es-abstract@1.24.2: - resolution: - { - integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==, - } - engines: { node: ">= 0.4" } - - es-define-property@1.0.1: - resolution: - { - integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, - } - engines: { node: ">= 0.4" } - - es-errors@1.3.0: - resolution: - { - integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, - } - engines: { node: ">= 0.4" } - - es-iterator-helpers@1.3.3: - resolution: - { - integrity: sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==, - } - engines: { node: ">= 0.4" } - - es-module-lexer@2.1.0: - resolution: - { - integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==, - } - - es-object-atoms@1.1.2: - resolution: - { - integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==, - } - engines: { node: ">= 0.4" } - - es-set-tostringtag@2.1.0: - resolution: - { - integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==, - } - engines: { node: ">= 0.4" } - - es-shim-unscopables@1.1.0: - resolution: - { - integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==, - } - engines: { node: ">= 0.4" } - - es-to-primitive@1.3.4: - resolution: - { - integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==, - } - engines: { node: ">= 0.4" } - - esbuild@0.25.12: - resolution: - { - integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==, - } - engines: { node: ">=18" } - hasBin: true - - escalade@3.2.0: - resolution: - { - integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, - } - engines: { node: ">=6" } - - escape-html@1.0.3: - resolution: - { - integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, - } - - escape-string-regexp@4.0.0: - resolution: - { - integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, - } - engines: { node: ">=10" } - - eslint-plugin-react-hooks@7.1.1: - resolution: - { - integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==, - } - engines: { node: ">=18" } - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 - - eslint-plugin-react@7.37.5: - resolution: - { - integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==, - } - engines: { node: ">=4" } - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - - eslint-scope@9.1.2: - resolution: - { - integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - - eslint-visitor-keys@3.4.3: - resolution: - { - integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } - - eslint-visitor-keys@5.0.1: - resolution: - { - integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - - eslint@10.6.0: - resolution: - { - integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - hasBin: true - peerDependencies: - jiti: "*" - peerDependenciesMeta: - jiti: - optional: true - - espree@11.2.0: - resolution: - { - integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - - esquery@1.7.0: - resolution: - { - integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==, - } - engines: { node: ">=0.10" } - - esrecurse@4.3.0: - resolution: - { - integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, - } - engines: { node: ">=4.0" } - - estraverse@5.3.0: - resolution: - { - integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, - } - engines: { node: ">=4.0" } - - estree-walker@3.0.3: - resolution: - { - integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, - } - - esutils@2.0.3: - resolution: - { - integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, - } - engines: { node: ">=0.10.0" } - - etag@1.8.1: - resolution: - { - integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==, - } - engines: { node: ">= 0.6" } - - expect-type@1.4.0: - resolution: - { - integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==, - } - engines: { node: ">=12.0.0" } - - express@5.2.1: - resolution: - { - integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==, - } - engines: { node: ">= 18" } - - fast-deep-equal@3.1.3: - resolution: - { - integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, - } - - fast-json-stable-stringify@2.1.0: - resolution: - { - integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, - } - - fast-levenshtein@2.0.6: - resolution: - { - integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, - } - - fast-safe-stringify@2.1.1: - resolution: - { - integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==, - } - - fdir@6.5.0: - resolution: - { - integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, - } - engines: { node: ">=12.0.0" } - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: - { - integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, - } - engines: { node: ">=16.0.0" } - - file-type@21.3.4: - resolution: - { - integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==, - } - engines: { node: ">=20" } - - fill-range@7.1.1: - resolution: - { - integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, - } - engines: { node: ">=8" } - - finalhandler@2.1.1: - resolution: - { - integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==, - } - engines: { node: ">= 18.0.0" } - - find-up@5.0.0: - resolution: - { - integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, - } - engines: { node: ">=10" } - - flat-cache@4.0.1: - resolution: - { - integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, - } - engines: { node: ">=16" } - - flatted@3.4.2: - resolution: - { - integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==, - } - - for-each@0.3.5: - resolution: - { - integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==, - } - engines: { node: ">= 0.4" } - - forwarded@0.2.0: - resolution: - { - integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==, - } - engines: { node: ">= 0.6" } - - fresh@2.0.0: - resolution: - { - integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==, - } - engines: { node: ">= 0.8" } - - fs.realpath@1.0.0: - resolution: - { - integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, - } - - fsevents@2.3.3: - resolution: - { - integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, - } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } - os: [darwin] - - function-bind@1.1.2: - resolution: - { - integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, - } - - function.prototype.name@1.2.0: - resolution: - { - integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==, - } - engines: { node: ">= 0.4" } - - functions-have-names@1.2.3: - resolution: - { - integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==, - } - - generator-function@2.0.1: - resolution: - { - integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==, - } - engines: { node: ">= 0.4" } - - gensync@1.0.0-beta.2: - resolution: - { - integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, - } - engines: { node: ">=6.9.0" } - - get-intrinsic@1.3.0: - resolution: - { - integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, - } - engines: { node: ">= 0.4" } - - get-proto@1.0.1: - resolution: - { - integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, - } - engines: { node: ">= 0.4" } - - get-symbol-description@1.1.0: - resolution: - { - integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==, - } - engines: { node: ">= 0.4" } - - glob-parent@5.1.2: - resolution: - { - integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, - } - engines: { node: ">= 6" } - - glob-parent@6.0.2: - resolution: - { - integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, - } - engines: { node: ">=10.13.0" } - - glob@7.2.3: - resolution: - { - integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, - } - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - - globalthis@1.0.4: - resolution: - { - integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==, - } - engines: { node: ">= 0.4" } - - gopd@1.2.0: - resolution: - { - integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, - } - engines: { node: ">= 0.4" } - - has-bigints@1.1.0: - resolution: - { - integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==, - } - engines: { node: ">= 0.4" } - - has-flag@4.0.0: - resolution: - { - integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, - } - engines: { node: ">=8" } - - has-property-descriptors@1.0.2: - resolution: - { - integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==, - } - - has-proto@1.2.0: - resolution: - { - integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==, - } - engines: { node: ">= 0.4" } - - has-symbols@1.1.0: - resolution: - { - integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, - } - engines: { node: ">= 0.4" } - - has-tostringtag@1.0.2: - resolution: - { - integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==, - } - engines: { node: ">= 0.4" } - - hasown@2.0.4: - resolution: - { - integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==, - } - engines: { node: ">= 0.4" } - - hermes-estree@0.25.1: - resolution: - { - integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==, - } - - hermes-parser@0.25.1: - resolution: - { - integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==, - } - - html-encoding-sniffer@6.0.0: - resolution: - { - integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==, - } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } - - html-escaper@2.0.2: - resolution: - { - integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==, - } - - http-errors@2.0.1: - resolution: - { - integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==, - } - engines: { node: ">= 0.8" } - - iconv-lite@0.7.2: - resolution: - { - integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==, - } - engines: { node: ">=0.10.0" } - - ieee754@1.2.1: - resolution: - { - integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, - } - - ignore@5.3.2: - resolution: - { - integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, - } - engines: { node: ">= 4" } - - ignore@7.0.5: - resolution: - { - integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==, - } - engines: { node: ">= 4" } - - immediate@3.0.6: - resolution: - { - integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==, - } - - imurmurhash@0.1.4: - resolution: - { - integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, - } - engines: { node: ">=0.8.19" } - - indent-string@4.0.0: - resolution: - { - integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==, - } - engines: { node: ">=8" } - - inflight@1.0.6: - resolution: - { - integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==, - } - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: - { - integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, - } - - internal-slot@1.1.0: - resolution: - { - integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==, - } - engines: { node: ">= 0.4" } - - ipaddr.js@1.9.1: - resolution: - { - integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==, - } - engines: { node: ">= 0.10" } - - is-array-buffer@3.0.5: - resolution: - { - integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==, - } - engines: { node: ">= 0.4" } - - is-async-function@2.1.1: - resolution: - { - integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==, - } - engines: { node: ">= 0.4" } - - is-bigint@1.1.0: - resolution: - { - integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==, - } - engines: { node: ">= 0.4" } - - is-binary-path@2.1.0: - resolution: - { - integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==, - } - engines: { node: ">=8" } - - is-boolean-object@1.2.2: - resolution: - { - integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==, - } - engines: { node: ">= 0.4" } - - is-callable@1.2.7: - resolution: - { - integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==, - } - engines: { node: ">= 0.4" } - - is-core-module@2.16.2: - resolution: - { - integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==, - } - engines: { node: ">= 0.4" } - - is-data-view@1.0.2: - resolution: - { - integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==, - } - engines: { node: ">= 0.4" } - - is-date-object@1.1.0: - resolution: - { - integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==, - } - engines: { node: ">= 0.4" } - - is-document.all@1.0.0: - resolution: - { - integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==, - } - engines: { node: ">= 0.4" } - - is-extglob@2.1.1: - resolution: - { - integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, - } - engines: { node: ">=0.10.0" } - - is-finalizationregistry@1.1.1: - resolution: - { - integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==, - } - engines: { node: ">= 0.4" } - - is-generator-function@1.1.2: - resolution: - { - integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==, - } - engines: { node: ">= 0.4" } - - is-glob@4.0.3: - resolution: - { - integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, - } - engines: { node: ">=0.10.0" } - - is-map@2.0.3: - resolution: - { - integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==, - } - engines: { node: ">= 0.4" } - - is-negative-zero@2.0.3: - resolution: - { - integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==, - } - engines: { node: ">= 0.4" } - - is-number-object@1.1.1: - resolution: - { - integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==, - } - engines: { node: ">= 0.4" } - - is-number@7.0.0: - resolution: - { - integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, - } - engines: { node: ">=0.12.0" } - - is-potential-custom-element-name@1.0.1: - resolution: - { - integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==, - } - - is-promise@4.0.0: - resolution: - { - integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==, - } - - is-regex@1.2.1: - resolution: - { - integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==, - } - engines: { node: ">= 0.4" } - - is-set@2.0.3: - resolution: - { - integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==, - } - engines: { node: ">= 0.4" } - - is-shared-array-buffer@1.0.4: - resolution: - { - integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==, - } - engines: { node: ">= 0.4" } - - is-string@1.1.1: - resolution: - { - integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==, - } - engines: { node: ">= 0.4" } - - is-symbol@1.1.1: - resolution: - { - integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==, - } - engines: { node: ">= 0.4" } - - is-typed-array@1.1.15: - resolution: - { - integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==, - } - engines: { node: ">= 0.4" } - - is-weakmap@2.0.2: - resolution: - { - integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==, - } - engines: { node: ">= 0.4" } - - is-weakref@1.1.1: - resolution: - { - integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==, - } - engines: { node: ">= 0.4" } - - is-weakset@2.0.4: - resolution: - { - integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==, - } - engines: { node: ">= 0.4" } - - isarray@1.0.0: - resolution: - { - integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==, - } - - isarray@2.0.5: - resolution: - { - integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==, - } - - isexe@2.0.0: - resolution: - { - integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, - } - - istanbul-lib-coverage@3.2.2: - resolution: - { - integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==, - } - engines: { node: ">=8" } - - istanbul-lib-report@3.0.1: - resolution: - { - integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==, - } - engines: { node: ">=10" } - - istanbul-reports@3.2.0: - resolution: - { - integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==, - } - engines: { node: ">=8" } - - iterare@1.2.1: - resolution: - { - integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==, - } - engines: { node: ">=6" } - - iterator.prototype@1.1.5: - resolution: - { - integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==, - } - engines: { node: ">= 0.4" } - - js-tokens@10.0.0: - resolution: - { - integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==, - } - - js-tokens@4.0.0: - resolution: - { - integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, - } - - jsdom@29.1.1: - resolution: - { - integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24.0.0 } - peerDependencies: - canvas: ^3.0.0 - peerDependenciesMeta: - canvas: - optional: true - - jsesc@3.1.0: - resolution: - { - integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, - } - engines: { node: ">=6" } - hasBin: true - - json-buffer@3.0.1: - resolution: - { - integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, - } - - json-schema-traverse@0.4.1: - resolution: - { - integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, - } - - json-stable-stringify-without-jsonify@1.0.1: - resolution: - { - integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, - } - - json5@2.2.3: - resolution: - { - integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, - } - engines: { node: ">=6" } - hasBin: true - - jsx-ast-utils@3.3.5: - resolution: - { - integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==, - } - engines: { node: ">=4.0" } - - jszip@3.10.1: - resolution: - { - integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==, - } - - keyv@4.5.4: - resolution: - { - integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, - } - - levn@0.4.1: - resolution: - { - integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, - } - engines: { node: ">= 0.8.0" } - - lie@3.3.0: - resolution: - { - integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==, - } - - lightningcss-android-arm64@1.32.0: - resolution: - { - integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==, - } - engines: { node: ">= 12.0.0" } - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: - { - integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==, - } - engines: { node: ">= 12.0.0" } - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: - { - integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==, - } - engines: { node: ">= 12.0.0" } - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: - { - integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==, - } - engines: { node: ">= 12.0.0" } - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: - { - integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==, - } - engines: { node: ">= 12.0.0" } - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: - { - integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==, - } - engines: { node: ">= 12.0.0" } - cpu: [arm64] - os: [linux] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: - { - integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==, - } - engines: { node: ">= 12.0.0" } - cpu: [arm64] - os: [linux] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: - { - integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==, - } - engines: { node: ">= 12.0.0" } - cpu: [x64] - os: [linux] - - lightningcss-linux-x64-musl@1.32.0: - resolution: - { - integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==, - } - engines: { node: ">= 12.0.0" } - cpu: [x64] - os: [linux] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: - { - integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==, - } - engines: { node: ">= 12.0.0" } - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: - { - integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==, - } - engines: { node: ">= 12.0.0" } - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: - { - integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==, - } - engines: { node: ">= 12.0.0" } - - load-esm@1.0.3: - resolution: - { - integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==, - } - engines: { node: ">=13.2.0" } - - locate-path@6.0.0: - resolution: - { - integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, - } - engines: { node: ">=10" } - - loose-envify@1.4.0: - resolution: - { - integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==, - } - hasBin: true - - lru-cache@11.5.1: - resolution: - { - integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==, - } - engines: { node: 20 || >=22 } - - lru-cache@5.1.1: - resolution: - { - integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, - } - - lz-string@1.5.0: - resolution: - { - integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==, - } - hasBin: true - - magic-string@0.30.21: - resolution: - { - integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, - } - - magicast@0.5.3: - resolution: - { - integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==, - } - - make-dir@4.0.0: - resolution: - { - integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==, - } - engines: { node: ">=10" } - - make-error@1.3.6: - resolution: - { - integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==, - } - - math-intrinsics@1.1.0: - resolution: - { - integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, - } - engines: { node: ">= 0.4" } - - mdn-data@2.27.1: - resolution: - { - integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==, - } - - media-typer@0.3.0: - resolution: - { - integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==, - } - engines: { node: ">= 0.6" } - - media-typer@1.1.0: - resolution: - { - integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==, - } - engines: { node: ">= 0.8" } - - merge-descriptors@2.0.0: - resolution: - { - integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==, - } - engines: { node: ">=18" } - - mime-db@1.52.0: - resolution: - { - integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, - } - engines: { node: ">= 0.6" } - - mime-db@1.54.0: - resolution: - { - integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==, - } - engines: { node: ">= 0.6" } - - mime-types@2.1.35: - resolution: - { - integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, - } - engines: { node: ">= 0.6" } - - mime-types@3.0.2: - resolution: - { - integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==, - } - engines: { node: ">=18" } - - min-indent@1.0.1: - resolution: - { - integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==, - } - engines: { node: ">=4" } - - minimatch@10.2.5: - resolution: - { - integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==, - } - engines: { node: 18 || 20 || >=22 } - - minimatch@3.1.5: - resolution: - { - integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==, - } - - minimist@1.2.8: - resolution: - { - integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, - } - - mkdirp@0.5.6: - resolution: - { - integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==, - } - hasBin: true - - mkdirp@1.0.4: - resolution: - { - integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==, - } - engines: { node: ">=10" } - hasBin: true - - ms@2.1.3: - resolution: - { - integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, - } - - multer@1.4.5-lts.2: - resolution: - { - integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==, - } - engines: { node: ">= 6.0.0" } - deprecated: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version. - - multer@2.1.1: - resolution: - { - integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==, - } - engines: { node: ">= 10.16.0" } - - nanoid@3.3.15: - resolution: - { - integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==, - } - engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } - hasBin: true - - natural-compare@1.4.0: - resolution: - { - integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, - } - - negotiator@1.0.0: - resolution: - { - integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==, - } - engines: { node: ">= 0.6" } - - node-exports-info@1.6.2: - resolution: - { - integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==, - } - engines: { node: ">= 0.4" } - - node-releases@2.0.50: - resolution: - { - integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==, - } - engines: { node: ">=18" } - - normalize-path@3.0.0: - resolution: - { - integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==, - } - engines: { node: ">=0.10.0" } - - object-assign@4.1.1: - resolution: - { - integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, - } - engines: { node: ">=0.10.0" } - - object-inspect@1.13.4: - resolution: - { - integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, - } - engines: { node: ">= 0.4" } - - object-keys@1.1.1: - resolution: - { - integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==, - } - engines: { node: ">= 0.4" } - - object.assign@4.1.7: - resolution: - { - integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==, - } - engines: { node: ">= 0.4" } - - object.entries@1.1.9: - resolution: - { - integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==, - } - engines: { node: ">= 0.4" } - - object.fromentries@2.0.8: - resolution: - { - integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==, - } - engines: { node: ">= 0.4" } - - object.values@1.2.1: - resolution: - { - integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==, - } - engines: { node: ">= 0.4" } - - obug@2.1.3: - resolution: - { - integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==, - } - engines: { node: ">=12.20.0" } - - on-finished@2.4.1: - resolution: - { - integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==, - } - engines: { node: ">= 0.8" } - - once@1.4.0: - resolution: - { - integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, - } - - optionator@0.9.4: - resolution: - { - integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, - } - engines: { node: ">= 0.8.0" } - - own-keys@1.0.1: - resolution: - { - integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==, - } - engines: { node: ">= 0.4" } - - p-limit@3.1.0: - resolution: - { - integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, - } - engines: { node: ">=10" } - - p-locate@5.0.0: - resolution: - { - integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, - } - engines: { node: ">=10" } - - pako@1.0.11: - resolution: - { - integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==, - } - - parse5@8.0.1: - resolution: - { - integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==, - } - - parseurl@1.3.3: - resolution: - { - integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==, - } - engines: { node: ">= 0.8" } - - path-exists@4.0.0: - resolution: - { - integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, - } - engines: { node: ">=8" } - - path-is-absolute@1.0.1: - resolution: - { - integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, - } - engines: { node: ">=0.10.0" } - - path-key@3.1.1: - resolution: - { - integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, - } - engines: { node: ">=8" } - - path-parse@1.0.7: - resolution: - { - integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, - } - - path-to-regexp@8.4.2: - resolution: - { - integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==, - } - - pathe@2.0.3: - resolution: - { - integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, - } - - picocolors@1.1.1: - resolution: - { - integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, - } - - picomatch@2.3.2: - resolution: - { - integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==, - } - engines: { node: ">=8.6" } - - picomatch@4.0.4: - resolution: - { - integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==, - } - engines: { node: ">=12" } - - possible-typed-array-names@1.1.0: - resolution: - { - integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==, - } - engines: { node: ">= 0.4" } - - postcss@8.5.15: - resolution: - { - integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==, - } - engines: { node: ^10 || ^12 || >=14 } - - prelude-ls@1.2.1: - resolution: - { - integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, - } - engines: { node: ">= 0.8.0" } - - pretty-format@27.5.1: - resolution: - { - integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==, - } - engines: { node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0 } - - process-nextick-args@2.0.1: - resolution: - { - integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==, - } - - prop-types@15.8.1: - resolution: - { - integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, - } - - proxy-addr@2.0.7: - resolution: - { - integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==, - } - engines: { node: ">= 0.10" } - - punycode@2.3.1: - resolution: - { - integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, - } - engines: { node: ">=6" } - - qs@6.15.3: - resolution: - { - integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==, - } - engines: { node: ">=0.6" } - - range-parser@1.3.0: - resolution: - { - integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==, - } - engines: { node: ">= 0.6" } - - raw-body@3.0.2: - resolution: - { - integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==, - } - engines: { node: ">= 0.10" } - - react-dom@19.2.7: - resolution: - { - integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==, - } - peerDependencies: - react: ^19.2.7 - - react-is@16.13.1: - resolution: - { - integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==, - } - - react-is@17.0.2: - resolution: - { - integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==, - } - - react-refresh@0.17.0: - resolution: - { - integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==, - } - engines: { node: ">=0.10.0" } - - react@19.2.7: - resolution: - { - integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==, - } - engines: { node: ">=0.10.0" } - - readable-stream@2.3.8: - resolution: - { - integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==, - } - - readable-stream@3.6.2: - resolution: - { - integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==, - } - engines: { node: ">= 6" } - - readdirp@3.6.0: - resolution: - { - integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, - } - engines: { node: ">=8.10.0" } - - redent@3.0.0: - resolution: - { - integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==, - } - engines: { node: ">=8" } - - reflect-metadata@0.2.2: - resolution: - { - integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==, - } - - reflect.getprototypeof@1.0.10: - resolution: - { - integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==, - } - engines: { node: ">= 0.4" } - - regexp.prototype.flags@1.5.4: - resolution: - { - integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==, - } - engines: { node: ">= 0.4" } - - require-from-string@2.0.2: - resolution: - { - integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==, - } - engines: { node: ">=0.10.0" } - - resolve@1.22.12: - resolution: - { - integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==, - } - engines: { node: ">= 0.4" } - hasBin: true - - resolve@2.0.0-next.7: - resolution: - { - integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==, - } - engines: { node: ">= 0.4" } - hasBin: true - - rimraf@2.7.1: - resolution: - { - integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==, - } - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rollup@4.62.2: - resolution: - { - integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==, - } - engines: { node: ">=18.0.0", npm: ">=8.0.0" } - hasBin: true - - router@2.2.0: - resolution: - { - integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==, - } - engines: { node: ">= 18" } - - rxjs@7.8.2: - resolution: - { - integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==, - } - - safe-array-concat@1.1.4: - resolution: - { - integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==, - } - engines: { node: ">=0.4" } - - safe-buffer@5.1.2: - resolution: - { - integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==, - } - - safe-buffer@5.2.1: - resolution: - { - integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, - } - - safe-push-apply@1.0.0: - resolution: - { - integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==, - } - engines: { node: ">= 0.4" } - - safe-regex-test@1.1.0: - resolution: - { - integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==, - } - engines: { node: ">= 0.4" } - - safer-buffer@2.1.2: - resolution: - { - integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, - } - - saxes@6.0.0: - resolution: - { - integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==, - } - engines: { node: ">=v12.22.7" } - - scheduler@0.27.0: - resolution: - { - integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==, - } - - semver@6.3.1: - resolution: - { - integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, - } - hasBin: true - - semver@7.8.5: - resolution: - { - integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==, - } - engines: { node: ">=10" } - hasBin: true - - send@1.2.1: - resolution: - { - integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==, - } - engines: { node: ">= 18" } - - serve-static@2.2.1: - resolution: - { - integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==, - } - engines: { node: ">= 18" } - - set-function-length@1.2.2: - resolution: - { - integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==, - } - engines: { node: ">= 0.4" } - - set-function-name@2.0.2: - resolution: - { - integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==, - } - engines: { node: ">= 0.4" } - - set-proto@1.0.0: - resolution: - { - integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==, - } - engines: { node: ">= 0.4" } - - setimmediate@1.0.5: - resolution: - { - integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==, - } - - setprototypeof@1.2.0: - resolution: - { - integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, - } - - shebang-command@2.0.0: - resolution: - { - integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, - } - engines: { node: ">=8" } - - shebang-regex@3.0.0: - resolution: - { - integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, - } - engines: { node: ">=8" } - - side-channel-list@1.0.1: - resolution: - { - integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==, - } - engines: { node: ">= 0.4" } - - side-channel-map@1.0.1: - resolution: - { - integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==, - } - engines: { node: ">= 0.4" } - - side-channel-weakmap@1.0.2: - resolution: - { - integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==, - } - engines: { node: ">= 0.4" } - - side-channel@1.1.1: - resolution: - { - integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==, - } - engines: { node: ">= 0.4" } - - siginfo@2.0.0: - resolution: - { - integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==, - } - - source-map-js@1.2.1: - resolution: - { - integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, - } - engines: { node: ">=0.10.0" } - - source-map-support@0.5.21: - resolution: - { - integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==, - } - - source-map@0.6.1: - resolution: - { - integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, - } - engines: { node: ">=0.10.0" } - - stackback@0.0.2: - resolution: - { - integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, - } - - statuses@2.0.2: - resolution: - { - integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==, - } - engines: { node: ">= 0.8" } - - std-env@4.1.0: - resolution: - { - integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==, - } - - stop-iteration-iterator@1.1.0: - resolution: - { - integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==, - } - engines: { node: ">= 0.4" } - - streamsearch@1.1.0: - resolution: - { - integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==, - } - engines: { node: ">=10.0.0" } - - string.prototype.matchall@4.0.12: - resolution: - { - integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==, - } - engines: { node: ">= 0.4" } - - string.prototype.repeat@1.0.0: - resolution: - { - integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==, - } - - string.prototype.trim@1.2.11: - resolution: - { - integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==, - } - engines: { node: ">= 0.4" } - - string.prototype.trimend@1.0.10: - resolution: - { - integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==, - } - engines: { node: ">= 0.4" } - - string.prototype.trimstart@1.0.8: - resolution: - { - integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==, - } - engines: { node: ">= 0.4" } - - string_decoder@1.1.1: - resolution: - { - integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==, - } - - string_decoder@1.3.0: - resolution: - { - integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, - } - - strip-bom@3.0.0: - resolution: - { - integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==, - } - engines: { node: ">=4" } - - strip-indent@3.0.0: - resolution: - { - integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==, - } - engines: { node: ">=8" } - - strip-json-comments@2.0.1: - resolution: - { - integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==, - } - engines: { node: ">=0.10.0" } - - strtok3@10.3.5: - resolution: - { - integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==, - } - engines: { node: ">=18" } - - supports-color@7.2.0: - resolution: - { - integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, - } - engines: { node: ">=8" } - - supports-preserve-symlinks-flag@1.0.0: - resolution: - { - integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, - } - engines: { node: ">= 0.4" } - - symbol-tree@3.2.4: - resolution: - { - integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, - } - - tinybench@2.9.0: - resolution: - { - integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==, - } - - tinyexec@1.2.4: - resolution: - { - integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==, - } - engines: { node: ">=18" } - - tinyglobby@0.2.17: - resolution: - { - integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==, - } - engines: { node: ">=12.0.0" } - - tinyrainbow@3.1.0: - resolution: - { - integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==, - } - engines: { node: ">=14.0.0" } - - tldts-core@7.4.4: - resolution: - { - integrity: sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==, - } - - tldts@7.4.4: - resolution: - { - integrity: sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==, - } - hasBin: true - - to-regex-range@5.0.1: - resolution: - { - integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, - } - engines: { node: ">=8.0" } - - toidentifier@1.0.1: - resolution: - { - integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==, - } - engines: { node: ">=0.6" } - - token-types@6.1.2: - resolution: - { - integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==, - } - engines: { node: ">=14.16" } - - tough-cookie@6.0.1: - resolution: - { - integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==, - } - engines: { node: ">=16" } - - tr46@6.0.0: - resolution: - { - integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==, - } - engines: { node: ">=20" } - - tree-kill@1.2.2: - resolution: - { - integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==, - } - hasBin: true - - ts-api-utils@2.5.0: - resolution: - { - integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==, - } - engines: { node: ">=18.12" } - peerDependencies: - typescript: ">=4.8.4" - - ts-node-dev@2.0.0: - resolution: - { - integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==, - } - engines: { node: ">=0.8.0" } - hasBin: true - peerDependencies: - node-notifier: "*" - typescript: "*" - peerDependenciesMeta: - node-notifier: - optional: true - - ts-node@10.9.2: - resolution: - { - integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==, - } - hasBin: true - peerDependencies: - "@swc/core": ">=1.2.50" - "@swc/wasm": ">=1.2.50" - "@types/node": "*" - typescript: ">=2.7" - peerDependenciesMeta: - "@swc/core": - optional: true - "@swc/wasm": - optional: true - - tsconfig@7.0.0: - resolution: - { - integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==, - } - - tslib@2.8.1: - resolution: - { - integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, - } - - type-check@0.4.0: - resolution: - { - integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, - } - engines: { node: ">= 0.8.0" } - - type-is@1.6.18: - resolution: - { - integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==, - } - engines: { node: ">= 0.6" } - - type-is@2.1.0: - resolution: - { - integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==, - } - engines: { node: ">= 18" } - - typed-array-buffer@1.0.3: - resolution: - { - integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==, - } - engines: { node: ">= 0.4" } - - typed-array-byte-length@1.0.3: - resolution: - { - integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==, - } - engines: { node: ">= 0.4" } - - typed-array-byte-offset@1.0.4: - resolution: - { - integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==, - } - engines: { node: ">= 0.4" } - - typed-array-length@1.0.8: - resolution: - { - integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==, - } - engines: { node: ">= 0.4" } - - typedarray@0.0.6: - resolution: - { - integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==, - } - - typescript-eslint@8.62.0: - resolution: - { - integrity: sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==, - } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.1.0" - - typescript@5.8.3: - resolution: - { - integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==, - } - engines: { node: ">=14.17" } - hasBin: true - - typescript@5.9.3: - resolution: - { - integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, - } - engines: { node: ">=14.17" } - hasBin: true - - uid@2.0.2: - resolution: - { - integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==, - } - engines: { node: ">=8" } - - uint8array-extras@1.5.0: - resolution: - { - integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==, - } - engines: { node: ">=18" } - - unbox-primitive@1.1.0: - resolution: - { - integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==, - } - engines: { node: ">= 0.4" } - - undici-types@6.21.0: - resolution: - { - integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==, - } - - undici@7.28.0: - resolution: - { - integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==, - } - engines: { node: ">=20.18.1" } - - unpipe@1.0.0: - resolution: - { - integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==, - } - engines: { node: ">= 0.8" } - - update-browserslist-db@1.2.3: - resolution: - { - integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==, - } - hasBin: true - peerDependencies: - browserslist: ">= 4.21.0" - - uri-js@4.4.1: - resolution: - { - integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, - } - - util-deprecate@1.0.2: - resolution: - { - integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, - } - - v8-compile-cache-lib@3.0.1: - resolution: - { - integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==, - } - - vary@1.1.2: - resolution: - { - integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, - } - engines: { node: ">= 0.8" } - - vite@6.4.3: - resolution: - { - integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==, - } - engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 } - hasBin: true - peerDependencies: - "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: ">=1.21.0" - less: "*" - lightningcss: ^1.21.0 - sass: "*" - sass-embedded: "*" - stylus: "*" - sugarss: "*" - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - "@types/node": - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@4.1.9: - resolution: - { - integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==, - } - engines: { node: ^20.0.0 || ^22.0.0 || >=24.0.0 } - hasBin: true - peerDependencies: - "@edge-runtime/vm": "*" - "@opentelemetry/api": ^1.9.0 - "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.1.9 - "@vitest/browser-preview": 4.1.9 - "@vitest/browser-webdriverio": 4.1.9 - "@vitest/coverage-istanbul": 4.1.9 - "@vitest/coverage-v8": 4.1.9 - "@vitest/ui": 4.1.9 - happy-dom: "*" - jsdom: "*" - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - "@edge-runtime/vm": - optional: true - "@opentelemetry/api": - optional: true - "@types/node": - optional: true - "@vitest/browser-playwright": - optional: true - "@vitest/browser-preview": - optional: true - "@vitest/browser-webdriverio": - optional: true - "@vitest/coverage-istanbul": - optional: true - "@vitest/coverage-v8": - optional: true - "@vitest/ui": - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - w3c-xmlserializer@5.0.0: - resolution: - { - integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==, - } - engines: { node: ">=18" } - - webidl-conversions@8.0.1: - resolution: - { - integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==, - } - engines: { node: ">=20" } - - whatwg-mimetype@5.0.0: - resolution: - { - integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==, - } - engines: { node: ">=20" } - - whatwg-url@16.0.1: - resolution: - { - integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==, - } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } - - which-boxed-primitive@1.1.1: - resolution: - { - integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==, - } - engines: { node: ">= 0.4" } - - which-builtin-type@1.2.1: - resolution: - { - integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==, - } - engines: { node: ">= 0.4" } - - which-collection@1.0.2: - resolution: - { - integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==, - } - engines: { node: ">= 0.4" } - - which-typed-array@1.1.22: - resolution: - { - integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==, - } - engines: { node: ">= 0.4" } - - which@2.0.2: - resolution: - { - integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, - } - engines: { node: ">= 8" } - hasBin: true - - why-is-node-running@2.3.0: - resolution: - { - integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==, - } - engines: { node: ">=8" } - hasBin: true - - word-wrap@1.2.5: - resolution: - { - integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, - } - engines: { node: ">=0.10.0" } - - wrappy@1.0.2: - resolution: - { - integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, - } - - xml-name-validator@5.0.0: - resolution: - { - integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==, - } - engines: { node: ">=18" } - - xmlchars@2.2.0: - resolution: - { - integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==, - } - - xtend@4.0.2: - resolution: - { - integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==, - } - engines: { node: ">=0.4" } - - yallist@3.1.1: - resolution: - { - integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, - } - - yn@3.1.1: - resolution: - { - integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==, - } - engines: { node: ">=6" } - - yocto-queue@0.1.0: - resolution: - { - integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, - } - engines: { node: ">=10" } - - zod-validation-error@4.0.2: - resolution: - { - integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==, - } - engines: { node: ">=18.0.0" } - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - - zod@4.4.3: - resolution: - { - integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==, - } - -snapshots: - "@adobe/css-tools@4.5.0": {} - - "@asamuzakjp/css-color@5.1.11": - dependencies: - "@asamuzakjp/generational-cache": 1.0.1 - "@csstools/css-calc": 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - "@csstools/css-color-parser": 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) - "@csstools/css-tokenizer": 4.0.0 - - "@asamuzakjp/dom-selector@7.1.1": - dependencies: - "@asamuzakjp/generational-cache": 1.0.1 - "@asamuzakjp/nwsapi": 2.3.9 - bidi-js: 1.0.3 - css-tree: 3.2.1 - is-potential-custom-element-name: 1.0.1 - - "@asamuzakjp/generational-cache@1.0.1": {} - - "@asamuzakjp/nwsapi@2.3.9": {} - - "@babel/code-frame@7.29.7": - dependencies: - "@babel/helper-validator-identifier": 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - "@babel/compat-data@7.29.7": {} - - "@babel/core@7.29.7": - dependencies: - "@babel/code-frame": 7.29.7 - "@babel/generator": 7.29.7 - "@babel/helper-compilation-targets": 7.29.7 - "@babel/helper-module-transforms": 7.29.7(@babel/core@7.29.7) - "@babel/helpers": 7.29.7 - "@babel/parser": 7.29.7 - "@babel/template": 7.29.7 - "@babel/traverse": 7.29.7 - "@babel/types": 7.29.7 - "@jridgewell/remapping": 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - "@babel/generator@7.29.7": - dependencies: - "@babel/parser": 7.29.7 - "@babel/types": 7.29.7 - "@jridgewell/gen-mapping": 0.3.13 - "@jridgewell/trace-mapping": 0.3.31 - jsesc: 3.1.0 - - "@babel/helper-compilation-targets@7.29.7": - dependencies: - "@babel/compat-data": 7.29.7 - "@babel/helper-validator-option": 7.29.7 - browserslist: 4.28.4 - lru-cache: 5.1.1 - semver: 6.3.1 - - "@babel/helper-globals@7.29.7": {} - - "@babel/helper-module-imports@7.29.7": - dependencies: - "@babel/traverse": 7.29.7 - "@babel/types": 7.29.7 - transitivePeerDependencies: - - supports-color - - "@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)": - dependencies: - "@babel/core": 7.29.7 - "@babel/helper-module-imports": 7.29.7 - "@babel/helper-validator-identifier": 7.29.7 - "@babel/traverse": 7.29.7 - transitivePeerDependencies: - - supports-color - - "@babel/helper-plugin-utils@7.29.7": {} - - "@babel/helper-string-parser@7.29.7": {} - - "@babel/helper-validator-identifier@7.29.7": {} - - "@babel/helper-validator-option@7.29.7": {} - - "@babel/helpers@7.29.7": - dependencies: - "@babel/template": 7.29.7 - "@babel/types": 7.29.7 - - "@babel/parser@7.29.7": - dependencies: - "@babel/types": 7.29.7 - - "@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)": - dependencies: - "@babel/core": 7.29.7 - "@babel/helper-plugin-utils": 7.29.7 - - "@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)": - dependencies: - "@babel/core": 7.29.7 - "@babel/helper-plugin-utils": 7.29.7 - - "@babel/runtime@7.29.7": {} - - "@babel/template@7.29.7": - dependencies: - "@babel/code-frame": 7.29.7 - "@babel/parser": 7.29.7 - "@babel/types": 7.29.7 - - "@babel/traverse@7.29.7": - dependencies: - "@babel/code-frame": 7.29.7 - "@babel/generator": 7.29.7 - "@babel/helper-globals": 7.29.7 - "@babel/parser": 7.29.7 - "@babel/template": 7.29.7 - "@babel/types": 7.29.7 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - "@babel/types@7.29.7": - dependencies: - "@babel/helper-string-parser": 7.29.7 - "@babel/helper-validator-identifier": 7.29.7 - - "@bcoe/v8-coverage@1.0.2": {} - - "@borewit/text-codec@0.2.2": {} - - "@bramus/specificity@2.4.2": - dependencies: - css-tree: 3.2.1 - - "@cspotcode/source-map-support@0.8.1": - dependencies: - "@jridgewell/trace-mapping": 0.3.9 - - "@csstools/color-helpers@6.1.0": {} - - "@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)": - dependencies: - "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) - "@csstools/css-tokenizer": 4.0.0 - - "@csstools/css-color-parser@4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)": - dependencies: - "@csstools/color-helpers": 6.1.0 - "@csstools/css-calc": 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) - "@csstools/css-tokenizer": 4.0.0 - - "@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)": - dependencies: - "@csstools/css-tokenizer": 4.0.0 - - "@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)": - optionalDependencies: - css-tree: 3.2.1 - - "@csstools/css-tokenizer@4.0.0": {} - - "@esbuild/aix-ppc64@0.25.12": - optional: true - - "@esbuild/android-arm64@0.25.12": - optional: true - - "@esbuild/android-arm@0.25.12": - optional: true - - "@esbuild/android-x64@0.25.12": - optional: true - - "@esbuild/darwin-arm64@0.25.12": - optional: true - - "@esbuild/darwin-x64@0.25.12": - optional: true - - "@esbuild/freebsd-arm64@0.25.12": - optional: true - - "@esbuild/freebsd-x64@0.25.12": - optional: true - - "@esbuild/linux-arm64@0.25.12": - optional: true - - "@esbuild/linux-arm@0.25.12": - optional: true - - "@esbuild/linux-ia32@0.25.12": - optional: true - - "@esbuild/linux-loong64@0.25.12": - optional: true - - "@esbuild/linux-mips64el@0.25.12": - optional: true - - "@esbuild/linux-ppc64@0.25.12": - optional: true - - "@esbuild/linux-riscv64@0.25.12": - optional: true - - "@esbuild/linux-s390x@0.25.12": - optional: true - - "@esbuild/linux-x64@0.25.12": - optional: true - - "@esbuild/netbsd-arm64@0.25.12": - optional: true - - "@esbuild/netbsd-x64@0.25.12": - optional: true - - "@esbuild/openbsd-arm64@0.25.12": - optional: true - - "@esbuild/openbsd-x64@0.25.12": - optional: true - - "@esbuild/openharmony-arm64@0.25.12": - optional: true - - "@esbuild/sunos-x64@0.25.12": - optional: true - - "@esbuild/win32-arm64@0.25.12": - optional: true - - "@esbuild/win32-ia32@0.25.12": - optional: true - - "@esbuild/win32-x64@0.25.12": - optional: true - - "@eslint-community/eslint-utils@4.9.1(eslint@10.6.0)": - dependencies: - eslint: 10.6.0 - eslint-visitor-keys: 3.4.3 - - "@eslint-community/regexpp@4.12.2": {} - - "@eslint/config-array@0.23.5": - dependencies: - "@eslint/object-schema": 3.0.5 - debug: 4.4.3 - minimatch: 10.2.5 - transitivePeerDependencies: - - supports-color - - "@eslint/config-helpers@0.6.0": - dependencies: - "@eslint/core": 1.2.1 - - "@eslint/core@1.2.1": - dependencies: - "@types/json-schema": 7.0.15 - - "@eslint/js@10.0.1(eslint@10.6.0)": - optionalDependencies: - eslint: 10.6.0 - - "@eslint/object-schema@3.0.5": {} - - "@eslint/plugin-kit@0.7.2": - dependencies: - "@eslint/core": 1.2.1 - levn: 0.4.1 - - "@exodus/bytes@1.15.1": {} - - "@humanfs/core@0.19.2": - dependencies: - "@humanfs/types": 0.15.0 - - "@humanfs/node@0.16.8": - dependencies: - "@humanfs/core": 0.19.2 - "@humanfs/types": 0.15.0 - "@humanwhocodes/retry": 0.4.3 - - "@humanfs/types@0.15.0": {} - - "@humanwhocodes/module-importer@1.0.1": {} - - "@humanwhocodes/retry@0.4.3": {} - - "@jridgewell/gen-mapping@0.3.13": - dependencies: - "@jridgewell/sourcemap-codec": 1.5.5 - "@jridgewell/trace-mapping": 0.3.31 - - "@jridgewell/remapping@2.3.5": - dependencies: - "@jridgewell/gen-mapping": 0.3.13 - "@jridgewell/trace-mapping": 0.3.31 - - "@jridgewell/resolve-uri@3.1.2": {} - - "@jridgewell/sourcemap-codec@1.5.5": {} - - "@jridgewell/trace-mapping@0.3.31": - dependencies: - "@jridgewell/resolve-uri": 3.1.2 - "@jridgewell/sourcemap-codec": 1.5.5 - - "@jridgewell/trace-mapping@0.3.9": - dependencies: - "@jridgewell/resolve-uri": 3.1.2 - "@jridgewell/sourcemap-codec": 1.5.5 - - "@lukeed/csprng@1.1.0": {} - - "@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2)": - dependencies: - file-type: 21.3.4 - iterare: 1.2.1 - load-esm: 1.0.3 - reflect-metadata: 0.2.2 - rxjs: 7.8.2 - tslib: 2.8.1 - uid: 2.0.2 - transitivePeerDependencies: - - supports-color - - "@nestjs/core@11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)": - dependencies: - "@nestjs/common": 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2) - fast-safe-stringify: 2.1.1 - iterare: 1.2.1 - path-to-regexp: 8.4.2 - reflect-metadata: 0.2.2 - rxjs: 7.8.2 - tslib: 2.8.1 - uid: 2.0.2 - optionalDependencies: - "@nestjs/platform-express": 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27) - - "@nestjs/platform-express@11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)": - dependencies: - "@nestjs/common": 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2) - "@nestjs/core": 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) - cors: 2.8.6 - express: 5.2.1 - multer: 2.1.1 - path-to-regexp: 8.4.2 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - "@rolldown/pluginutils@1.0.0-beta.27": {} - - "@rollup/rollup-android-arm-eabi@4.62.2": - optional: true - - "@rollup/rollup-android-arm64@4.62.2": - optional: true - - "@rollup/rollup-darwin-arm64@4.62.2": - optional: true - - "@rollup/rollup-darwin-x64@4.62.2": - optional: true - - "@rollup/rollup-freebsd-arm64@4.62.2": - optional: true - - "@rollup/rollup-freebsd-x64@4.62.2": - optional: true - - "@rollup/rollup-linux-arm-gnueabihf@4.62.2": - optional: true - - "@rollup/rollup-linux-arm-musleabihf@4.62.2": - optional: true - - "@rollup/rollup-linux-arm64-gnu@4.62.2": - optional: true - - "@rollup/rollup-linux-arm64-musl@4.62.2": - optional: true - - "@rollup/rollup-linux-loong64-gnu@4.62.2": - optional: true - - "@rollup/rollup-linux-loong64-musl@4.62.2": - optional: true - - "@rollup/rollup-linux-ppc64-gnu@4.62.2": - optional: true - - "@rollup/rollup-linux-ppc64-musl@4.62.2": - optional: true - - "@rollup/rollup-linux-riscv64-gnu@4.62.2": - optional: true - - "@rollup/rollup-linux-riscv64-musl@4.62.2": - optional: true - - "@rollup/rollup-linux-s390x-gnu@4.62.2": - optional: true - - "@rollup/rollup-linux-x64-gnu@4.62.2": - optional: true - - "@rollup/rollup-linux-x64-musl@4.62.2": - optional: true - - "@rollup/rollup-openbsd-x64@4.62.2": - optional: true - - "@rollup/rollup-openharmony-arm64@4.62.2": - optional: true - - "@rollup/rollup-win32-arm64-msvc@4.62.2": - optional: true - - "@rollup/rollup-win32-ia32-msvc@4.62.2": - optional: true - - "@rollup/rollup-win32-x64-gnu@4.62.2": - optional: true - - "@rollup/rollup-win32-x64-msvc@4.62.2": - optional: true - - "@standard-schema/spec@1.1.0": {} - - "@testing-library/dom@10.4.1": - dependencies: - "@babel/code-frame": 7.29.7 - "@babel/runtime": 7.29.7 - "@types/aria-query": 5.0.4 - aria-query: 5.3.0 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - picocolors: 1.1.1 - pretty-format: 27.5.1 - - "@testing-library/jest-dom@6.9.1": - dependencies: - "@adobe/css-tools": 4.5.0 - aria-query: 5.3.0 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - picocolors: 1.1.1 - redent: 3.0.0 - - "@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@babel/runtime": 7.29.7 - "@testing-library/dom": 10.4.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)": - dependencies: - "@testing-library/dom": 10.4.1 - - "@tokenizer/inflate@0.4.1": - dependencies: - debug: 4.4.3 - token-types: 6.1.2 - transitivePeerDependencies: - - supports-color - - "@tokenizer/token@0.3.0": {} - - "@tsconfig/node10@1.0.12": {} - - "@tsconfig/node12@1.0.11": {} - - "@tsconfig/node14@1.0.3": {} - - "@tsconfig/node16@1.0.4": {} - - "@types/aria-query@5.0.4": {} - - "@types/babel__core@7.20.5": - dependencies: - "@babel/parser": 7.29.7 - "@babel/types": 7.29.7 - "@types/babel__generator": 7.27.0 - "@types/babel__template": 7.4.4 - "@types/babel__traverse": 7.28.0 - - "@types/babel__generator@7.27.0": - dependencies: - "@babel/types": 7.29.7 - - "@types/babel__template@7.4.4": - dependencies: - "@babel/parser": 7.29.7 - "@babel/types": 7.29.7 - - "@types/babel__traverse@7.28.0": - dependencies: - "@babel/types": 7.29.7 - - "@types/body-parser@1.19.6": - dependencies: - "@types/connect": 3.4.38 - "@types/node": 22.20.0 - - "@types/chai@5.2.3": - dependencies: - "@types/deep-eql": 4.0.2 - assertion-error: 2.0.1 - - "@types/connect@3.4.38": - dependencies: - "@types/node": 22.20.0 - - "@types/deep-eql@4.0.2": {} - - "@types/esrecurse@4.3.1": {} - - "@types/estree@1.0.9": {} - - "@types/express-serve-static-core@5.1.1": - dependencies: - "@types/node": 22.20.0 - "@types/qs": 6.15.1 - "@types/range-parser": 1.2.7 - "@types/send": 1.2.1 - - "@types/express@5.0.6": - dependencies: - "@types/body-parser": 1.19.6 - "@types/express-serve-static-core": 5.1.1 - "@types/serve-static": 2.2.0 - - "@types/http-errors@2.0.5": {} - - "@types/json-schema@7.0.15": {} - - "@types/multer@1.4.13": - dependencies: - "@types/express": 5.0.6 - - "@types/node@22.20.0": - dependencies: - undici-types: 6.21.0 - - "@types/qs@6.15.1": {} - - "@types/range-parser@1.2.7": {} - - "@types/react-dom@19.2.3(@types/react@19.2.17)": - dependencies: - "@types/react": 19.2.17 - - "@types/react@19.2.17": - dependencies: - csstype: 3.2.3 - - "@types/send@1.2.1": - dependencies: - "@types/node": 22.20.0 - - "@types/serve-static@2.2.0": - dependencies: - "@types/http-errors": 2.0.5 - "@types/node": 22.20.0 - - "@types/strip-bom@3.0.0": {} - - "@types/strip-json-comments@0.0.30": {} - - "@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.6.0)(typescript@5.8.3))(eslint@10.6.0)(typescript@5.8.3)": - dependencies: - "@eslint-community/regexpp": 4.12.2 - "@typescript-eslint/parser": 8.62.0(eslint@10.6.0)(typescript@5.8.3) - "@typescript-eslint/scope-manager": 8.62.0 - "@typescript-eslint/type-utils": 8.62.0(eslint@10.6.0)(typescript@5.8.3) - "@typescript-eslint/utils": 8.62.0(eslint@10.6.0)(typescript@5.8.3) - "@typescript-eslint/visitor-keys": 8.62.0 - eslint: 10.6.0 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0)(typescript@5.9.3)": - dependencies: - "@eslint-community/regexpp": 4.12.2 - "@typescript-eslint/parser": 8.62.0(eslint@10.6.0)(typescript@5.9.3) - "@typescript-eslint/scope-manager": 8.62.0 - "@typescript-eslint/type-utils": 8.62.0(eslint@10.6.0)(typescript@5.9.3) - "@typescript-eslint/utils": 8.62.0(eslint@10.6.0)(typescript@5.9.3) - "@typescript-eslint/visitor-keys": 8.62.0 - eslint: 10.6.0 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/parser@8.62.0(eslint@10.6.0)(typescript@5.8.3)": - dependencies: - "@typescript-eslint/scope-manager": 8.62.0 - "@typescript-eslint/types": 8.62.0 - "@typescript-eslint/typescript-estree": 8.62.0(typescript@5.8.3) - "@typescript-eslint/visitor-keys": 8.62.0 - debug: 4.4.3 - eslint: 10.6.0 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/parser@8.62.0(eslint@10.6.0)(typescript@5.9.3)": - dependencies: - "@typescript-eslint/scope-manager": 8.62.0 - "@typescript-eslint/types": 8.62.0 - "@typescript-eslint/typescript-estree": 8.62.0(typescript@5.9.3) - "@typescript-eslint/visitor-keys": 8.62.0 - debug: 4.4.3 - eslint: 10.6.0 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/project-service@8.62.0(typescript@5.8.3)": - dependencies: - "@typescript-eslint/tsconfig-utils": 8.62.0(typescript@5.8.3) - "@typescript-eslint/types": 8.62.0 - debug: 4.4.3 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/project-service@8.62.0(typescript@5.9.3)": - dependencies: - "@typescript-eslint/tsconfig-utils": 8.62.0(typescript@5.9.3) - "@typescript-eslint/types": 8.62.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/scope-manager@8.62.0": - dependencies: - "@typescript-eslint/types": 8.62.0 - "@typescript-eslint/visitor-keys": 8.62.0 - - "@typescript-eslint/tsconfig-utils@8.62.0(typescript@5.8.3)": - dependencies: - typescript: 5.8.3 - - "@typescript-eslint/tsconfig-utils@8.62.0(typescript@5.9.3)": - dependencies: - typescript: 5.9.3 - - "@typescript-eslint/type-utils@8.62.0(eslint@10.6.0)(typescript@5.8.3)": - dependencies: - "@typescript-eslint/types": 8.62.0 - "@typescript-eslint/typescript-estree": 8.62.0(typescript@5.8.3) - "@typescript-eslint/utils": 8.62.0(eslint@10.6.0)(typescript@5.8.3) - debug: 4.4.3 - eslint: 10.6.0 - ts-api-utils: 2.5.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/type-utils@8.62.0(eslint@10.6.0)(typescript@5.9.3)": - dependencies: - "@typescript-eslint/types": 8.62.0 - "@typescript-eslint/typescript-estree": 8.62.0(typescript@5.9.3) - "@typescript-eslint/utils": 8.62.0(eslint@10.6.0)(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.6.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/types@8.62.0": {} - - "@typescript-eslint/typescript-estree@8.62.0(typescript@5.8.3)": - dependencies: - "@typescript-eslint/project-service": 8.62.0(typescript@5.8.3) - "@typescript-eslint/tsconfig-utils": 8.62.0(typescript@5.8.3) - "@typescript-eslint/types": 8.62.0 - "@typescript-eslint/visitor-keys": 8.62.0 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.5 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/typescript-estree@8.62.0(typescript@5.9.3)": - dependencies: - "@typescript-eslint/project-service": 8.62.0(typescript@5.9.3) - "@typescript-eslint/tsconfig-utils": 8.62.0(typescript@5.9.3) - "@typescript-eslint/types": 8.62.0 - "@typescript-eslint/visitor-keys": 8.62.0 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.5 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/utils@8.62.0(eslint@10.6.0)(typescript@5.8.3)": - dependencies: - "@eslint-community/eslint-utils": 4.9.1(eslint@10.6.0) - "@typescript-eslint/scope-manager": 8.62.0 - "@typescript-eslint/types": 8.62.0 - "@typescript-eslint/typescript-estree": 8.62.0(typescript@5.8.3) - eslint: 10.6.0 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/utils@8.62.0(eslint@10.6.0)(typescript@5.9.3)": - dependencies: - "@eslint-community/eslint-utils": 4.9.1(eslint@10.6.0) - "@typescript-eslint/scope-manager": 8.62.0 - "@typescript-eslint/types": 8.62.0 - "@typescript-eslint/typescript-estree": 8.62.0(typescript@5.9.3) - eslint: 10.6.0 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - "@typescript-eslint/visitor-keys@8.62.0": - dependencies: - "@typescript-eslint/types": 8.62.0 - eslint-visitor-keys: 5.0.1 - - "@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@22.20.0)(lightningcss@1.32.0))": - dependencies: - "@babel/core": 7.29.7 - "@babel/plugin-transform-react-jsx-self": 7.29.7(@babel/core@7.29.7) - "@babel/plugin-transform-react-jsx-source": 7.29.7(@babel/core@7.29.7) - "@rolldown/pluginutils": 1.0.0-beta.27 - "@types/babel__core": 7.20.5 - react-refresh: 0.17.0 - vite: 6.4.3(@types/node@22.20.0)(lightningcss@1.32.0) - transitivePeerDependencies: - - supports-color - - "@vitest/coverage-v8@4.1.9(vitest@4.1.9)": - dependencies: - "@bcoe/v8-coverage": 1.0.2 - "@vitest/utils": 4.1.9 - ast-v8-to-istanbul: 1.0.4 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.3 - obug: 2.1.3 - std-env: 4.1.0 - tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@22.20.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@6.4.3(@types/node@22.20.0)(lightningcss@1.32.0)) - - "@vitest/expect@4.1.9": - dependencies: - "@standard-schema/spec": 1.1.0 - "@types/chai": 5.2.3 - "@vitest/spy": 4.1.9 - "@vitest/utils": 4.1.9 - chai: 6.2.2 - tinyrainbow: 3.1.0 - - "@vitest/mocker@4.1.9(vite@6.4.3(@types/node@22.20.0)(lightningcss@1.32.0))": - dependencies: - "@vitest/spy": 4.1.9 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 6.4.3(@types/node@22.20.0)(lightningcss@1.32.0) - - "@vitest/pretty-format@4.1.9": - dependencies: - tinyrainbow: 3.1.0 - - "@vitest/runner@4.1.9": - dependencies: - "@vitest/utils": 4.1.9 - pathe: 2.0.3 - - "@vitest/snapshot@4.1.9": - dependencies: - "@vitest/pretty-format": 4.1.9 - "@vitest/utils": 4.1.9 - magic-string: 0.30.21 - pathe: 2.0.3 - - "@vitest/spy@4.1.9": {} - - "@vitest/utils@4.1.9": - dependencies: - "@vitest/pretty-format": 4.1.9 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 - - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - - acorn-jsx@5.3.2(acorn@8.17.0): - dependencies: - acorn: 8.17.0 - - acorn-walk@8.3.5: - dependencies: - acorn: 8.17.0 - - acorn@8.17.0: {} - - ajv@6.15.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-regex@5.0.1: {} - - ansi-styles@5.2.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - - append-field@1.0.0: {} - - arg@4.1.3: {} - - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-includes@3.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - - array.prototype.findlast@1.2.5: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - es-shim-unscopables: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 - - array.prototype.tosorted@1.1.4: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - assertion-error@2.0.1: {} - - ast-v8-to-istanbul@1.0.4: - dependencies: - "@jridgewell/trace-mapping": 0.3.31 - estree-walker: 3.0.3 - js-tokens: 10.0.0 - - async-function@1.0.0: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - balanced-match@1.0.2: {} - - balanced-match@4.0.4: {} - - baseline-browser-mapping@2.10.40: {} - - bidi-js@1.0.3: - dependencies: - require-from-string: 2.0.2 - - binary-extensions@2.3.0: {} - - body-parser@2.3.0: - dependencies: - bytes: 3.1.2 - content-type: 2.0.0 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.15.3 - raw-body: 3.0.2 - type-is: 2.1.0 - transitivePeerDependencies: - - supports-color - - brace-expansion@1.1.15: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@5.0.6: - dependencies: - balanced-match: 4.0.4 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.28.4: - dependencies: - baseline-browser-mapping: 2.10.40 - caniuse-lite: 1.0.30001799 - electron-to-chromium: 1.5.379 - node-releases: 2.0.50 - update-browserslist-db: 1.2.3(browserslist@4.28.4) - - buffer-from@1.1.2: {} - - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - - bytes@3.1.2: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.9: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - caniuse-lite@1.0.30001799: {} - - chai@6.2.2: {} - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - concat-map@0.0.1: {} - - concat-stream@1.6.2: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 2.3.8 - typedarray: 0.0.6 - - concat-stream@2.0.0: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - typedarray: 0.0.6 - - content-disposition@1.1.0: {} - - content-type@1.0.5: {} - - content-type@2.0.0: {} - - convert-source-map@2.0.0: {} - - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - - core-util-is@1.0.3: {} - - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - - create-require@1.1.1: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - css-tree@3.2.1: - dependencies: - mdn-data: 2.27.1 - source-map-js: 1.2.1 - - css.escape@1.5.1: {} - - csstype@3.2.3: {} - - data-urls@7.0.0: - dependencies: - whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 - transitivePeerDependencies: - - "@noble/hashes" - - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decimal.js@10.6.0: {} - - deep-is@0.1.4: {} - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - depd@2.0.0: {} - - dequal@2.0.3: {} - - detect-libc@2.1.2: - optional: true - - diff@4.0.4: {} - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - dom-accessibility-api@0.5.16: {} - - dom-accessibility-api@0.6.3: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - dynamic-dedupe@0.3.0: - dependencies: - xtend: 4.0.2 - - ee-first@1.1.1: {} - - electron-to-chromium@1.5.379: {} - - encodeurl@2.0.0: {} - - entities@8.0.0: {} - - es-abstract-get@1.0.0: - dependencies: - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - is-callable: 1.2.7 - object-inspect: 1.13.4 - - es-abstract@1.24.2: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.4 - function.prototype.name: 1.2.0 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.4 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.11 - string.prototype.trimend: 1.0.10 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.8 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.22 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-iterator-helpers@1.3.3: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-set-tostringtag: 2.1.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - iterator.prototype: 1.1.5 - math-intrinsics: 1.1.0 - - es-module-lexer@2.1.0: {} - - es-object-atoms@1.1.2: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.4 - - es-to-primitive@1.3.4: - dependencies: - es-abstract-get: 1.0.0 - es-define-property: 1.0.1 - es-errors: 1.3.0 - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - - esbuild@0.25.12: - optionalDependencies: - "@esbuild/aix-ppc64": 0.25.12 - "@esbuild/android-arm": 0.25.12 - "@esbuild/android-arm64": 0.25.12 - "@esbuild/android-x64": 0.25.12 - "@esbuild/darwin-arm64": 0.25.12 - "@esbuild/darwin-x64": 0.25.12 - "@esbuild/freebsd-arm64": 0.25.12 - "@esbuild/freebsd-x64": 0.25.12 - "@esbuild/linux-arm": 0.25.12 - "@esbuild/linux-arm64": 0.25.12 - "@esbuild/linux-ia32": 0.25.12 - "@esbuild/linux-loong64": 0.25.12 - "@esbuild/linux-mips64el": 0.25.12 - "@esbuild/linux-ppc64": 0.25.12 - "@esbuild/linux-riscv64": 0.25.12 - "@esbuild/linux-s390x": 0.25.12 - "@esbuild/linux-x64": 0.25.12 - "@esbuild/netbsd-arm64": 0.25.12 - "@esbuild/netbsd-x64": 0.25.12 - "@esbuild/openbsd-arm64": 0.25.12 - "@esbuild/openbsd-x64": 0.25.12 - "@esbuild/openharmony-arm64": 0.25.12 - "@esbuild/sunos-x64": 0.25.12 - "@esbuild/win32-arm64": 0.25.12 - "@esbuild/win32-ia32": 0.25.12 - "@esbuild/win32-x64": 0.25.12 - - escalade@3.2.0: {} - - escape-html@1.0.3: {} - - escape-string-regexp@4.0.0: {} - - eslint-plugin-react-hooks@7.1.1(eslint@10.6.0): - dependencies: - "@babel/core": 7.29.7 - "@babel/parser": 7.29.7 - eslint: 10.6.0 - hermes-parser: 0.25.1 - zod: 4.4.3 - zod-validation-error: 4.0.2(zod@4.4.3) - transitivePeerDependencies: - - supports-color - - eslint-plugin-react@7.37.5(eslint@10.6.0): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.3.3 - eslint: 10.6.0 - estraverse: 5.3.0 - hasown: 2.0.4 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.5 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.7 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - - eslint-scope@9.1.2: - dependencies: - "@types/esrecurse": 4.3.1 - "@types/estree": 1.0.9 - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@5.0.1: {} - - eslint@10.6.0: - dependencies: - "@eslint-community/eslint-utils": 4.9.1(eslint@10.6.0) - "@eslint-community/regexpp": 4.12.2 - "@eslint/config-array": 0.23.5 - "@eslint/config-helpers": 0.6.0 - "@eslint/core": 1.2.1 - "@eslint/plugin-kit": 0.7.2 - "@humanfs/node": 0.16.8 - "@humanwhocodes/module-importer": 1.0.1 - "@humanwhocodes/retry": 0.4.3 - "@types/estree": 1.0.9 - ajv: 6.15.0 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 9.1.2 - eslint-visitor-keys: 5.0.1 - espree: 11.2.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@11.2.0: - dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) - eslint-visitor-keys: 5.0.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@3.0.3: - dependencies: - "@types/estree": 1.0.9 - - esutils@2.0.3: {} - - etag@1.8.1: {} - - expect-type@1.4.0: {} - - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.3.0 - content-disposition: 1.1.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.3 - range-parser: 1.3.0 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.1.0 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fast-safe-stringify@2.1.1: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - file-type@21.3.4: - dependencies: - "@tokenizer/inflate": 0.4.1 - strtok3: 10.3.5 - token-types: 6.1.2 - uint8array-extras: 1.5.0 - transitivePeerDependencies: - - supports-color - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - finalhandler@2.1.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.4.2 - keyv: 4.5.4 - - flatted@3.4.2: {} - - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - - forwarded@0.2.0: {} - - fresh@2.0.0: {} - - fs.realpath@1.0.0: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - function.prototype.name@1.2.0: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - es-define-property: 1.0.1 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - hasown: 2.0.4 - is-callable: 1.2.7 - is-document.all: 1.0.0 - - functions-have-names@1.2.3: {} - - generator-function@2.0.1: {} - - gensync@1.0.0-beta.2: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.2 - - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - - gopd@1.2.0: {} - - has-bigints@1.1.0: {} - - has-flag@4.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 - - hermes-estree@0.25.1: {} - - hermes-parser@0.25.1: - dependencies: - hermes-estree: 0.25.1 - - html-encoding-sniffer@6.0.0: - dependencies: - "@exodus/bytes": 1.15.1 - transitivePeerDependencies: - - "@noble/hashes" - - html-escaper@2.0.2: {} - - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - - ieee754@1.2.1: {} - - ignore@5.3.2: {} - - ignore@7.0.5: {} - - immediate@3.0.6: {} - - imurmurhash@0.1.4: {} - - indent-string@4.0.0: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.4 - side-channel: 1.1.1 - - ipaddr.js@1.9.1: {} - - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-callable@1.2.7: {} - - is-core-module@2.16.2: - dependencies: - hasown: 2.0.4 - - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-document.all@1.0.0: - dependencies: - call-bound: 1.0.4 - - is-extglob@2.1.1: {} - - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-generator-function@1.1.2: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.1 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-map@2.0.3: {} - - is-negative-zero@2.0.3: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-number@7.0.0: {} - - is-potential-custom-element-name@1.0.1: {} - - is-promise@4.0.0: {} - - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.22 - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - isarray@1.0.0: {} - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - - iterare@1.2.1: {} - - iterator.prototype@1.1.5: - dependencies: - define-data-property: 1.1.4 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - has-symbols: 1.1.0 - set-function-name: 2.0.2 - - js-tokens@10.0.0: {} - - js-tokens@4.0.0: {} - - jsdom@29.1.1: - dependencies: - "@asamuzakjp/css-color": 5.1.11 - "@asamuzakjp/dom-selector": 7.1.1 - "@bramus/specificity": 2.4.2 - "@csstools/css-syntax-patches-for-csstree": 1.1.5(css-tree@3.2.1) - "@exodus/bytes": 1.15.1 - css-tree: 3.2.1 - data-urls: 7.0.0 - decimal.js: 10.6.0 - html-encoding-sniffer: 6.0.0 - is-potential-custom-element-name: 1.0.1 - lru-cache: 11.5.1 - parse5: 8.0.1 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 6.0.1 - undici: 7.28.0 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 8.0.1 - whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - "@noble/hashes" - - jsesc@3.1.0: {} - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@2.2.3: {} - - jsx-ast-utils@3.3.5: - dependencies: - array-includes: 3.1.9 - array.prototype.flat: 1.3.3 - object.assign: 4.1.7 - object.values: 1.2.1 - - jszip@3.10.1: - dependencies: - lie: 3.3.0 - pako: 1.0.11 - readable-stream: 2.3.8 - setimmediate: 1.0.5 - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lie@3.3.0: - dependencies: - immediate: 3.0.6 - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - optional: true - - load-esm@1.0.3: {} - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - - lru-cache@11.5.1: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - lz-string@1.5.0: {} - - magic-string@0.30.21: - dependencies: - "@jridgewell/sourcemap-codec": 1.5.5 - - magicast@0.5.3: - dependencies: - "@babel/parser": 7.29.7 - "@babel/types": 7.29.7 - source-map-js: 1.2.1 - - make-dir@4.0.0: - dependencies: - semver: 7.8.5 - - make-error@1.3.6: {} - - math-intrinsics@1.1.0: {} - - mdn-data@2.27.1: {} - - media-typer@0.3.0: {} - - media-typer@1.1.0: {} - - merge-descriptors@2.0.0: {} - - mime-db@1.52.0: {} - - mime-db@1.54.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - - min-indent@1.0.1: {} - - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.6 - - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.15 - - minimist@1.2.8: {} - - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - - mkdirp@1.0.4: {} - - ms@2.1.3: {} - - multer@1.4.5-lts.2: - dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 1.6.2 - mkdirp: 0.5.6 - object-assign: 4.1.1 - type-is: 1.6.18 - xtend: 4.0.2 - - multer@2.1.1: - dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 2.0.0 - type-is: 1.6.18 - - nanoid@3.3.15: {} - - natural-compare@1.4.0: {} - - negotiator@1.0.0: {} - - node-exports-info@1.6.2: - dependencies: - array.prototype.flatmap: 1.3.3 - es-errors: 1.3.0 - object.entries: 1.1.9 - semver: 6.3.1 - - node-releases@2.0.50: {} - - normalize-path@3.0.0: {} - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.entries@1.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - obug@2.1.3: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - pako@1.0.11: {} - - parse5@8.0.1: - dependencies: - entities: 8.0.0 - - parseurl@1.3.3: {} - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-to-regexp@8.4.2: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@2.3.2: {} - - picomatch@4.0.4: {} - - possible-typed-array-names@1.1.0: {} - - postcss@8.5.15: - dependencies: - nanoid: 3.3.15 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - - process-nextick-args@2.0.1: {} - - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - punycode@2.3.1: {} - - qs@6.15.3: - dependencies: - es-define-property: 1.0.1 - side-channel: 1.1.1 - - range-parser@1.3.0: {} - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - - react-dom@19.2.7(react@19.2.7): - dependencies: - react: 19.2.7 - scheduler: 0.27.0 - - react-is@16.13.1: {} - - react-is@17.0.2: {} - - react-refresh@0.17.0: {} - - react@19.2.7: {} - - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.2 - - redent@3.0.0: - dependencies: - indent-string: 4.0.0 - strip-indent: 3.0.0 - - reflect-metadata@0.2.2: {} - - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - - require-from-string@2.0.2: {} - - resolve@1.22.12: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - resolve@2.0.0-next.7: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - node-exports-info: 1.6.2 - object-keys: 1.1.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - rimraf@2.7.1: - dependencies: - glob: 7.2.3 - - rollup@4.62.2: - dependencies: - "@types/estree": 1.0.9 - optionalDependencies: - "@rollup/rollup-android-arm-eabi": 4.62.2 - "@rollup/rollup-android-arm64": 4.62.2 - "@rollup/rollup-darwin-arm64": 4.62.2 - "@rollup/rollup-darwin-x64": 4.62.2 - "@rollup/rollup-freebsd-arm64": 4.62.2 - "@rollup/rollup-freebsd-x64": 4.62.2 - "@rollup/rollup-linux-arm-gnueabihf": 4.62.2 - "@rollup/rollup-linux-arm-musleabihf": 4.62.2 - "@rollup/rollup-linux-arm64-gnu": 4.62.2 - "@rollup/rollup-linux-arm64-musl": 4.62.2 - "@rollup/rollup-linux-loong64-gnu": 4.62.2 - "@rollup/rollup-linux-loong64-musl": 4.62.2 - "@rollup/rollup-linux-ppc64-gnu": 4.62.2 - "@rollup/rollup-linux-ppc64-musl": 4.62.2 - "@rollup/rollup-linux-riscv64-gnu": 4.62.2 - "@rollup/rollup-linux-riscv64-musl": 4.62.2 - "@rollup/rollup-linux-s390x-gnu": 4.62.2 - "@rollup/rollup-linux-x64-gnu": 4.62.2 - "@rollup/rollup-linux-x64-musl": 4.62.2 - "@rollup/rollup-openbsd-x64": 4.62.2 - "@rollup/rollup-openharmony-arm64": 4.62.2 - "@rollup/rollup-win32-arm64-msvc": 4.62.2 - "@rollup/rollup-win32-ia32-msvc": 4.62.2 - "@rollup/rollup-win32-x64-gnu": 4.62.2 - "@rollup/rollup-win32-x64-msvc": 4.62.2 - fsevents: 2.3.3 - - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.4.2 - transitivePeerDependencies: - - supports-color - - rxjs@7.8.2: - dependencies: - tslib: 2.8.1 - - safe-array-concat@1.1.4: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - - safe-buffer@5.1.2: {} - - safe-buffer@5.2.1: {} - - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - - safer-buffer@2.1.2: {} - - saxes@6.0.0: - dependencies: - xmlchars: 2.2.0 - - scheduler@0.27.0: {} - - semver@6.3.1: {} - - semver@7.8.5: {} - - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.3.0 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - - setimmediate@1.0.5: {} - - setprototypeof@1.2.0: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - siginfo@2.0.0: {} - - source-map-js@1.2.1: {} - - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} - - stackback@0.0.2: {} - - statuses@2.0.2: {} - - std-env@4.1.0: {} - - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - - streamsearch@1.1.0: {} - - string.prototype.matchall@4.0.12: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.4 - set-function-name: 2.0.2 - side-channel: 1.1.1 - - string.prototype.repeat@1.0.0: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.24.2 - - string.prototype.trim@1.2.11: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - has-property-descriptors: 1.0.2 - safe-regex-test: 1.1.0 - - string.prototype.trimend@1.0.10: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - strip-bom@3.0.0: {} - - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - - strip-json-comments@2.0.1: {} - - strtok3@10.3.5: - dependencies: - "@tokenizer/token": 0.3.0 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - symbol-tree@3.2.4: {} - - tinybench@2.9.0: {} - - tinyexec@1.2.4: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - tinyrainbow@3.1.0: {} - - tldts-core@7.4.4: {} - - tldts@7.4.4: - dependencies: - tldts-core: 7.4.4 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - toidentifier@1.0.1: {} - - token-types@6.1.2: - dependencies: - "@borewit/text-codec": 0.2.2 - "@tokenizer/token": 0.3.0 - ieee754: 1.2.1 - - tough-cookie@6.0.1: - dependencies: - tldts: 7.4.4 - - tr46@6.0.0: - dependencies: - punycode: 2.3.1 - - tree-kill@1.2.2: {} - - ts-api-utils@2.5.0(typescript@5.8.3): - dependencies: - typescript: 5.8.3 - - ts-api-utils@2.5.0(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - - ts-node-dev@2.0.0(@types/node@22.20.0)(typescript@5.9.3): - dependencies: - chokidar: 3.6.0 - dynamic-dedupe: 0.3.0 - minimist: 1.2.8 - mkdirp: 1.0.4 - resolve: 1.22.12 - rimraf: 2.7.1 - source-map-support: 0.5.21 - tree-kill: 1.2.2 - ts-node: 10.9.2(@types/node@22.20.0)(typescript@5.9.3) - tsconfig: 7.0.0 - typescript: 5.9.3 - transitivePeerDependencies: - - "@swc/core" - - "@swc/wasm" - - "@types/node" - - ts-node@10.9.2(@types/node@22.20.0)(typescript@5.9.3): - dependencies: - "@cspotcode/source-map-support": 0.8.1 - "@tsconfig/node10": 1.0.12 - "@tsconfig/node12": 1.0.11 - "@tsconfig/node14": 1.0.3 - "@tsconfig/node16": 1.0.4 - "@types/node": 22.20.0 - acorn: 8.17.0 - acorn-walk: 8.3.5 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.4 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - - tsconfig@7.0.0: - dependencies: - "@types/strip-bom": 3.0.0 - "@types/strip-json-comments": 0.0.30 - strip-bom: 3.0.0 - strip-json-comments: 2.0.1 - - tslib@2.8.1: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 - mime-types: 3.0.2 - - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.8: - dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - - typedarray@0.0.6: {} - - typescript-eslint@8.62.0(eslint@10.6.0)(typescript@5.8.3): - dependencies: - "@typescript-eslint/eslint-plugin": 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.6.0)(typescript@5.8.3))(eslint@10.6.0)(typescript@5.8.3) - "@typescript-eslint/parser": 8.62.0(eslint@10.6.0)(typescript@5.8.3) - "@typescript-eslint/typescript-estree": 8.62.0(typescript@5.8.3) - "@typescript-eslint/utils": 8.62.0(eslint@10.6.0)(typescript@5.8.3) - eslint: 10.6.0 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - typescript-eslint@8.62.0(eslint@10.6.0)(typescript@5.9.3): - dependencies: - "@typescript-eslint/eslint-plugin": 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0)(typescript@5.9.3) - "@typescript-eslint/parser": 8.62.0(eslint@10.6.0)(typescript@5.9.3) - "@typescript-eslint/typescript-estree": 8.62.0(typescript@5.9.3) - "@typescript-eslint/utils": 8.62.0(eslint@10.6.0)(typescript@5.9.3) - eslint: 10.6.0 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - typescript@5.8.3: {} - - typescript@5.9.3: {} - - uid@2.0.2: - dependencies: - "@lukeed/csprng": 1.1.0 - - uint8array-extras@1.5.0: {} - - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - - undici-types@6.21.0: {} - - undici@7.28.0: {} - - unpipe@1.0.0: {} - - update-browserslist-db@1.2.3(browserslist@4.28.4): - dependencies: - browserslist: 4.28.4 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - util-deprecate@1.0.2: {} - - v8-compile-cache-lib@3.0.1: {} - - vary@1.1.2: {} - - vite@6.4.3(@types/node@22.20.0)(lightningcss@1.32.0): - dependencies: - esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.15 - rollup: 4.62.2 - tinyglobby: 0.2.17 - optionalDependencies: - "@types/node": 22.20.0 - fsevents: 2.3.3 - lightningcss: 1.32.0 - - vitest@4.1.9(@types/node@22.20.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@6.4.3(@types/node@22.20.0)(lightningcss@1.32.0)): - dependencies: - "@vitest/expect": 4.1.9 - "@vitest/mocker": 4.1.9(vite@6.4.3(@types/node@22.20.0)(lightningcss@1.32.0)) - "@vitest/pretty-format": 4.1.9 - "@vitest/runner": 4.1.9 - "@vitest/snapshot": 4.1.9 - "@vitest/spy": 4.1.9 - "@vitest/utils": 4.1.9 - es-module-lexer: 2.1.0 - expect-type: 1.4.0 - magic-string: 0.30.21 - obug: 2.1.3 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.1.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 6.4.3(@types/node@22.20.0)(lightningcss@1.32.0) - why-is-node-running: 2.3.0 - optionalDependencies: - "@types/node": 22.20.0 - "@vitest/coverage-v8": 4.1.9(vitest@4.1.9) - jsdom: 29.1.1 - transitivePeerDependencies: - - msw - - w3c-xmlserializer@5.0.0: - dependencies: - xml-name-validator: 5.0.0 - - webidl-conversions@8.0.1: {} - - whatwg-mimetype@5.0.0: {} - - whatwg-url@16.0.1: - dependencies: - "@exodus/bytes": 1.15.1 - tr46: 6.0.0 - webidl-conversions: 8.0.1 - transitivePeerDependencies: - - "@noble/hashes" - - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.2.0 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.22 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.22: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - word-wrap@1.2.5: {} - - wrappy@1.0.2: {} - - xml-name-validator@5.0.0: {} - - xmlchars@2.2.0: {} - - xtend@4.0.2: {} - - yallist@3.1.1: {} - - yn@3.1.1: {} - - yocto-queue@0.1.0: {} - - zod-validation-error@4.0.2(zod@4.4.3): - dependencies: - zod: 4.4.3 - - zod@4.4.3: {} diff --git a/bucket_catch/pnpm-workspace.yaml b/bucket_catch/pnpm-workspace.yaml deleted file mode 100644 index dee51e92..00000000 --- a/bucket_catch/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - - "packages/*" diff --git a/docs/restructure-live-state.md b/docs/restructure-live-state.md index b108c48f..b3bafc48 100644 --- a/docs/restructure-live-state.md +++ b/docs/restructure-live-state.md @@ -60,11 +60,11 @@ file must be repointed in the same step. Re-run the script with `meta/scripts/check_directory_depth.sh --all` at baseline: -| Bucket | Files | -|---|---| -| Total violations | 556 | -| Disappear with the archives/extractions | 349 | -| **Remain — the step D worklist** | **207** | +| Bucket | Files | +| --------------------------------------- | ------- | +| Total violations | 556 | +| Disappear with the archives/extractions | 349 | +| **Remain — the step D worklist** | **207** | Step D worklist by directory: diff --git a/docs/superpowers/contracts/archive-bucket-catch-poker-stakes-2026-08-23.json b/docs/superpowers/contracts/archive-bucket-catch-poker-stakes-2026-08-23.json new file mode 100644 index 00000000..2cd7a70e --- /dev/null +++ b/docs/superpowers/contracts/archive-bucket-catch-poker-stakes-2026-08-23.json @@ -0,0 +1,20 @@ +{ + "title": "Archive bucket_catch and poker-stakes to testsAndMisc-archive", + "objective": "Remove two self-contained apps from testsAndMisc, preserving their full git history in kuhyx/testsAndMisc-archive, as the zero-risk opening step of the repo restructure. Neither unit is referenced by any other code, systemd unit, or symlink, which is why they go first.", + "acceptance_criteria": [ + "Both directories are present in kuhyx/testsAndMisc-archive at their original repo-relative paths, confirmed through the GitHub API rather than a local clone.", + "The archived commit count for each unit equals the number of monorepo commits touching it: bucket_catch 10, poker-stakes 2.", + "diff -r between the archived copy and the pre-removal working tree reports no content differences for either unit.", + "Tracked file counts match exactly: bucket_catch 64, poker-stakes 65.", + ".github/workflows/poker-stakes-ci.yml is removed in the same commit as the app it builds, so no workflow is left pointing at a missing path.", + "No file under .github/workflows/ still references either unit.", + "meta/scripts/report_live_state.sh output is unchanged, proving neither unit had live system state pointing at it.", + "pre-commit passes without --no-verify." + ], + "out_of_scope": [ + "billsplit, which is archived separately because it is coupled to python_pkg/billsplit_coverage and an app_icons registry entry that must be deleted in the same commit.", + "Rewriting historical planning notes that mention these names; they are dated records, not live configuration.", + "Wiring the directory-depth gate, which stays a dry run until the tree conforms." + ], + "verifier": "gh api repos/kuhyx/testsAndMisc-archive/contents lists both units; git ls-files counts match source; diff -r is clean; ./meta/scripts/check_directory_depth.sh --all drops from 556 to 451; ./meta/scripts/report_live_state.sh still reports 27 reference lines; pre-commit run over the staged set passes." +} diff --git a/docs/superpowers/evidence/archive-bucket-catch-poker-stakes-2026-08-23.json b/docs/superpowers/evidence/archive-bucket-catch-poker-stakes-2026-08-23.json new file mode 100644 index 00000000..dca7aee9 --- /dev/null +++ b/docs/superpowers/evidence/archive-bucket-catch-poker-stakes-2026-08-23.json @@ -0,0 +1,55 @@ +{ + "intent": "Archive bucket_catch and poker-stakes out of testsAndMisc into testsAndMisc-archive, with full per-directory git history preserved, as the first (zero-risk) step of the repo restructure.", + "scope": [ + "bucket_catch/ (64 files) - removed", + "poker-stakes/ (65 files) - removed", + ".github/workflows/poker-stakes-ci.yml - removed with its app", + "AGENTS.md - repo layout table updated", + "Non-goal: billsplit is archived separately, since it is coupled to python_pkg/billsplit_coverage and the app_icons registry" + ], + "changes": [ + "Extracted each directory with git filter-repo --path, preserving every commit that touched it (bucket_catch 10, poker-stakes 2 - both matching git log counts in the monorepo).", + "Merged both into kuhyx/testsAndMisc-archive with --allow-unrelated-histories, keeping original repo-relative paths as that archive already does for C/, CPP/, horatio/ and the rest.", + "Removed both trees plus poker-stakes-ci.yml from the monorepo (130 files, 21869 deletions) and dropped their rows from the AGENTS.md layout table." + ], + "verification": [ + { + "command": "git ls-files | wc -l, in archive vs source", + "result": "pass", + "evidence": "bucket_catch 64 = 64, poker-stakes 65 = 65." + }, + { + "command": "diff -r --brief / /", + "result": "pass", + "evidence": "No content differences for either unit (excluding untracked node_modules/build dirs that were never tracked)." + }, + { + "command": "gh api repos/kuhyx/testsAndMisc-archive/contents --jq '.[].name'", + "result": "pass", + "evidence": "bucket_catch and poker-stakes both listed alongside the pre-existing C, CPP, TS, horatio, linux_configuration, pomodoro_app, python_pkg, robotgo_demo, sonic_pi." + }, + { + "command": "./meta/scripts/check_directory_depth.sh --all", + "result": "pass (improvement)", + "evidence": "Depth violations dropped 556 -> 451, consistent with the 105 deep files these two units contributed." + }, + { + "command": "grep -rln 'bucket_catch|poker-stakes' .github/workflows/", + "result": "pass", + "evidence": "No matches - no workflow still references a removed unit." + }, + { + "command": "./meta/scripts/report_live_state.sh", + "result": "pass", + "evidence": "27 live reference lines, unchanged from baseline: neither unit had any systemd unit, /etc config or symlink pointing at it." + } + ], + "risks": [ + "The archive push is the only copy of these trees outside this repo's history; verified present via the GitHub API before deleting locally.", + "Historical planning notes (NEXT_SESSION_PROMPT.md, refactor_claude_todo_resume.md, docs/app-split-recipes.md) still mention both names. Left as-is: they are dated records of past work, not live configuration." + ], + "rollback": [ + "git revert the removal commit to restore both trees and the workflow.", + "The archive repo keeps its copy either way; re-verify with pre-commit run --all-files after any revert." + ] +} diff --git a/poker-stakes/.gitignore b/poker-stakes/.gitignore deleted file mode 100644 index 0e75fe55..00000000 --- a/poker-stakes/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -dist -coverage diff --git a/poker-stakes/eslint.config.js b/poker-stakes/eslint.config.js deleted file mode 100644 index aa051ed0..00000000 --- a/poker-stakes/eslint.config.js +++ /dev/null @@ -1,47 +0,0 @@ -import js from '@eslint/js' -import react from '@eslint-react/eslint-plugin' -import vitest from '@vitest/eslint-plugin' -import { defineConfig } from 'eslint/config' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import globals from 'globals' -import tseslint from 'typescript-eslint' - -export default defineConfig( - { ignores: ['dist', 'coverage', 'node_modules'] }, - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.strictTypeChecked, - tseslint.configs.stylisticTypeChecked, - react.configs['strict-type-checked'], - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - globals: globals.browser, - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - }, - rules: { - '@typescript-eslint/explicit-function-return-type': 'error', - '@typescript-eslint/consistent-type-imports': 'error', - '@typescript-eslint/no-shadow': 'error', - 'no-console': 'error', - 'eqeqeq': ['error', 'always'], - 'prefer-const': ['error', { destructuring: 'all' }], - }, - }, - { - files: ['src/**/*.test.{ts,tsx}', 'src/test/**/*.ts'], - extends: [vitest.configs.recommended], - languageOptions: { globals: { ...globals.browser, ...globals.node } }, - }, - { - files: ['eslint.config.js'], - extends: [tseslint.configs.disableTypeChecked], - }, -) diff --git a/poker-stakes/index.html b/poker-stakes/index.html deleted file mode 100644 index 0d0b28a3..00000000 --- a/poker-stakes/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Poker Stakes — lose the hand, pay the price - - -
- - - diff --git a/poker-stakes/package-lock.json b/poker-stakes/package-lock.json deleted file mode 100644 index ac9494e5..00000000 --- a/poker-stakes/package-lock.json +++ /dev/null @@ -1,4852 +0,0 @@ -{ - "name": "poker-stakes", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "poker-stakes", - "version": "0.1.0", - "dependencies": { - "react": "19.2.8", - "react-dom": "19.2.8" - }, - "devDependencies": { - "@eslint-react/eslint-plugin": "5.18.1", - "@eslint/js": "10.0.1", - "@testing-library/jest-dom": "7.0.0", - "@testing-library/react": "16.3.2", - "@testing-library/user-event": "14.6.1", - "@types/react": "19.2.18", - "@types/react-dom": "19.2.4", - "@vitejs/plugin-react": "6.0.5", - "@vitest/coverage-v8": "4.1.10", - "@vitest/eslint-plugin": "1.6.24", - "eslint": "10.8.0", - "eslint-plugin-react-hooks": "7.1.1", - "eslint-plugin-react-refresh": "0.5.3", - "globals": "17.8.0", - "jsdom": "30.0.1", - "typescript": "6.0.3", - "typescript-eslint": "8.65.0", - "vite": "8.2.0", - "vitest": "4.1.10" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", - "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@asamuzakjp/css-color": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", - "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^3.3.0", - "@csstools/css-color-parser": "^4.1.10", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.5.2" - }, - "engines": { - "node": "^22.13.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", - "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2" - }, - "engines": { - "node": "^22.13.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", - "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.8", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", - "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", - "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", - "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.1.0", - "@csstools/css-calc": "^3.3.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", - "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint-react/ast": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-5.18.1.tgz", - "integrity": "sha512-6NTpdv+Z6eHT2lc8DkfJZUG6tUvZQkRBlvBpYuoeen+HIvzz59aOt1YEL4dd6FQlXD4DPbCBGXCjcV879hT8Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "^8.65.0", - "@typescript-eslint/typescript-estree": "^8.65.0", - "@typescript-eslint/utils": "^8.65.0", - "string-ts": "^2.3.1" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/@eslint-react/core": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-5.18.1.tgz", - "integrity": "sha512-n2avrghmy22iMFJWGXKacyMMvtaFrPqq1Yydd7L1DKmUe/hubjD4Uc1ZKV/F2xRh8dQ0Gp2b5TVv7H/Nq5DxUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-react/ast": "5.18.1", - "@eslint-react/eslint": "5.18.1", - "@eslint-react/shared": "5.18.1", - "@eslint-react/var": "5.18.1", - "@typescript-eslint/scope-manager": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", - "@typescript-eslint/utils": "^8.65.0", - "ts-pattern": "^5.9.0" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/@eslint-react/eslint": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/@eslint-react/eslint/-/eslint-5.18.1.tgz", - "integrity": "sha512-T7AOWRwc1+gBEQUFoGSsBVHmWpsanGWVU52NTmWxij9ZGIlqbG+JC6vpCBa6IzGPKbulCu4B/np2JIydZQM1pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "^8.65.0" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/@eslint-react/eslint-plugin": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/@eslint-react/eslint-plugin/-/eslint-plugin-5.18.1.tgz", - "integrity": "sha512-BlgYu//MKKnLHhQhh6E6ChSbEYj/a/cuRnyc+akOUtn8qCEqf2Le+/Rcxjfso3ujDjrq5yZt8E28aNOubtE6WA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-react/shared": "5.18.1", - "eslint-plugin-react-dom": "5.18.1", - "eslint-plugin-react-jsx": "5.18.1", - "eslint-plugin-react-naming-convention": "5.18.1", - "eslint-plugin-react-rsc": "5.18.1", - "eslint-plugin-react-web-api": "5.18.1", - "eslint-plugin-react-x": "5.18.1" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/@eslint-react/jsx": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/@eslint-react/jsx/-/jsx-5.18.1.tgz", - "integrity": "sha512-sT0JQ0yw/prFwtEu0cplzS0fZBpNHL1s7awHrD5xKABpqsVS2wofB52p2TSX2nmuNj011hSUW+xlu58GXYZ/fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-react/ast": "5.18.1", - "@eslint-react/eslint": "5.18.1", - "@eslint-react/shared": "5.18.1", - "@eslint-react/var": "5.18.1", - "@typescript-eslint/types": "^8.65.0", - "@typescript-eslint/utils": "^8.65.0", - "ts-pattern": "^5.9.0" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/@eslint-react/shared": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-5.18.1.tgz", - "integrity": "sha512-sLtaIw5PGVriltlBfDD/wUvF4+K3EDGuFhoUF2HKSIdhf28glyrcf6s2d0HnjFXUjXKcD325MNWL7Cectvs2GA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-react/eslint": "5.18.1", - "@typescript-eslint/utils": "^8.65.0", - "ts-pattern": "^5.9.0", - "zod": "^3.25.0 || ^4.0.0" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/@eslint-react/var": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-5.18.1.tgz", - "integrity": "sha512-w1OP5/EJ4ocGWSw+nSpZnfehlnAvfVxOrlgx7k+xTHACyoFQZZXrehOYC7M/0ArQA68sOXo0C1ymEfTJE30StQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-react/ast": "5.18.1", - "@eslint-react/eslint": "5.18.1", - "@typescript-eslint/scope-manager": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", - "@typescript-eslint/utils": "^8.65.0", - "ts-pattern": "^5.9.0" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", - "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.144.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", - "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", - "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", - "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", - "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", - "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", - "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", - "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", - "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", - "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", - "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", - "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", - "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", - "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", - "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", - "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", - "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": ">=22", - "npm": ">=6", - "yarn": ">=1" - }, - "peerDependencies": { - "@testing-library/dom": ">=10 <11" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@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/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/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", - "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", - "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/type-utils": "8.65.0", - "@typescript-eslint/utils": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.65.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", - "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", - "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", - "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", - "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/utils": "8.65.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", - "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", - "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.65.0", - "@typescript-eslint/tsconfig-utils": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", - "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", - "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.65.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", - "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", - "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", - "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", - "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", - "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", - "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.65.0", - "@typescript-eslint/tsconfig-utils": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", - "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.65.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", - "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.67.0", - "@typescript-eslint/types": "^8.67.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", - "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.67.0", - "@typescript-eslint/visitor-keys": "8.67.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", - "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", - "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.67.0", - "@typescript-eslint/typescript-estree": "8.67.0", - "@typescript-eslint/utils": "8.67.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", - "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", - "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.67.0", - "@typescript-eslint/tsconfig-utils": "8.67.0", - "@typescript-eslint/types": "8.67.0", - "@typescript-eslint/visitor-keys": "8.67.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", - "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.67.0", - "@typescript-eslint/types": "8.67.0", - "@typescript-eslint/typescript-estree": "8.67.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.67.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", - "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.67.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", - "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", - "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.10", - "ast-v8-to-istanbul": "^1.0.0", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.2", - "obug": "^2.1.1", - "std-env": "^4.0.0-rc.1", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.1.10", - "vitest": "4.1.10" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/eslint-plugin": { - "version": "1.6.24", - "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.6.24.tgz", - "integrity": "sha512-U5isuChJ58H1M6oRGaIotYbH2gM1rhzKUjtiarqpLNG/3+fzXIXs5RyTLrYQJmQPnXbvIMPFvI9z8ufpFZc8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "^8.58.0", - "@typescript-eslint/utils": "^8.58.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "*", - "eslint": ">=8.57.0", - "typescript": ">=5.0.0", - "vitest": "*" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "typescript": { - "optional": true - }, - "vitest": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "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/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/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", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "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-v8-to-istanbul": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", - "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", - "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.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/birecord": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/birecord/-/birecord-0.1.2.tgz", - "integrity": "sha512-5PAPTTmMpMEb+GuMb5DebfBkipRGyIW9+gtwEBSoDA9xkhHILm04+hZQ702pMksu3d8YAuGkmgTzQWcKqTPScA==", - "dev": true, - "license": "(MIT OR Apache-2.0)" - }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.11.12", - "caniuse-lite": "^1.0.30001809", - "electron-to-chromium": "^1.5.402", - "node-releases": "^2.0.53", - "update-browserslist-db": "^1.3.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001809", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", - "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/compare-versions": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", - "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/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", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "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", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "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/electron-to-chromium": { - "version": "1.5.405", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz", - "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==", - "dev": true, - "license": "ISC" - }, - "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", - "dev": true, - "license": "MIT", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.7.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.2", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-dom": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-dom/-/eslint-plugin-react-dom-5.18.1.tgz", - "integrity": "sha512-0PakQ8iPZ2yq4zi99T28l+NlefxrE/92CpIQ1SbbFT2fqNIjRz1L+ewWIzAsMdyUUjQzcCj8domEJ36y20K8Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-react/ast": "5.18.1", - "@eslint-react/eslint": "5.18.1", - "@eslint-react/jsx": "5.18.1", - "@eslint-react/shared": "5.18.1", - "@typescript-eslint/types": "^8.65.0", - "@typescript-eslint/utils": "^8.65.0", - "compare-versions": "^6.1.1" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-react-jsx": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-jsx/-/eslint-plugin-react-jsx-5.18.1.tgz", - "integrity": "sha512-bT8/pcYwp1Anyjnvjqz1jRJBGdo4ydXlR6hciyQUiHzOxAWpIOP61ggAVgbA9JU3qOr4/hldMpGZHdJP5pqfgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-react/ast": "5.18.1", - "@eslint-react/core": "5.18.1", - "@eslint-react/eslint": "5.18.1", - "@eslint-react/jsx": "5.18.1", - "@eslint-react/shared": "5.18.1", - "@typescript-eslint/types": "^8.65.0", - "@typescript-eslint/utils": "^8.65.0" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/eslint-plugin-react-naming-convention": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-naming-convention/-/eslint-plugin-react-naming-convention-5.18.1.tgz", - "integrity": "sha512-fN6U+Q5QOZ7ZJlTn7XsKB0NvRx0qDp+7tV2Zht3nhtKQ2tl4cxFz8QV78l++FqMtBtCNfxTiInuo5u5Xt8Ymjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-react/ast": "5.18.1", - "@eslint-react/core": "5.18.1", - "@eslint-react/eslint": "5.18.1", - "@eslint-react/shared": "5.18.1", - "@eslint-react/var": "5.18.1", - "@typescript-eslint/types": "^8.65.0", - "@typescript-eslint/utils": "^8.65.0", - "ts-pattern": "^5.9.0" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", - "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": "^9 || ^10" - } - }, - "node_modules/eslint-plugin-react-rsc": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-rsc/-/eslint-plugin-react-rsc-5.18.1.tgz", - "integrity": "sha512-s792p16e8YNhGA3vgIseDxdrwTuNb+gHn0Gau5PsTZ42d8Zuvi3ZJWLfkAZfvMfblaDev6tSAv2fZzWB0brEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-react/ast": "5.18.1", - "@eslint-react/core": "5.18.1", - "@eslint-react/eslint": "5.18.1", - "@eslint-react/shared": "5.18.1", - "@eslint-react/var": "5.18.1", - "@typescript-eslint/types": "^8.65.0", - "@typescript-eslint/utils": "^8.65.0" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/eslint-plugin-react-web-api": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-web-api/-/eslint-plugin-react-web-api-5.18.1.tgz", - "integrity": "sha512-Az8kJEM2Bk8PhJR3UBLWBVZkir/2l+qX545PTi8Kx3JdDlK6IlU1vz69/B5tJ/YXo6cJKP3bKTqaAzcT5ikl8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-react/ast": "5.18.1", - "@eslint-react/core": "5.18.1", - "@eslint-react/eslint": "5.18.1", - "@eslint-react/shared": "5.18.1", - "@eslint-react/var": "5.18.1", - "@typescript-eslint/types": "^8.65.0", - "@typescript-eslint/utils": "^8.65.0", - "birecord": "^0.1.2", - "ts-pattern": "^5.9.0" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/eslint-plugin-react-x": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-5.18.1.tgz", - "integrity": "sha512-i0a17vUMoMhiHjcXz4IYfO42Br6xQkbu+PqVftUHM49i1bbYD+22GFMgNmKkLCMNKXZSQhocKvIKUwhMIbFTQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-react/ast": "5.18.1", - "@eslint-react/core": "5.18.1", - "@eslint-react/eslint": "5.18.1", - "@eslint-react/jsx": "5.18.1", - "@eslint-react/shared": "5.18.1", - "@eslint-react/var": "5.18.1", - "@typescript-eslint/scope-manager": "^8.65.0", - "@typescript-eslint/type-utils": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", - "@typescript-eslint/typescript-estree": "^8.65.0", - "@typescript-eslint/utils": "^8.65.0", - "compare-versions": "^6.1.1", - "string-ts": "^2.3.1", - "ts-api-utils": "^2.5.0", - "ts-pattern": "^5.9.0" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": "*" - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "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", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "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/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz", - "integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "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/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "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/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsdom": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", - "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^6.0.5", - "@asamuzakjp/dom-selector": "^8.3.0", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.7", - "@exodus/bytes": "^1.15.1", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.2", - "undici": "^8.9.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^17.1.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" - }, - "peerDependencies": { - "canvas": "^3.2.3" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/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.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", - "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/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": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.53", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", - "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/obug": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", - "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/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", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.17", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "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/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", - "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.8" - } - }, - "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/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/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rolldown": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", - "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.144.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.2.4", - "@rolldown/binding-darwin-arm64": "1.2.4", - "@rolldown/binding-darwin-x64": "1.2.4", - "@rolldown/binding-freebsd-x64": "1.2.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", - "@rolldown/binding-linux-arm64-gnu": "1.2.4", - "@rolldown/binding-linux-arm64-musl": "1.2.4", - "@rolldown/binding-linux-ppc64-gnu": "1.2.4", - "@rolldown/binding-linux-s390x-gnu": "1.2.4", - "@rolldown/binding-linux-x64-gnu": "1.2.4", - "@rolldown/binding-linux-x64-musl": "1.2.4", - "@rolldown/binding-openharmony-arm64": "1.2.4", - "@rolldown/binding-win32-arm64-msvc": "1.2.4", - "@rolldown/binding-win32-x64-msvc": "1.2.4" - } - }, - "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.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/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/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/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": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-ts": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/string-ts/-/string-ts-2.3.1.tgz", - "integrity": "sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==", - "dev": true, - "license": "MIT" - }, - "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/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/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/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", - "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", - "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "7.4.10", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", - "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.4.10" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.4.10", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", - "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tough-cookie": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", - "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-pattern": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.9.0.tgz", - "integrity": "sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", - "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.65.0", - "@typescript-eslint/parser": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/utils": "8.65.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/project-service": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", - "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", - "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", - "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", - "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", - "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.65.0", - "@typescript-eslint/tsconfig-utils": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", - "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", - "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.65.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/undici": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", - "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", - "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", - "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.23", - "rolldown": "~1.2.0", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/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": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", - "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.15.1", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^22.14.0 || >=24.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/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", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - } - } -} diff --git a/poker-stakes/package.json b/poker-stakes/package.json deleted file mode 100644 index 851b2e23..00000000 --- a/poker-stakes/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "poker-stakes", - "version": "0.1.0", - "scripts": { - "dev": "vite", - "build": "tsc --noEmit && vite build", - "preview": "vite preview", - "typecheck": "tsc --noEmit", - "lint": "eslint . --max-warnings 0", - "test": "vitest run", - "coverage": "vitest run --coverage", - "check": "npm run typecheck && npm run lint && npm run coverage" - }, - "dependencies": { - "react": "19.2.8", - "react-dom": "19.2.8" - }, - "devDependencies": { - "@eslint-react/eslint-plugin": "5.18.1", - "@eslint/js": "10.0.1", - "@testing-library/jest-dom": "7.0.0", - "@testing-library/react": "16.3.2", - "@testing-library/user-event": "14.6.1", - "@types/react": "19.2.18", - "@types/react-dom": "19.2.4", - "@vitejs/plugin-react": "6.0.5", - "@vitest/coverage-v8": "4.1.10", - "@vitest/eslint-plugin": "1.6.24", - "eslint": "10.8.0", - "eslint-plugin-react-hooks": "7.1.1", - "eslint-plugin-react-refresh": "0.5.3", - "globals": "17.8.0", - "jsdom": "30.0.1", - "typescript": "6.0.3", - "typescript-eslint": "8.65.0", - "vite": "8.2.0", - "vitest": "4.1.10" - }, - "private": true, - "type": "module" -} diff --git a/poker-stakes/src/App.test.tsx b/poker-stakes/src/App.test.tsx deleted file mode 100644 index d75bccc3..00000000 --- a/poker-stakes/src/App.test.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { render, screen, within } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { beforeEach, describe, expect, it } from 'vitest' -import { DEBTS_STORAGE_KEY } from './stakes/storage' -import type { SettledDebt } from './stakes/types' -import { App } from './App' - -/** In-memory Storage so each test gets an isolated ledger, never the real localStorage. */ -const memoryStorage = (seed: Record = {}): Storage => { - const map = new Map(Object.entries(seed)) - return { - get length(): number { - return map.size - }, - clear: (): void => { - map.clear() - }, - getItem: (key: string): string | null => map.get(key) ?? null, - key: (index: number): string | null => [...map.keys()][index] ?? null, - removeItem: (key: string): void => { - map.delete(key) - }, - setItem: (key: string, value: string): void => { - map.set(key, value) - }, - } -} - -const debt = (overrides: Partial = {}): SettledDebt => ({ - id: 'd1', - taskTypeId: 'pushups', - owedUnits: 100, - reason: 'cashOut', - settledAt: 1, - paidUnits: 0, - ...overrides, -}) - -const storageWithDebts = (debts: SettledDebt[]): Storage => - memoryStorage({ [DEBTS_STORAGE_KEY]: JSON.stringify(debts) }) - -/** BuyInScreen's pre-filled chip count — the value every test below sits down with. */ -const DEFAULT_BUY_IN = 100 - -/** The betting controls, or null between hands / before any session exists. */ -const bettingControls = (): HTMLElement | null => document.querySelector('.controls') - -/** The first legal action currently offered, or null if no decision is pending. */ -const firstLegalAction = (): HTMLElement | null => { - const controls = bettingControls() - if (controls === null) { - return null - } - return within(controls).getAllByRole('button')[0] ?? null -} - -/** Plays hands until the session ends, always taking the first legal action offered. */ -const playUntilSettled = async (user: ReturnType, maxActions = 400): Promise => { - for (let i = 0; i < maxActions; i += 1) { - const deal = screen.queryByText('Deal next hand') - if (deal !== null) { - await user.click(deal) - continue - } - const action = firstLegalAction() - if (action === null) { - return - } - await user.click(action) - } -} - -describe('App', () => { - beforeEach(() => { - localStorage.clear() - }) - - it('renders the title and the buy-in screen before any session starts', () => { - render() - expect(screen.getByText('Poker Stakes')).toBeInTheDocument() - expect(screen.getByText('Sit down')).toBeInTheDocument() - expect(screen.getByText('No debts settled yet.')).toBeInTheDocument() - }) - - it('defaults to the real localStorage and a clock-derived seed when no props are given', () => { - render() - expect(screen.getByText('Poker Stakes')).toBeInTheDocument() - expect(screen.getByText('Sit down')).toBeInTheDocument() - }) - - it('buys in and deals the opening hand straight away', async () => { - const user = userEvent.setup() - render() - await user.click(screen.getByText('Sit down')) - expect(screen.queryByText('Sit down')).not.toBeInTheDocument() - // Sitting down deals the opening hand immediately, so the player faces a decision at once. - expect(bettingControls()).not.toBeNull() - }) - - it('records a debt in the history panel once the session settles', async () => { - const user = userEvent.setup() - render() - await user.click(screen.getByText('Sit down')) - await playUntilSettled(user) - // Settling returns to the buy-in screen; the debt is now in the durable ledger. It appears - // twice — once in the buy-in gate, once in the history panel — which is the intended layout. - expect(screen.queryByText('No debts settled yet.')).not.toBeInTheDocument() - expect(screen.getAllByText(/Pushups: /).length).toBeGreaterThan(0) - }) - - it('blocks a new buy-in while a debt is outstanding, and unblocks once it is logged off', async () => { - const user = userEvent.setup() - render() - expect(screen.queryByText('Sit down')).not.toBeInTheDocument() - expect(screen.getByText(/outstanding debt/)).toBeInTheDocument() - - // The gate and the history panel each render a control for the same debt; either clears it. - const [gateInput] = screen.getAllByLabelText('Log units done for Pushups') - const [logButton] = screen.getAllByText('Log progress') - if (gateInput === undefined || logButton === undefined) { - throw new Error('expected the outstanding-debt controls to render') - } - await user.type(gateInput, '10') - await user.click(logButton) - - expect(screen.getByText('Sit down')).toBeInTheDocument() - }) - - it('blocks buying in when the stored ledger is unreadable (fail-closed)', () => { - render() - expect(screen.queryByText('Sit down')).not.toBeInTheDocument() - // Both surfaces fail closed independently: the buy-in gate and the history panel. - expect( - screen.getByText('Your debt history could not be read. Buying in is blocked until this is resolved.'), - ).toBeInTheDocument() - expect(screen.getByText('Your debt history could not be read.')).toBeInTheDocument() - }) - - it('cashes out between hands and settles the resulting debt', async () => { - const user = userEvent.setup() - render() - await user.click(screen.getByText('Sit down')) - - // Play the opening hand to completion so we land in `betweenHands`, where cash-out is legal. - for (let i = 0; i < 200 && screen.queryByText('Deal next hand') === null; i += 1) { - const action = firstLegalAction() - if (action === null) { - break - } - await user.click(action) - } - - // Pin the actual numbers: the panel previews `2*buyIn - stack`, and the settled ledger row - // must show exactly that same figure. A looser assertion here would pass whether or not a - // debt was recorded at all. - const preview = screen.getByText(/Current stack: \d+ chips\./) - const previewText = preview.textContent - const parsed = /Current stack: (\d+) chips\. Cashing out now would owe (\d+) units\./.exec(previewText) - if (parsed === null) { - throw new Error(`cash-out panel did not match the expected copy: ${previewText}`) - } - const [, stackText, owedText] = parsed - const finalChips = Number(stackText) - const previewOwed = Number(owedText) - expect(previewOwed).toBe(Math.max(0, 2 * DEFAULT_BUY_IN - finalChips)) - - await user.click(screen.getByText('Leave table')) - - // Cashing out settles the session, so the app returns to the (now debt-gated) buy-in screen, - // and the ledger row carries the same owed figure the preview promised. - expect(screen.getByText(/outstanding debt/)).toBeInTheDocument() - expect(screen.getAllByText(`Pushups: 0 / ${String(previewOwed)} done`).length).toBeGreaterThan(0) - }) - - it('settles a bust at exactly twice the buy-in', async () => { - const user = userEvent.setup() - const storage = memoryStorage() - render() - await user.click(screen.getByText('Sit down')) - await playUntilSettled(user) - - const stored: unknown = JSON.parse(storage.getItem(DEBTS_STORAGE_KEY) ?? '[]') - expect(Array.isArray(stored)).toBe(true) - const debts = stored as SettledDebt[] - expect(debts.length).toBeGreaterThan(0) - const settled = debts[debts.length - 1] - if (settled === undefined) { - throw new Error('expected a settled debt') - } - // Seed 7 + always-take-the-first-legal-action busts deterministically, so this pins the - // bust case exactly rather than accepting either branch. Busting to zero is the formula's - // natural maximum, not a special case: 2*buyIn - 0. - expect(settled.reason).toBe('bust') - expect(settled.owedUnits).toBe(2 * DEFAULT_BUY_IN) - }) -}) diff --git a/poker-stakes/src/App.tsx b/poker-stakes/src/App.tsx deleted file mode 100644 index 5c3ee5c1..00000000 --- a/poker-stakes/src/App.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useState } from 'react' -import type { ReactElement } from 'react' -import { BettingControls } from './ui/BettingControls' -import { BuyInScreen } from './ui/BuyInScreen' -import { CashOutPanel } from './ui/CashOutPanel' -import { DebtHistoryPanel } from './ui/DebtHistoryPanel' -import { HandLog } from './ui/HandLog' -import { Table } from './ui/Table' -import { useGame } from './ui/useGame' - -interface AppProps { - /** Injected so tests can drive a scripted in-memory ledger; defaults to the real localStorage. */ - storage?: Storage - /** Injected so tests can script a deterministic deal; defaults to a per-mount clock read. */ - seed?: number -} - -export const App = ({ storage = localStorage, seed }: AppProps = {}): ReactElement => { - // Lazy init: `Date.now()` must be read once per mount, not on every render — an inline default - // would make the component impure and re-seed the deck on each re-render. - const [dealSeed] = useState(() => seed ?? Date.now()) - const { state, buyIn, dealHand, submitPlayerAction, cashOut, logDebtProgress } = useGame(storage, dealSeed) - const { debtLoad, buyIn: activeBuyIn, session, handPhase } = state - - return ( -
-
-

Poker Stakes

-

Lose the hand, pay the price — in pushups, problems or reviews.

-
- {session === null || activeBuyIn === null ? ( - - ) : ( - <> - - {handPhase?.kind === 'awaiting' && ( - - )} - {/* - `handPhase` is never null while a session exists — `useGame.buyIn` deals the opening - hand as part of sitting down — so the only two states here are the awaiting/between - pair above and below. - */} - {handPhase?.kind === 'betweenHands' && ( - <> - - - - )} - - - )} - - - ) -} diff --git a/poker-stakes/src/engine/bettingRound.test.ts b/poker-stakes/src/engine/bettingRound.test.ts deleted file mode 100644 index 6b1460d6..00000000 --- a/poker-stakes/src/engine/bettingRound.test.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { applyAction, legalActions } from './bettingRound' -import type { BettingState, SeatState } from './bettingRound' - -const seat = (overrides: Partial = {}): SeatState => ({ - stack: 100, - committed: 0, - hasActed: false, - folded: false, - allIn: false, - ...overrides, -}) - -const state = (overrides: Partial = {}): BettingState => ({ - street: 'preflop', - pot: 0, - toAct: 'player', - seats: { player: seat(), opponent: seat() }, - betToCall: 0, - minRaiseSize: 10, - roundOver: false, - ...overrides, -}) - -const totalChips = (s: BettingState): number => - s.pot + s.seats.player.stack + s.seats.player.committed + s.seats.opponent.stack + s.seats.opponent.committed - -describe('legalActions', () => { - it('returns no actions once the round is over', () => { - expect(legalActions(state({ roundOver: true }))).toEqual([]) - }) - - it('returns no actions for a folded actor', () => { - expect(legalActions(state({ seats: { player: seat({ folded: true }), opponent: seat() } }))).toEqual([]) - }) - - it('returns no actions for an all-in actor', () => { - expect( - legalActions(state({ seats: { player: seat({ allIn: true, stack: 0 }), opponent: seat() } })), - ).toEqual([]) - }) - - it('offers check (not call) when nothing is owed', () => { - const actions = legalActions(state({ betToCall: 0 })) - expect(actions).toContainEqual({ type: 'fold' }) - expect(actions).toContainEqual({ type: 'check' }) - expect(actions.some((a) => a.type === 'call')).toBe(false) - }) - - it('offers call (not check) when a bet is owed', () => { - const actions = legalActions( - state({ betToCall: 20, seats: { player: seat({ committed: 0 }), opponent: seat({ committed: 20 }) } }), - ) - expect(actions).toContainEqual({ type: 'fold' }) - expect(actions).toContainEqual({ type: 'call' }) - expect(actions.some((a) => a.type === 'check')).toBe(false) - }) - - it('offers bet (not raise) as the open-action name when nothing is committed yet', () => { - const actions = legalActions(state({ betToCall: 0 })) - expect(actions.some((a) => a.type === 'bet')).toBe(true) - expect(actions.some((a) => a.type === 'raise')).toBe(false) - }) - - it('offers raise (not bet) as the open-action name when a bet is already in', () => { - const actions = legalActions( - state({ betToCall: 20, seats: { player: seat({ committed: 0 }), opponent: seat({ committed: 20 }) } }), - ) - expect(actions.some((a) => a.type === 'raise')).toBe(true) - expect(actions.some((a) => a.type === 'bet')).toBe(false) - }) - - it('offers a single bet/raise size (all-in only) when stack is too small for a full min-raise', () => { - const actions = legalActions( - state({ - betToCall: 20, - minRaiseSize: 50, - seats: { player: seat({ committed: 0, stack: 25 }), opponent: seat({ committed: 20 }) }, - }), - ) - const raises = actions.filter((a) => a.type === 'raise') - expect(raises).toHaveLength(1) - expect(raises[0]).toEqual({ type: 'raise', amount: 25 }) - }) - - it('offers no bet/raise when the actor can only call all-in (stack equals amount owed)', () => { - const actions = legalActions( - state({ - betToCall: 20, - seats: { player: seat({ committed: 0, stack: 20 }), opponent: seat({ committed: 20 }) }, - }), - ) - expect(actions.some((a) => a.type === 'bet' || a.type === 'raise')).toBe(false) - expect(actions).toContainEqual({ type: 'call' }) - }) - - it('offers both a minimum and an all-in size when stack exceeds the minimum raise', () => { - const actions = legalActions(state({ betToCall: 0, minRaiseSize: 10, seats: { player: seat({ stack: 100 }), opponent: seat() } })) - const bets = actions.filter((a) => a.type === 'bet') - expect(bets).toHaveLength(2) - expect(bets[0]).toEqual({ type: 'bet', amount: 10 }) - expect(bets[1]).toEqual({ type: 'bet', amount: 100 }) - }) -}) - -describe('applyAction: fold', () => { - it('marks the actor folded and ends the round', () => { - const s = state() - const next = applyAction(s, { type: 'fold' }) - expect(next.seats.player.folded).toBe(true) - expect(next.roundOver).toBe(true) - expect(totalChips(next)).toBe(totalChips(s)) - }) -}) - -describe('applyAction: check', () => { - it('passes action to the other seat without ending the round if they have not acted', () => { - const s = state() - const next = applyAction(s, { type: 'check' }) - expect(next.toAct).toBe('opponent') - expect(next.roundOver).toBe(false) - }) - - it('ends the round when both seats have checked', () => { - const s = state() - const afterPlayer = applyAction(s, { type: 'check' }) - const afterOpponent = applyAction(afterPlayer, { type: 'check' }) - expect(afterOpponent.roundOver).toBe(true) - expect(totalChips(afterOpponent)).toBe(totalChips(s)) - }) -}) - -describe('applyAction: call', () => { - it('matches the bet, ends the round, and preserves total chips (opponent already acted)', () => { - const s = state({ - betToCall: 20, - toAct: 'player', - seats: { - player: seat({ committed: 0, stack: 100 }), - opponent: seat({ committed: 20, stack: 80, hasActed: true }), - }, - }) - const next = applyAction(s, { type: 'call' }) - expect(next.seats.player.committed).toBe(20) - expect(next.seats.player.stack).toBe(80) - expect(next.roundOver).toBe(true) - expect(totalChips(next)).toBe(totalChips(s)) - }) - - it('does NOT end the round when the other seat has not yet acted (heads-up big-blind option)', () => { - // SB completes preflop to match the BB's posted blind; BB has not acted this round yet. - const s = state({ - betToCall: 10, - toAct: 'player', - seats: { - player: seat({ committed: 0, stack: 100 }), - opponent: seat({ committed: 10, stack: 90, hasActed: false }), - }, - }) - const next = applyAction(s, { type: 'call' }) - expect(next.roundOver).toBe(false) - expect(next.toAct).toBe('opponent') - expect(totalChips(next)).toBe(totalChips(s)) - }) - - it('goes all-in when the stack is smaller than the call amount owed is not offered, but a call for exactly the stack works', () => { - const s = state({ - betToCall: 20, - seats: { player: seat({ committed: 0, stack: 20 }), opponent: seat({ committed: 20 }) }, - }) - const next = applyAction(s, { type: 'call' }) - expect(next.seats.player.stack).toBe(0) - expect(next.seats.player.allIn).toBe(true) - expect(totalChips(next)).toBe(totalChips(s)) - }) -}) - -describe('applyAction: bet/raise', () => { - it('opens a bet, updates betToCall and minRaiseSize, and requires the other seat to act again', () => { - const s = state({ minRaiseSize: 10 }) - const next = applyAction(s, { type: 'bet', amount: 10 }) - expect(next.betToCall).toBe(10) - expect(next.minRaiseSize).toBe(10) - expect(next.toAct).toBe('opponent') - expect(next.roundOver).toBe(false) - expect(next.seats.opponent.hasActed).toBe(false) - expect(totalChips(next)).toBe(totalChips(s)) - }) - - // A short all-in (a raise that adds LESS than the current minimum) must not lower the floor for - // whoever acts next — that is what `Math.max(state.minRaiseSize, raiseSize)` protects. Without - // the floor, a 5-chip all-in over a min-raise of 50 would let the next player re-raise by 5. - it('does not lower minRaiseSize when an all-in raise is smaller than the current minimum', () => { - const s = state({ - betToCall: 20, - minRaiseSize: 50, - toAct: 'opponent', - seats: { - player: seat({ committed: 20, stack: 80, hasActed: true }), - opponent: seat({ committed: 0, stack: 25 }), - }, - }) - // All-in for 25 total: only 5 more than the 20 to call, well under the 50 min-raise. - const next = applyAction(s, { type: 'raise', amount: 25 }) - expect(next.betToCall).toBe(25) - expect(next.minRaiseSize).toBe(50) - expect(next.seats.opponent.allIn).toBe(true) - expect(totalChips(next)).toBe(totalChips(s)) - }) - - it('raises on top of an existing bet and tracks the new min-raise size', () => { - const s = state({ - betToCall: 10, - minRaiseSize: 10, - toAct: 'opponent', - seats: { player: seat({ committed: 10, stack: 90, hasActed: true }), opponent: seat({ committed: 0, stack: 100 }) }, - }) - const next = applyAction(s, { type: 'raise', amount: 30 }) - expect(next.betToCall).toBe(30) - expect(next.minRaiseSize).toBe(20) - expect(next.toAct).toBe('player') - expect(totalChips(next)).toBe(totalChips(s)) - }) - - it('caps spend at the actor stack and marks all-in when betting the full stack', () => { - const s = state({ seats: { player: seat({ stack: 40 }), opponent: seat() } }) - const next = applyAction(s, { type: 'bet', amount: 40 }) - expect(next.seats.player.stack).toBe(0) - expect(next.seats.player.allIn).toBe(true) - expect(totalChips(next)).toBe(totalChips(s)) - }) -}) - -describe('a full betting round preserves total chips end to end', () => { - it('bet, raise, call sequence', () => { - let s = state({ minRaiseSize: 10, seats: { player: seat({ stack: 200 }), opponent: seat({ stack: 200 }) } }) - const initialTotal = totalChips(s) - s = applyAction(s, { type: 'bet', amount: 20 }) - s = applyAction(s, { type: 'raise', amount: 60 }) - s = applyAction(s, { type: 'call' }) - expect(s.roundOver).toBe(true) - expect(totalChips(s)).toBe(initialTotal) - }) -}) diff --git a/poker-stakes/src/engine/bettingRound.ts b/poker-stakes/src/engine/bettingRound.ts deleted file mode 100644 index ccb747a5..00000000 --- a/poker-stakes/src/engine/bettingRound.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { Seat, Street } from './types' - -export type Action = - | { type: 'fold' } - | { type: 'check' } - | { type: 'call' } - | { type: 'bet'; amount: number } - | { type: 'raise'; amount: number } - -export interface SeatState { - stack: number - committed: number // chips this seat has put in during the CURRENT betting round - hasActed: boolean - folded: boolean - allIn: boolean -} - -export interface BettingState { - street: Street - pot: number // chips committed in PREVIOUS rounds, not yet including this round - toAct: Seat - seats: Record - betToCall: number // highest `committed` this round - minRaiseSize: number // minimum additional amount a raise must add beyond a call, per no-limit rules - roundOver: boolean -} - -const other = (seat: Seat): Seat => (seat === 'player' ? 'opponent' : 'player') - -/** - * Chips this seat still needs to add to match betToCall. - * - * The `Math.max(0, ...)` floor is defensive only: `applyAction` always sets `betToCall` to the - * highest `committed` in the round, so no seat's `committed` can exceed it and the subtraction is - * never negative in practice — the full suite passes with the floor removed. It stays because it - * costs nothing and makes the "never negative" contract local to this function rather than an - * invariant a reader has to reconstruct from every `betToCall` write site. - */ -const owedToCall = (state: BettingState, seat: Seat): number => - Math.max(0, state.betToCall - state.seats[seat].committed) - -/** Generates the legal actions for the seat to act. Illegal actions are never representable in the UI. */ -export const legalActions = (state: BettingState): readonly Action[] => { - if (state.roundOver) { - return [] - } - const actor = state.seats[state.toAct] - if (actor.folded || actor.allIn) { - return [] - } - - const toCall = owedToCall(state, state.toAct) - const actions: Action[] = [{ type: 'fold' }] - - if (toCall === 0) { - actions.push({ type: 'check' }) - } else { - actions.push({ type: 'call' }) - } - - const maxAdditional = actor.stack - const minOpenOrRaiseAdditional = toCall + state.minRaiseSize - if (maxAdditional > toCall) { - const kind = state.betToCall === 0 ? 'bet' : 'raise' - const minAdditional = Math.min(minOpenOrRaiseAdditional, maxAdditional) - // Every legal size from the minimum up to an all-in is available; expose min and all-in as - // concrete options rather than an open-ended range, since the UI drives a discrete control. - actions.push({ type: kind, amount: minAdditional }) - if (maxAdditional > minAdditional) { - actions.push({ type: kind, amount: maxAdditional }) - } - } - - return actions -} - -const applyChips = (seat: SeatState, additional: number): SeatState => { - const spend = Math.min(additional, seat.stack) - return { - ...seat, - stack: seat.stack - spend, - committed: seat.committed + spend, - allIn: seat.stack - spend === 0, - } -} - -/** Applies an action, returning the next BettingState. Assumes the action came from legalActions(state). */ -export const applyAction = (state: BettingState, action: Action): BettingState => { - const actingSeat = state.toAct - const opponentSeat = other(actingSeat) - - if (action.type === 'fold') { - return { - ...state, - seats: { ...state.seats, [actingSeat]: { ...state.seats[actingSeat], folded: true, hasActed: true } }, - roundOver: true, - } - } - - if (action.type === 'check') { - const nextSeats: Record = { - ...state.seats, - [actingSeat]: { ...state.seats[actingSeat], hasActed: true }, - } - const bothActed = nextSeats[opponentSeat].hasActed - return { ...state, seats: nextSeats, toAct: opponentSeat, roundOver: bothActed } - } - - if (action.type === 'call') { - const toCall = owedToCall(state, actingSeat) - const nextSeats: Record = { - ...state.seats, - [actingSeat]: { ...applyChips(state.seats[actingSeat], toCall), hasActed: true }, - } - // A call closes the round UNLESS the other seat has never acted this round yet (the - // heads-up big-blind-option case: SB completes preflop, BB still gets first say). - const bothActed = nextSeats[opponentSeat].hasActed - return { ...state, seats: nextSeats, toAct: opponentSeat, roundOver: bothActed } - } - - // bet or raise - const nextActingSeatState: SeatState = { - ...applyChips(state.seats[actingSeat], action.amount), - hasActed: true, - } - const raiseSize = nextActingSeatState.committed - state.betToCall - return { - ...state, - seats: { - ...state.seats, - [actingSeat]: nextActingSeatState, - [opponentSeat]: { ...state.seats[opponentSeat], hasActed: false }, - }, - betToCall: nextActingSeatState.committed, - minRaiseSize: Math.max(state.minRaiseSize, raiseSize), - toAct: opponentSeat, - roundOver: false, - } -} diff --git a/poker-stakes/src/engine/deck.test.ts b/poker-stakes/src/engine/deck.test.ts deleted file mode 100644 index bb7494d3..00000000 --- a/poker-stakes/src/engine/deck.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { buildDeck, createDealCursor, dealCards, shuffleDeck } from './deck' -import { createRng } from './rng' -import type { Card } from './types' - -const cardKey = (card: Card): string => `${String(card.rank)}-${card.suit}` - -describe('buildDeck', () => { - it('builds exactly 52 unique cards', () => { - const deck = buildDeck() - expect(deck).toHaveLength(52) - expect(new Set(deck.map(cardKey)).size).toBe(52) - }) -}) - -describe('shuffleDeck', () => { - it('returns a permutation of the input deck', () => { - const deck = buildDeck() - const shuffled = shuffleDeck(createRng(1), deck) - expect(shuffled).toHaveLength(52) - expect(new Set(shuffled.map(cardKey))).toEqual(new Set(deck.map(cardKey))) - }) - - it('is deterministic for a given seed', () => { - const deck = buildDeck() - const a = shuffleDeck(createRng(42), deck) - const b = shuffleDeck(createRng(42), deck) - expect(a).toEqual(b) - }) - - it('produces a different order for a different seed', () => { - const deck = buildDeck() - const a = shuffleDeck(createRng(1), deck) - const b = shuffleDeck(createRng(2), deck) - expect(a).not.toEqual(b) - }) - - it('does not mutate the input deck', () => { - const deck = buildDeck() - const before = [...deck] - shuffleDeck(createRng(7), deck) - expect(deck).toEqual(before) - }) -}) - -describe('dealCards', () => { - it('deals the requested count and advances the cursor', () => { - const deck = shuffleDeck(createRng(3), buildDeck()) - const cursor0 = createDealCursor(deck) - const { cards, cursor: cursor1 } = dealCards(cursor0, 2) - expect(cards).toEqual(deck.slice(0, 2)) - expect(cursor1.next).toBe(2) - }) - - it('deals sequential batches without overlap', () => { - const deck = shuffleDeck(createRng(4), buildDeck()) - const { cursor: afterHole } = dealCards(createDealCursor(deck), 4) - const { cards: flop, cursor: afterFlop } = dealCards(afterHole, 3) - expect(flop).toEqual(deck.slice(4, 7)) - expect(afterFlop.next).toBe(7) - }) - - it('throws when asked for more cards than remain', () => { - const deck = buildDeck() - const cursor = createDealCursor(deck) - expect(() => dealCards(cursor, 53)).toThrow('not enough cards remaining') - }) -}) diff --git a/poker-stakes/src/engine/deck.ts b/poker-stakes/src/engine/deck.ts deleted file mode 100644 index f06b63ec..00000000 --- a/poker-stakes/src/engine/deck.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Rng } from './rng' -import { nextInt } from './rng' -import type { Card, Rank, Suit } from './types' - -const SUITS: readonly Suit[] = ['clubs', 'diamonds', 'hearts', 'spades'] -const RANKS: readonly Rank[] = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] - -/** Builds a deterministic 52-card deck in fixed suit/rank order. */ -export const buildDeck = (): readonly Card[] => { - const deck: Card[] = [] - for (const suit of SUITS) { - for (const rank of RANKS) { - deck.push({ rank, suit }) - } - } - return deck -} - -/** - * Selection-sampling shuffle: repeatedly pulls a random remaining card into - * the output. Uses splice (never indexed reads) so no element access is ever - * typed `Card | undefined` under noUncheckedIndexedAccess. - */ -export const shuffleDeck = (rng: Rng, deck: readonly Card[]): Card[] => { - const pool: Card[] = [...deck] - const out: Card[] = [] - while (pool.length > 0) { - for (const card of pool.splice(nextInt(rng, pool.length), 1)) { - out.push(card) - } - } - return out -} - -export interface DealCursor { - deck: readonly Card[] - next: number -} - -export const createDealCursor = (deck: readonly Card[]): DealCursor => ({ deck, next: 0 }) - -/** Deals `count` cards from the cursor, returning the cards and an advanced cursor. */ -export const dealCards = ( - cursor: DealCursor, - count: number, -): { cards: Card[]; cursor: DealCursor } => { - if (cursor.next + count > cursor.deck.length) { - throw new Error('dealCards: not enough cards remaining in deck') - } - const cards = cursor.deck.slice(cursor.next, cursor.next + count) - return { cards, cursor: { deck: cursor.deck, next: cursor.next + count } } -} diff --git a/poker-stakes/src/engine/handEval.compare.test.ts b/poker-stakes/src/engine/handEval.compare.test.ts deleted file mode 100644 index 55805808..00000000 --- a/poker-stakes/src/engine/handEval.compare.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { compareHandScore, evaluate7, rankLabel } from './handEval' -import type { HandScore } from './handEval' -import { card, seven } from '../test/handEvalFixtures' - -describe('evaluate7', () => { - it('picks the best 5 of 7 when the best hand uses only some hole+board cards', () => { - const hand = seven([ - card(14, 'clubs'), - card(14, 'diamonds'), - card(14, 'hearts'), - card(14, 'spades'), - card(2, 'clubs'), - card(3, 'diamonds'), - card(4, 'hearts'), - ]) - const score = evaluate7(hand) - expect(score.category).toBe('quads') - }) - - it('matches the best rank5 result across all 21 combinations', () => { - const cards = seven([ - card(10, 'clubs'), - card(11, 'clubs'), - card(12, 'clubs'), - card(13, 'clubs'), - card(14, 'clubs'), - card(2, 'hearts'), - card(2, 'diamonds'), - ]) - const score = evaluate7(cards) - expect(score.category).toBe('straightFlush') - expect(score.tiebreakers).toEqual([14]) - }) -}) - -describe('compareHandScore category comparison', () => { - it('ranks a lower category below a higher one in both directions', () => { - const high: HandScore = { category: 'quads', tiebreakers: [2] } - const low: HandScore = { category: 'highCard', tiebreakers: [14] } - expect(compareHandScore(high, low)).toBe(1) - expect(compareHandScore(low, high)).toBe(-1) - }) -}) - -describe('compareHandScore tiebreaker padding', () => { - it('treats a missing tiebreaker slot as lower than a present one', () => { - const a: HandScore = { category: 'highCard', tiebreakers: [14, 10] } - const b: HandScore = { category: 'highCard', tiebreakers: [14] } - expect(compareHandScore(a, b)).toBe(1) - expect(compareHandScore(b, a)).toBe(-1) - }) -}) - -describe('rankLabel', () => { - it('labels face cards and ace', () => { - expect(rankLabel(14)).toBe('A') - expect(rankLabel(13)).toBe('K') - expect(rankLabel(12)).toBe('Q') - expect(rankLabel(11)).toBe('J') - }) - - it('labels number cards as their numeral', () => { - expect(rankLabel(10)).toBe('10') - expect(rankLabel(2)).toBe('2') - }) -}) diff --git a/poker-stakes/src/engine/handEval.rank5.test.ts b/poker-stakes/src/engine/handEval.rank5.test.ts deleted file mode 100644 index 4216a268..00000000 --- a/poker-stakes/src/engine/handEval.rank5.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { buildDeck } from './deck' -import { compareHandScore, rank5 } from './handEval' -import type { HandCategory } from './handEval' -import type { Card } from './types' -import { card, combinations5, five } from '../test/handEvalFixtures' - -describe('rank5 exhaustive classification', () => { - it('classifies every 5-card hand out of 52 into the correct standard-count category', () => { - const deck = buildDeck() - const histogram: Record = { - highCard: 0, - onePair: 0, - twoPair: 0, - trips: 0, - straight: 0, - flush: 0, - fullHouse: 0, - quads: 0, - straightFlush: 0, - } - - let total = 0 - for (const [i0, i1, i2, i3, i4] of combinations5(deck.length)) { - const hand = five([deck[i0], deck[i1], deck[i2], deck[i3], deck[i4]] as Card[]) - const { category } = rank5(hand) - histogram[category] += 1 - total += 1 - } - - expect(total).toBe(2_598_960) - // Straight/flush counts here fold the royal flush into straightFlush (A-high straight flush). - expect(histogram).toEqual({ - highCard: 1_302_540, - onePair: 1_098_240, - twoPair: 123_552, - trips: 54_912, - straight: 10_200, - flush: 5_108, - fullHouse: 3_744, - quads: 624, - straightFlush: 40, - }) - }, 30_000) -}) - -describe('rank5 named fixtures', () => { - it('scores a wheel straight (A-2-3-4-5) as 5-high', () => { - const hand = five([ - card(14, 'clubs'), - card(2, 'diamonds'), - card(3, 'hearts'), - card(4, 'spades'), - card(5, 'clubs'), - ]) - const score = rank5(hand) - expect(score.category).toBe('straight') - expect(score.tiebreakers).toEqual([5]) - }) - - it('scores a wheel straight flush as 5-high straight flush', () => { - const hand = five([ - card(14, 'clubs'), - card(2, 'clubs'), - card(3, 'clubs'), - card(4, 'clubs'), - card(5, 'clubs'), - ]) - const score = rank5(hand) - expect(score.category).toBe('straightFlush') - expect(score.tiebreakers).toEqual([5]) - }) - - it('scores a broadway (ace-high) straight correctly', () => { - const hand = five([ - card(14, 'clubs'), - card(13, 'diamonds'), - card(12, 'hearts'), - card(11, 'spades'), - card(10, 'clubs'), - ]) - const score = rank5(hand) - expect(score.category).toBe('straight') - expect(score.tiebreakers).toEqual([14]) - }) - - it('breaks ties between two one-pair hands by kicker', () => { - const a = rank5( - five([card(9, 'clubs'), card(9, 'diamonds'), card(14, 'hearts'), card(4, 'spades'), card(2, 'clubs')]), - ) - const b = rank5( - five([card(9, 'clubs'), card(9, 'diamonds'), card(13, 'hearts'), card(4, 'spades'), card(2, 'clubs')]), - ) - expect(compareHandScore(a, b)).toBe(1) - expect(compareHandScore(b, a)).toBe(-1) - }) - - it('breaks ties between two full houses by trips rank, not pair rank', () => { - // 2-2-2-A-A must beat 3-3-3-K-K: trips rank (2 < 3) decides, not the pair kicker (A > K). - const twosOverAces = rank5( - five([card(2, 'clubs'), card(2, 'diamonds'), card(2, 'hearts'), card(14, 'spades'), card(14, 'clubs')]), - ) - const threesOverKings = rank5( - five([card(3, 'clubs'), card(3, 'diamonds'), card(3, 'hearts'), card(13, 'spades'), card(13, 'clubs')]), - ) - expect(compareHandScore(threesOverKings, twosOverAces)).toBe(1) - expect(compareHandScore(twosOverAces, threesOverKings)).toBe(-1) - }) - - it('breaks ties between two two-pair hands by top pair, then bottom pair, then kicker', () => { - const acesAndTwos = rank5( - five([card(14, 'clubs'), card(14, 'diamonds'), card(2, 'hearts'), card(2, 'spades'), card(5, 'clubs')]), - ) - const acesAndThrees = rank5( - five([card(14, 'hearts'), card(14, 'spades'), card(3, 'clubs'), card(3, 'diamonds'), card(4, 'hearts')]), - ) - expect(compareHandScore(acesAndThrees, acesAndTwos)).toBe(1) - expect(compareHandScore(acesAndTwos, acesAndThrees)).toBe(-1) - }) - - it('breaks ties between two quads hands by the quad rank', () => { - const fours = rank5( - five([card(4, 'clubs'), card(4, 'diamonds'), card(4, 'hearts'), card(4, 'spades'), card(2, 'clubs')]), - ) - const fives = rank5( - five([card(5, 'clubs'), card(5, 'diamonds'), card(5, 'hearts'), card(5, 'spades'), card(2, 'diamonds')]), - ) - expect(compareHandScore(fives, fours)).toBe(1) - expect(compareHandScore(fours, fives)).toBe(-1) - }) - - it('breaks ties between two trips hands by trips rank then kickers', () => { - const treys = rank5( - five([card(3, 'clubs'), card(3, 'diamonds'), card(3, 'hearts'), card(14, 'spades'), card(13, 'clubs')]), - ) - const fours = rank5( - five([card(4, 'clubs'), card(4, 'diamonds'), card(4, 'hearts'), card(2, 'spades'), card(3, 'clubs')]), - ) - expect(compareHandScore(fours, treys)).toBe(1) - expect(compareHandScore(treys, fours)).toBe(-1) - }) - - it('breaks ties between two flushes by highest differing card', () => { - const aceHighFlush = rank5( - five([card(14, 'clubs'), card(9, 'clubs'), card(7, 'clubs'), card(4, 'clubs'), card(2, 'clubs')]), - ) - const kingHighFlush = rank5( - five([card(13, 'diamonds'), card(10, 'diamonds'), card(8, 'diamonds'), card(5, 'diamonds'), card(3, 'diamonds')]), - ) - expect(compareHandScore(aceHighFlush, kingHighFlush)).toBe(1) - expect(compareHandScore(kingHighFlush, aceHighFlush)).toBe(-1) - }) - - it('breaks ties between two straights by the high card', () => { - const sevenHigh = rank5( - five([card(7, 'clubs'), card(6, 'diamonds'), card(5, 'hearts'), card(4, 'spades'), card(3, 'clubs')]), - ) - const eightHigh = rank5( - five([card(8, 'clubs'), card(7, 'diamonds'), card(6, 'hearts'), card(5, 'spades'), card(4, 'clubs')]), - ) - expect(compareHandScore(eightHigh, sevenHigh)).toBe(1) - expect(compareHandScore(sevenHigh, eightHigh)).toBe(-1) - }) - - it('recognizes an exact split (identical hand scores)', () => { - const a = rank5( - five([card(9, 'clubs'), card(9, 'diamonds'), card(14, 'hearts'), card(4, 'spades'), card(2, 'clubs')]), - ) - const b = rank5( - five([card(9, 'hearts'), card(9, 'spades'), card(14, 'clubs'), card(4, 'diamonds'), card(2, 'hearts')]), - ) - expect(compareHandScore(a, b)).toBe(0) - }) -}) diff --git a/poker-stakes/src/engine/handEval.ts b/poker-stakes/src/engine/handEval.ts deleted file mode 100644 index 314c6ed5..00000000 --- a/poker-stakes/src/engine/handEval.ts +++ /dev/null @@ -1,182 +0,0 @@ -import type { Card, Rank } from './types' - -export type HandCategory = - | 'highCard' - | 'onePair' - | 'twoPair' - | 'trips' - | 'straight' - | 'flush' - | 'fullHouse' - | 'quads' - | 'straightFlush' - -const CATEGORY_RANK: Record = { - highCard: 0, - onePair: 1, - twoPair: 2, - trips: 3, - straight: 4, - flush: 5, - fullHouse: 6, - quads: 7, - straightFlush: 8, -} - -export interface HandScore { - category: HandCategory - tiebreakers: readonly number[] -} - -type Five = readonly [Card, Card, Card, Card, Card] -type Seven = readonly [Card, Card, Card, Card, Card, Card, Card] - -/** -1 if a < b, 0 if equal, 1 if a > b. Higher HandScore wins. */ -export const compareHandScore = (a: HandScore, b: HandScore): -1 | 0 | 1 => { - const catA = CATEGORY_RANK[a.category] - const catB = CATEGORY_RANK[b.category] - if (catA !== catB) { - return catA > catB ? 1 : -1 - } - const len = Math.max(a.tiebreakers.length, b.tiebreakers.length) - for (let i = 0; i < len; i += 1) { - const ta = a.tiebreakers[i] ?? 0 - const tb = b.tiebreakers[i] ?? 0 - if (ta !== tb) { - return ta > tb ? 1 : -1 - } - } - return 0 -} - -const countBy = (values: readonly T[]): Map => { - const counts = new Map() - for (const value of values) { - counts.set(value, (counts.get(value) ?? 0) + 1) - } - return counts -} - -/** Ranks sorted descending, deduped, from a set of cards. */ -const descendingRanks = (cards: Five): number[] => { - const ranks = cards.map((c) => c.rank) - return [...new Set(ranks)].sort((a, b) => b - a) -} - -/** Straight high card, or null if the 5 ranks (deduped) don't form a run of 5. Wheel (A-2-3-4-5) scores as 5-high. */ -const straightHigh = (uniqueDescRanks: readonly number[]): number | null => { - if (uniqueDescRanks.length !== 5) { - return null - } - const [r0, r1, r2, r3, r4] = uniqueDescRanks as readonly [number, number, number, number, number] - const isRun = r0 - r1 === 1 && r1 - r2 === 1 && r2 - r3 === 1 && r3 - r4 === 1 - if (isRun) { - return r0 - } - const isWheel = r0 === 14 && r1 === 5 && r2 === 4 && r3 === 3 && r4 === 2 - return isWheel ? 5 : null -} - -/** Evaluates exactly 5 cards. */ -export const rank5 = (cards: Five): HandScore => { - const suits = cards.map((c) => c.suit) - const isFlush = new Set(suits).size === 1 - - const rankCounts = countBy(cards.map((c) => c.rank)) - const uniqueDesc = descendingRanks(cards) - const straightTop = straightHigh(uniqueDesc) - - const groups = [...rankCounts.entries()].sort((a, b) => { - if (a[1] !== b[1]) { - return b[1] - a[1] - } - return b[0] - a[0] - }) - const groupCounts = groups.map(([, count]) => count) - const groupRanks = groups.map(([rank]) => rank) - - if (isFlush && straightTop !== null) { - return { category: 'straightFlush', tiebreakers: [straightTop] } - } - if (groupCounts[0] === 4) { - return { category: 'quads', tiebreakers: groupRanks } - } - if (groupCounts[0] === 3 && groupCounts[1] === 2) { - return { category: 'fullHouse', tiebreakers: groupRanks } - } - if (isFlush) { - return { category: 'flush', tiebreakers: uniqueDesc } - } - if (straightTop !== null) { - return { category: 'straight', tiebreakers: [straightTop] } - } - if (groupCounts[0] === 3) { - return { category: 'trips', tiebreakers: groupRanks } - } - if (groupCounts[0] === 2 && groupCounts[1] === 2) { - return { category: 'twoPair', tiebreakers: groupRanks } - } - if (groupCounts[0] === 2) { - return { category: 'onePair', tiebreakers: groupRanks } - } - return { category: 'highCard', tiebreakers: uniqueDesc } -} - -type FiveIndices = readonly [number, number, number, number, number] - -/** All C(7,5) = 21 five-card index combinations out of positions 0-6, written as a literal tuple. */ -const SEVEN_CHOOSE_FIVE: readonly [FiveIndices, ...FiveIndices[]] = [ - [0, 1, 2, 3, 4], - [0, 1, 2, 3, 5], - [0, 1, 2, 3, 6], - [0, 1, 2, 4, 5], - [0, 1, 2, 4, 6], - [0, 1, 2, 5, 6], - [0, 1, 3, 4, 5], - [0, 1, 3, 4, 6], - [0, 1, 3, 5, 6], - [0, 1, 4, 5, 6], - [0, 2, 3, 4, 5], - [0, 2, 3, 4, 6], - [0, 2, 3, 5, 6], - [0, 2, 4, 5, 6], - [0, 3, 4, 5, 6], - [1, 2, 3, 4, 5], - [1, 2, 3, 4, 6], - [1, 2, 3, 5, 6], - [1, 2, 4, 5, 6], - [1, 3, 4, 5, 6], - [2, 3, 4, 5, 6], -] - -/** Best 5-card HandScore out of exactly 7 cards. */ -export const evaluate7 = (cards: Seven): HandScore => { - const [firstCombo, ...restCombos] = SEVEN_CHOOSE_FIVE - const fiveFor = (combo: FiveIndices): Five => - [cards[combo[0]], cards[combo[1]], cards[combo[2]], cards[combo[3]], cards[combo[4]]] as Five - - let best = rank5(fiveFor(firstCombo)) - for (const combo of restCombos) { - const score = rank5(fiveFor(combo)) - if (compareHandScore(score, best) > 0) { - best = score - } - } - return best -} - -export const rankLabel = (rank: Rank): string => { - if (rank === 14) { - return 'A' - } - if (rank === 13) { - return 'K' - } - if (rank === 12) { - return 'Q' - } - if (rank === 11) { - return 'J' - } - return String(rank) -} diff --git a/poker-stakes/src/engine/handFlow.test.ts b/poker-stakes/src/engine/handFlow.test.ts deleted file mode 100644 index b00d3612..00000000 --- a/poker-stakes/src/engine/handFlow.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { Action, BettingState } from './bettingRound' -import { legalActions } from './bettingRound' -import { playHand } from './handFlow' -import type { HandSetup } from './handFlow' -import { createRng } from './rng' -import type { Seat } from './types' - -const setup = (overrides: Partial = {}): HandSetup => ({ - buttonSeat: 'player', - bigBlind: 10, - smallBlind: 5, - stacks: { player: 200, opponent: 200 }, - ...overrides, -}) - -const totalStack = (setupArg: HandSetup): number => setupArg.stacks.player + setupArg.stacks.opponent - -/** Wraps a decide() callback, asserting the returned action is always one legalActions offered. */ -const enforceLegal = ( - decide: (betting: BettingState, seat: Seat) => Action, -): ((betting: BettingState, seat: Seat) => Action) => { - return (betting, seat) => { - const action = decide(betting, seat) - expect(legalActions(betting)).toContainEqual(action) - return action - } -} - -/** Builds a decide() callback from a fixed queue of actions, applied in call order regardless of seat. */ -const scripted = (actions: readonly Action[]): ((betting: BettingState, seat: Seat) => Action) => { - const queue = [...actions] - return enforceLegal(() => { - const next = queue.shift() - if (next === undefined) { - throw new Error('scripted: ran out of scripted actions') - } - return next - }) -} - -/** Always checks/calls — used to run a hand to showdown with minimal betting. */ -const alwaysCheckOrCall = enforceLegal((betting) => { - const toCall = betting.betToCall - betting.seats[betting.toAct].committed - return toCall > 0 ? { type: 'call' } : { type: 'check' } -}) - -describe('playHand: showdown', () => { - it('runs a full hand to showdown and awards the pot to the better hand', () => { - const s = setup() - const result = playHand(createRng(1), s, { decide: alwaysCheckOrCall }) - expect(result.wentToShowdown).toBe(true) - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - expect(['player', 'opponent', 'split']).toContain(result.winner) - }) - - it('is deterministic for a fixed seed', () => { - const s = setup() - const a = playHand(createRng(99), s, { decide: alwaysCheckOrCall }) - const b = playHand(createRng(99), s, { decide: alwaysCheckOrCall }) - expect(a).toEqual(b) - }) -}) - -describe('playHand: fold preflop', () => { - it('awards the pot to the non-folder and refunds nothing beyond the folded blind', () => { - const s = setup() - const decide = scripted([{ type: 'fold' }]) - const result = playHand(createRng(2), s, { decide }) - expect(result.wentToShowdown).toBe(false) - expect(result.loserSeat).toBe('player') - expect(result.winner).toBe('opponent') - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - }) -}) - -describe('playHand: fold postflop', () => { - it('carries the pot across streets and awards it on a later fold', () => { - const s = setup() - // Preflop: player calls, opponent checks (round ends). Flop: opponent bets, player folds. - const decide = scripted([{ type: 'call' }, { type: 'check' }, { type: 'bet', amount: 10 }, { type: 'fold' }]) - const result = playHand(createRng(3), s, { decide }) - expect(result.wentToShowdown).toBe(false) - expect(result.loserSeat).toBe('player') - expect(result.potAwarded).toBeGreaterThanOrEqual(s.bigBlind * 2) - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - }) -}) - -describe('playHand: early all-in preserves total chips', () => { - /** Shoves all-in whenever a bet/raise is legal, otherwise calls/checks — drives every hand to an all-in quickly. */ - const shoveOrCheckCall = enforceLegal((betting, seat) => { - const actions = legalActions(betting) - const shove = actions.find((a) => a.type === 'bet' || a.type === 'raise') - if (shove !== undefined) { - return shove - } - return alwaysCheckOrCall(betting, seat) - }) - - it('handles a short-stacked all-in preflop and resolves at showdown', () => { - const s = setup({ stacks: { player: 15, opponent: 200 } }) - const result = playHand(createRng(4), s, { decide: shoveOrCheckCall }) - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - expect(result.wentToShowdown).toBe(true) - }) -}) - -describe('playHand: fold — opponent as loser', () => { - it('awards the pot to the player when the opponent folds', () => { - const s = setup() - // Preflop: player calls, opponent gets the option and folds. - const decide = scripted([{ type: 'call' }, { type: 'fold' }]) - const result = playHand(createRng(6), s, { decide }) - expect(result.loserSeat).toBe('opponent') - expect(result.winner).toBe('player') - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - }) -}) - -describe('playHand: uncalled excess is returned in both directions', () => { - it('returns excess to the opponent when the opponent bets more and the player folds', () => { - const s = setup() - // Preflop: call, check. Flop: opponent bets, player folds — opponent's committed ends up - // higher than player's, exercising the "opponent higher" branch of returnUncalledExcess. - const decide = scripted([{ type: 'call' }, { type: 'check' }, { type: 'bet', amount: 10 }, { type: 'fold' }]) - const result = playHand(createRng(3), s, { decide }) - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - }) - - it('returns excess to the player when the player bets more and the opponent folds', () => { - const s = setup() - // Preflop: call, check. Flop: opponent checks first, player bets, opponent folds — player's - // committed ends up higher, exercising the "player higher" branch of returnUncalledExcess. - const decide = scripted([{ type: 'call' }, { type: 'check' }, { type: 'check' }, { type: 'bet', amount: 10 }, { type: 'fold' }]) - const result = playHand(createRng(3), s, { decide }) - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - }) -}) - -describe('playHand: split pot', () => { - it('splits the pot evenly on an exact tie (deterministic seed)', () => { - // Seed 15 with check/call-only play produces a genuine board-play tie at showdown. - const s = setup() - const result = playHand(createRng(15), s, { decide: alwaysCheckOrCall }) - expect(result.winner).toBe('split') - expect(result.potAwarded % 2).toBe(0) - expect(result.finalStacks).toEqual({ player: 200, opponent: 200 }) - }) - - it('conserves total chips across many seeds regardless of outcome', () => { - const s = setup() - for (let seed = 1; seed <= 50; seed += 1) { - const result = playHand(createRng(seed), s, { decide: alwaysCheckOrCall }) - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - expect(result.potAwarded % 2).toBe(0) - } - }) -}) - -describe('playHand: button rotation', () => { - it('lets the opponent be the button/small blind and act first preflop', () => { - const s = setup({ buttonSeat: 'opponent' }) - const result = playHand(createRng(5), s, { decide: alwaysCheckOrCall }) - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - }) -}) - -describe('playHand: a seat all-in from a blind post never acts again', () => { - it('never offers a decision to a 0-stack seat on a later street (button posts SB all-in)', () => { - // Button posts smallBlind=1 from a 1-chip stack, going all-in preflop. A fresh, buggy - // `allIn: false` reset on flop/turn/river would let this 0-stack seat fold/check again. - const s = setup({ buttonSeat: 'player', bigBlind: 2, smallBlind: 1, stacks: { player: 1, opponent: 200 } }) - const seenActingWithZeroStack: Seat[] = [] - const decide = (betting: BettingState, seat: Seat): Action => { - if (betting.seats[seat].stack === 0) { - seenActingWithZeroStack.push(seat) - } - return alwaysCheckOrCall(betting, seat) - } - const result = playHand(createRng(1), s, { decide }) - expect(seenActingWithZeroStack).toEqual([]) - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - }) - - it('never offers a decision to a 0-stack seat on a later street (non-button posts BB all-in)', () => { - const s = setup({ buttonSeat: 'player', bigBlind: 2, smallBlind: 1, stacks: { player: 200, opponent: 2 } }) - const seenActingWithZeroStack: Seat[] = [] - const decide = (betting: BettingState, seat: Seat): Action => { - if (betting.seats[seat].stack === 0) { - seenActingWithZeroStack.push(seat) - } - return alwaysCheckOrCall(betting, seat) - } - const result = playHand(createRng(1), s, { decide }) - expect(seenActingWithZeroStack).toEqual([]) - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - }) -}) diff --git a/poker-stakes/src/engine/handFlow.ts b/poker-stakes/src/engine/handFlow.ts deleted file mode 100644 index 0b977ffb..00000000 --- a/poker-stakes/src/engine/handFlow.ts +++ /dev/null @@ -1,182 +0,0 @@ -import type { Action, BettingState } from './bettingRound' -import { applyAction, legalActions } from './bettingRound' -import { createDealCursor, dealCards, shuffleDeck } from './deck' -import { buildDeck } from './deck' -import { compareHandScore, evaluate7 } from './handEval' -import type { HandScore } from './handEval' -import type { Rng } from './rng' -import type { Card, Seat } from './types' - -export interface HeadsUpStacks { - player: number - opponent: number -} - -export interface HandSetup { - buttonSeat: Seat // heads-up: button posts the small blind and acts first preflop - bigBlind: number - smallBlind: number - stacks: HeadsUpStacks -} - -export interface HandResult { - winner: Seat | 'split' - potAwarded: number - wentToShowdown: boolean - finalStacks: HeadsUpStacks - loserSeat: Seat | null - board: readonly Card[] - /** Both seats' hole cards, but ONLY at a real showdown — a folder's cards are never revealed. */ - revealedHoleCards: Record | null -} - -import { - initialBettingState, - other, - potFromBetting, - returnUncalledExcess, - stacksFromBetting, - STREET_ORDER, -} from './handFlowBetting' - -export { potFromBetting } from './handFlowBetting' - - -export interface HandFlowCallbacks { - /** - * Called whenever it's a seat's turn; must return one of legalActions(betting). Receives ONLY - * the acting seat's own hole cards — never the opponent's — so a decision function can never - * see cards it shouldn't, by construction rather than by discipline. - */ - decide: (betting: BettingState, seat: Seat, board: readonly Card[], ownHoleCards: readonly Card[]) => Action -} - -/** One paused decision point within a hand: who must act, and on what they may see. */ -export interface HandStep { - betting: BettingState - seat: Seat - board: readonly Card[] - ownHoleCards: readonly Card[] -} - -/** - * Runs a single hand end-to-end as a generator: posts blinds, deals hole cards, and pauses at - * every decision point (`yield`ing a HandStep) instead of calling a callback directly. The caller - * resumes it by passing the chosen Action into `.next(action)`. This lets a UI drive the hand one - * decision at a time (suspend on the player's turn, wait for a click) without making the engine - * itself async — it's suspension, not IO. - */ -export function* playHandSteps(rng: Rng, setup: HandSetup): Generator { - const deck = shuffleDeck(rng, buildDeck()) - let cursor = createDealCursor(deck) - - const dealtPlayer = dealCards(cursor, 2) - cursor = dealtPlayer.cursor - const dealtOpponent = dealCards(cursor, 2) - cursor = dealtOpponent.cursor - const holeCards: Record = { - player: dealtPlayer.cards, - opponent: dealtOpponent.cards, - } - - let board: Card[] = [] - let betting = initialBettingState(setup, 'preflop', 0, setup.stacks) - - for (const street of STREET_ORDER) { - if (street !== 'preflop') { - const settled = returnUncalledExcess(betting) - const communityCount = street === 'flop' ? 3 : 1 - const dealt = dealCards(cursor, communityCount) - cursor = dealt.cursor - board = [...board, ...dealt.cards] - betting = initialBettingState(setup, street, potFromBetting(settled), stacksFromBetting(settled)) - } - - while (legalActions(betting).length > 0) { - const action = yield { betting, seat: betting.toAct, board, ownHoleCards: holeCards[betting.toAct] } - betting = applyAction(betting, action) - } - - if (betting.seats.player.folded || betting.seats.opponent.folded) { - return resolveByFold(betting, board) - } - } - - return resolveByShowdown(betting, board, holeCards) -} - -/** - * Runs a single hand end-to-end: posts blinds, deals hole cards, runs betting rounds interleaved - * with community-card deals, and resolves the pot at fold or showdown. Thin synchronous driver - * over `playHandSteps` for callers (tests, the PC-only path) that don't need to suspend mid-hand. - */ -export const playHand = (rng: Rng, setup: HandSetup, callbacks: HandFlowCallbacks): HandResult => { - const gen = playHandSteps(rng, setup) - let step = gen.next() - while (!step.done) { - const { betting, seat, board, ownHoleCards } = step.value - step = gen.next(callbacks.decide(betting, seat, board, ownHoleCards)) - } - return step.value -} - -const resolveByFold = (betting: BettingState, board: readonly Card[]): HandResult => { - const settled = returnUncalledExcess(betting) - const pot = potFromBetting(settled) - const loserSeat: Seat = settled.seats.player.folded ? 'player' : 'opponent' - const winnerSeat = other(loserSeat) - const finalStacks: HeadsUpStacks = { - player: settled.seats.player.stack + (winnerSeat === 'player' ? pot : 0), - opponent: settled.seats.opponent.stack + (winnerSeat === 'opponent' ? pot : 0), - } - // A folder's cards are never revealed, even to show what they held. - return { winner: winnerSeat, potAwarded: pot, wentToShowdown: false, finalStacks, loserSeat, board, revealedHoleCards: null } -} - -const resolveByShowdown = ( - betting: BettingState, - board: readonly Card[], - holeCards: Record, -): HandResult => { - const settled = returnUncalledExcess(betting) - const pot = potFromBetting(settled) - const playerCards = [...holeCards.player, ...board] as [Card, Card, Card, Card, Card, Card, Card] - const opponentCards = [...holeCards.opponent, ...board] as [Card, Card, Card, Card, Card, Card, Card] - const playerScore: HandScore = evaluate7(playerCards) - const opponentScore: HandScore = evaluate7(opponentCards) - const cmp = compareHandScore(playerScore, opponentScore) - - const stacks = stacksFromBetting(settled) - if (cmp === 0) { - // Split pot: heads-up play always returns uncalled excess before showdown, so both seats' - // committed amounts are equal and the pot is provably even — no odd-chip remainder rule needed. - const half = pot / 2 - return { - winner: 'split', - potAwarded: pot, - wentToShowdown: true, - finalStacks: { - player: stacks.player + half, - opponent: stacks.opponent + half, - }, - loserSeat: null, - board, - revealedHoleCards: holeCards, - } - } - - const winnerSeat: Seat = cmp > 0 ? 'player' : 'opponent' - const loserSeat = other(winnerSeat) - return { - winner: winnerSeat, - potAwarded: pot, - wentToShowdown: true, - finalStacks: { - player: stacks.player + (winnerSeat === 'player' ? pot : 0), - opponent: stacks.opponent + (winnerSeat === 'opponent' ? pot : 0), - }, - loserSeat, - board, - revealedHoleCards: holeCards, - } -} diff --git a/poker-stakes/src/engine/handFlowBetting.ts b/poker-stakes/src/engine/handFlowBetting.ts deleted file mode 100644 index 211f0376..00000000 --- a/poker-stakes/src/engine/handFlowBetting.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { BettingState, SeatState } from './bettingRound' -import type { Seat, Street } from './types' -import type { HandSetup, HeadsUpStacks } from './handFlow' - -/** - * Betting-state helpers for handFlow, split out to stay under the 250-line cap. - */ - -export const other = (seat: Seat): Seat => (seat === 'player' ? 'opponent' : 'player') - -export const STREET_ORDER: readonly Street[] = ['preflop', 'flop', 'turn', 'river'] - -export const initialBettingState = (setup: HandSetup, street: Street, pot: number, stacks: HeadsUpStacks): BettingState => { - const nonButton = other(setup.buttonSeat) - // A seat already at 0 chips (all-in from an earlier street's blind/bet) stays all-in on every - // later street — a fresh `allIn: false` here would wrongly offer fold/check on a 0 stack. - const buttonSeat: SeatState = { - stack: stacks[setup.buttonSeat], - committed: 0, - hasActed: false, - folded: false, - allIn: stacks[setup.buttonSeat] === 0, - } - const nonButtonSeat: SeatState = { - stack: stacks[nonButton], - committed: 0, - hasActed: false, - folded: false, - allIn: stacks[nonButton] === 0, - } - - if (street === 'preflop') { - // Heads-up: button posts small blind and acts FIRST preflop. - const buttonPosted: SeatState = { ...buttonSeat, ...postBlind(buttonSeat, setup.smallBlind) } - const nonButtonPosted: SeatState = { ...nonButtonSeat, ...postBlind(nonButtonSeat, setup.bigBlind) } - return { - street, - pot, - toAct: setup.buttonSeat, - seats: { [setup.buttonSeat]: buttonPosted, [nonButton]: nonButtonPosted } as Record, - betToCall: nonButtonPosted.committed, - minRaiseSize: setup.bigBlind, - roundOver: false, - } - } - - // Postflop: non-button (big blind) acts first. - return { - street, - pot, - toAct: nonButton, - seats: { [setup.buttonSeat]: buttonSeat, [nonButton]: nonButtonSeat } as Record, - betToCall: 0, - minRaiseSize: setup.bigBlind, - roundOver: false, - } -} - -export const postBlind = (seatState: SeatState, amount: number): SeatState => { - const spend = Math.min(amount, seatState.stack) - return { stack: seatState.stack - spend, committed: spend, hasActed: false, folded: false, allIn: seatState.stack - spend === 0 } -} - -/** Returns the uncalled excess of the seat with the higher committed amount, and the state with it returned. */ -export const returnUncalledExcess = (betting: BettingState): BettingState => { - const { player, opponent } = betting.seats - if (player.committed === opponent.committed) { - return betting - } - const [higher, higherSeat, lowerCommitted] = - player.committed > opponent.committed ? [player, 'player' as Seat, opponent.committed] : [opponent, 'opponent' as Seat, player.committed] - const excess = higher.committed - lowerCommitted - return { - ...betting, - seats: { - ...betting.seats, - [higherSeat]: { ...higher, committed: higher.committed - excess, stack: higher.stack + excess }, - }, - } -} - -/** The live pot, including both seats' amounts committed on the current street (not yet swept into `betting.pot`). */ -export const potFromBetting = (betting: BettingState): number => - betting.pot + betting.seats.player.committed + betting.seats.opponent.committed - -export const stacksFromBetting = (betting: BettingState): HeadsUpStacks => ({ - player: betting.seats.player.stack, - opponent: betting.seats.opponent.stack, -}) diff --git a/poker-stakes/src/engine/interactiveHand.test.ts b/poker-stakes/src/engine/interactiveHand.test.ts deleted file mode 100644 index 43eb72bd..00000000 --- a/poker-stakes/src/engine/interactiveHand.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { legalActions } from './bettingRound' -import type { HandSetup } from './handFlow' -import { startInteractiveHand } from './interactiveHand' -import { createRng } from './rng' - -const setup = (overrides: Partial = {}): HandSetup => ({ - buttonSeat: 'player', - bigBlind: 10, - smallBlind: 5, - stacks: { player: 200, opponent: 200 }, - ...overrides, -}) - -const totalStack = (setupArg: HandSetup): number => setupArg.stacks.player + setupArg.stacks.opponent - -/** Drives an interactive hand to completion always checking/calling on the player's turns. */ -const runToCompletion = (hand: ReturnType): void => { - while (hand.status.kind === 'awaiting') { - const betting = hand.status.step.betting - const toCall = betting.betToCall - betting.seats.player.committed - hand.submit(toCall > 0 ? { type: 'call' } : { type: 'check' }) - } -} - -describe('startInteractiveHand', () => { - it('pauses on the player seat only, never surfacing the opponent as awaiting', () => { - const s = setup() - const hand = startInteractiveHand(createRng(1), createRng(1 ^ 0x9e3779b9), s, 1) - expect(hand.status.kind).toBe('awaiting') - if (hand.status.kind !== 'awaiting') { - throw new Error('unreachable: asserted above') - } - expect(hand.status.step.seat).toBe('player') - }) - - it('every offered awaiting action is legal for the current betting state', () => { - const s = setup() - const hand = startInteractiveHand(createRng(1), createRng(1 ^ 0x9e3779b9), s, 1) - while (hand.status.kind === 'awaiting') { - const { step } = hand.status - const legal = legalActions(step.betting) - const toCall = step.betting.betToCall - step.betting.seats.player.committed - const action = toCall > 0 ? { type: 'call' as const } : { type: 'check' as const } - expect(legal).toContainEqual(action) - hand.submit(action) - } - }) - - it('runs a full hand to completion via alternating player submits and internal opponent turns', () => { - const s = setup() - const hand = startInteractiveHand(createRng(1), createRng(1 ^ 0x9e3779b9), s, 1) - runToCompletion(hand) - expect(hand.status.kind).toBe('complete') - if (hand.status.kind !== 'complete') { - throw new Error('unreachable: asserted above') - } - const { result } = hand.status - expect(result.finalStacks.player + result.finalStacks.opponent).toBe(totalStack(s)) - }) - - it('resolves a preflop player fold without ever pausing again', () => { - const s = setup() - const hand = startInteractiveHand(createRng(2), createRng(2 ^ 0x9e3779b9), s, 1) - expect(hand.status.kind).toBe('awaiting') - hand.submit({ type: 'fold' }) - expect(hand.status.kind).toBe('complete') - if (hand.status.kind !== 'complete') { - throw new Error('unreachable: asserted above') - } - expect(hand.status.result.loserSeat).toBe('player') - }) - - it('throws if submit is called with no decision pending', () => { - const s = setup() - const hand = startInteractiveHand(createRng(2), createRng(2 ^ 0x9e3779b9), s, 1) - hand.submit({ type: 'fold' }) - expect(hand.status.kind).toBe('complete') - expect(() => { - hand.submit({ type: 'fold' }) - }).toThrow('no player decision is pending') - }) - - it('is deterministic for fixed deal and policy seeds', () => { - const s = setup() - const a = startInteractiveHand(createRng(1), createRng(1 ^ 0x9e3779b9), s, 1) - runToCompletion(a) - const b = startInteractiveHand(createRng(1), createRng(1 ^ 0x9e3779b9), s, 1) - runToCompletion(b) - expect(a.status).toEqual(b.status) - }) - - it('changing opponent iterations does not change the deal (dealRng and policyRng are independent)', () => { - const s = setup() - const collectBoards = (opponentIterations: number): unknown[] => { - const hand = startInteractiveHand(createRng(1), createRng(1 ^ 0x9e3779b9), s, opponentIterations) - const boards: unknown[] = [] - while (hand.status.kind === 'awaiting') { - const { step } = hand.status - boards.push(step.board) - const toCall = step.betting.betToCall - step.betting.seats.player.committed - hand.submit(toCall > 0 ? { type: 'call' } : { type: 'check' }) - } - return boards - } - // Different iteration counts consume policyRng differently; if it leaked into dealing, the - // board sequence collected at each player-decision pause would diverge between the two runs. - expect(collectBoards(1)).toEqual(collectBoards(5)) - }) -}) diff --git a/poker-stakes/src/engine/interactiveHand.ts b/poker-stakes/src/engine/interactiveHand.ts deleted file mode 100644 index 2c2664de..00000000 --- a/poker-stakes/src/engine/interactiveHand.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { Action } from './bettingRound' -import { legalActions } from './bettingRound' -import type { HandResult, HandSetup, HandStep } from './handFlow' -import { playHandSteps } from './handFlow' -import { decideAction } from './opponentPolicy' -import type { Rng } from './rng' -import type { Card } from './types' - -/** - * Exactly one of the two states holds at any time — a discriminated union instead of two - * independently-nullable fields, so "both null" / "both set" are unrepresentable rather than - * defensive-but-unreachable states a caller has to guard against. - */ -export type HandStatus = { kind: 'awaiting'; step: HandStep } | { kind: 'complete'; result: HandResult } - -export interface InteractiveHand { - readonly status: HandStatus - /** Submits the player's action for the current `awaiting` step, resolves any opponent turns, and advances. */ - submit: (action: Action) => void -} - -/** - * Drives `playHandSteps` to completion, resolving every OPPONENT decision point internally via - * `decideAction` (using its own policy RNG, separate from the dealing RNG so tuning `iterations` - * never perturbs deal order) and only ever pausing at `status.step` on the PLAYER's turn. This is - * the seam a UI drives: read `status`, call `submit(action)` on a click, repeat until `complete`. - */ -export const startInteractiveHand = ( - dealRng: Rng, - policyRng: Rng, - setup: HandSetup, - opponentIterations: number, -): InteractiveHand => { - const gen = playHandSteps(dealRng, setup) - - const advance = (next: IteratorResult): HandStatus => { - let step = next - while (!step.done) { - if (step.value.seat === 'player') { - return { kind: 'awaiting', step: step.value } - } - const { betting, board, ownHoleCards } = step.value - const legal = legalActions(betting) - const ownHole = ownHoleCards as [Card, Card] - const action = decideAction(betting, legal, ownHole, board, policyRng, opponentIterations) - step = gen.next(action) - } - return { kind: 'complete', result: step.value } - } - - let status: HandStatus = advance(gen.next()) - - return { - get status(): HandStatus { - return status - }, - submit: (action: Action): void => { - if (status.kind !== 'awaiting') { - throw new Error('startInteractiveHand: no player decision is pending') - } - status = advance(gen.next(action)) - }, - } -} diff --git a/poker-stakes/src/engine/invariants.test.ts b/poker-stakes/src/engine/invariants.test.ts deleted file mode 100644 index 1213168d..00000000 --- a/poker-stakes/src/engine/invariants.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { legalActions } from './bettingRound' -import { startInteractiveHand } from './interactiveHand' -import { createRng } from './rng' -import { applyHandResult, cashOutSession, deriveBlindSize, startSession } from './session' -import { settleSession } from '../stakes/settlement' - -const BUY_IN_CHIPS = 100 -const SEEDS = 40 -const MAX_HANDS = 400 -const POLICY_SEED_SALT = 0x9e3779b9 - -/** - * Property sweep over many seeds, complementing the scripted per-module tests: those pin specific - * scenarios, this asserts the properties that must hold for EVERY session the engine can produce. - * - * Chip conservation is the important one — a heads-up table is a closed system, so if chips are - * ever created or destroyed the stakes model is compromised (the settlement formula reads - * `finalChips` directly, so invented chips would become forgiven debt). - */ -describe('engine invariants across many seeds', () => { - it('conserves chips, keeps stacks non-negative, and always settles by the formula', () => { - const failures: string[] = [] - let totalDecisions = 0 - let sessionsEnded = 0 - - for (let seed = 1; seed <= SEEDS; seed += 1) { - let session = startSession({ buyInChips: BUY_IN_CHIPS }) - const dealRng = createRng(seed) - const policyRng = createRng(seed ^ POLICY_SEED_SALT) - const tableTotal = session.stacks.player + session.stacks.opponent - - for (let hand = 0; hand < MAX_HANDS && session.outcome === null; hand += 1) { - const { bigBlind, smallBlind } = deriveBlindSize(BUY_IN_CHIPS) - const interactive = startInteractiveHand( - dealRng, - policyRng, - { buttonSeat: session.buttonSeat, bigBlind, smallBlind, stacks: session.stacks }, - 50, - ) - - // Always take the first legal action — an arbitrary but deterministic strategy. The - // invariants below must hold regardless of how badly the player plays. - let guard = 0 - while (interactive.status.kind === 'awaiting' && guard < 200) { - const [first] = legalActions(interactive.status.step.betting) - if (first === undefined) { - break - } - interactive.submit(first) - totalDecisions += 1 - guard += 1 - } - - const status = interactive.status - if (status.kind !== 'complete') { - failures.push(`seed ${String(seed)} hand ${String(hand)}: hand never completed`) - break - } - - session = applyHandResult(session, status.result) - const sum = session.stacks.player + session.stacks.opponent - if (sum !== tableTotal) { - failures.push(`seed ${String(seed)} hand ${String(hand)}: chips ${String(sum)} != ${String(tableTotal)}`) - } - if (session.stacks.player < 0 || session.stacks.opponent < 0) { - failures.push(`seed ${String(seed)} hand ${String(hand)}: negative stack`) - } - } - - if (session.outcome !== null) { - sessionsEnded += 1 - } - const ended = session.outcome !== null ? session : cashOutSession(session) - const { outcome } = ended - if (outcome !== null) { - const debt = settleSession({ taskTypeId: 'pushups', chips: BUY_IN_CHIPS }, outcome, 'id', 0) - const expected = Math.max(0, 2 * BUY_IN_CHIPS - outcome.finalChips) - if (debt.owedUnits !== expected) { - failures.push(`seed ${String(seed)}: owed ${String(debt.owedUnits)} != ${String(expected)}`) - } - if (debt.owedUnits > 2 * BUY_IN_CHIPS) { - failures.push(`seed ${String(seed)}: owed ${String(debt.owedUnits)} exceeds the 2x cap`) - } - } - } - - // Guards against the sweep silently doing nothing (an empty loop would otherwise "pass"). - expect(totalDecisions).toBeGreaterThan(500) - expect(sessionsEnded).toBe(SEEDS) - expect(failures.slice(0, 10)).toEqual([]) - }) -}) diff --git a/poker-stakes/src/engine/opponentPolicy.test.ts b/poker-stakes/src/engine/opponentPolicy.test.ts deleted file mode 100644 index 75b25582..00000000 --- a/poker-stakes/src/engine/opponentPolicy.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { Action, BettingState, SeatState } from './bettingRound' -import { legalActions } from './bettingRound' -import { chooseAction, decideAction, estimateEquity, potOddsToCall } from './opponentPolicy' -import { createRng } from './rng' -import type { Card } from './types' - -const card = (rank: Card['rank'], suit: Card['suit']): Card => ({ rank, suit }) - -const seat = (overrides: Partial = {}): SeatState => ({ - stack: 100, - committed: 0, - hasActed: false, - folded: false, - allIn: false, - ...overrides, -}) - -const betting = (overrides: Partial = {}): BettingState => ({ - street: 'preflop', - pot: 0, - toAct: 'player', - seats: { player: seat(), opponent: seat() }, - betToCall: 0, - minRaiseSize: 10, - roundOver: false, - ...overrides, -}) - -describe('chooseAction', () => { - it('bets/raises the max size on strong equity when available', () => { - const legal: Action[] = [{ type: 'fold' }, { type: 'check' }, { type: 'bet', amount: 10 }, { type: 'bet', amount: 100 }] - expect(chooseAction(legal, 0.9, 0.2)).toEqual({ type: 'bet', amount: 100 }) - }) - - it('bets/raises the min size on moderate equity when strong-equity threshold is not met', () => { - const legal: Action[] = [{ type: 'fold' }, { type: 'check' }, { type: 'bet', amount: 10 }, { type: 'bet', amount: 100 }] - expect(chooseAction(legal, 0.6, 0.2)).toEqual({ type: 'bet', amount: 10 }) - }) - - it('calls/checks when equity clears pot odds but is below the aggressive thresholds', () => { - const legal: Action[] = [{ type: 'fold' }, { type: 'call' }] - expect(chooseAction(legal, 0.3, 0.2)).toEqual({ type: 'call' }) - }) - - it('folds when equity is below pot odds and folding is available', () => { - const legal: Action[] = [{ type: 'fold' }, { type: 'call' }] - expect(chooseAction(legal, 0.1, 0.5)).toEqual({ type: 'fold' }) - }) - - it('checks for free (no fold available, e.g. facing no bet) when equity is below thresholds', () => { - const legal: Action[] = [{ type: 'check' }] - expect(chooseAction(legal, 0.1, 0)).toEqual({ type: 'check' }) - }) - - it('falls back to the only legal action when nothing else matches', () => { - const legal: Action[] = [{ type: 'call' }] - // High equity but no aggressive option and no fold available: falls through to passive. - expect(chooseAction(legal, 0.95, 0.5)).toEqual({ type: 'call' }) - }) - - it('picks the first legal action as a last resort when only aggressive options exist above threshold gating', () => { - // Only a single bet is legal and equity/pot-odds are both very low with no fold/passive offered. - const legal: Action[] = [{ type: 'bet', amount: 10 }] - expect(chooseAction(legal, 0, 1)).toEqual({ type: 'bet', amount: 10 }) - }) - - it('checks/calls when equity is low and there is no fold option available (facing an all-in check)', () => { - // No fold offered (e.g. already all-in facing no further decision except check) — falls - // through the "fold" branch to the passive one instead. - const legal: Action[] = [{ type: 'check' }] - expect(chooseAction(legal, 0, 1)).toEqual({ type: 'check' }) - }) - - it('throws when given no legal actions at all', () => { - expect(() => chooseAction([], 0.5, 0.5)).toThrow('no legal actions to choose from') - }) -}) - -describe('potOddsToCall', () => { - it('is zero when nothing is owed', () => { - expect(potOddsToCall(betting({ betToCall: 0 }))).toBe(0) - }) - - it('computes the fraction of the resulting pot the call would represent', () => { - const s = betting({ - betToCall: 20, - pot: 0, - seats: { player: seat({ committed: 0 }), opponent: seat({ committed: 20 }) }, - }) - // toCall = 20, potAfterCall = 0 + 0 + 20 + 20 = 40, odds = 20/40 = 0.5 - expect(potOddsToCall(s)).toBe(0.5) - }) -}) - -describe('estimateEquity', () => { - it('returns a value in [0, 1]', () => { - const ownHole: [Card, Card] = [card(14, 'clubs'), card(14, 'diamonds')] - const equity = estimateEquity(ownHole, [], createRng(1), 50) - expect(equity).toBeGreaterThanOrEqual(0) - expect(equity).toBeLessThanOrEqual(1) - }) - - it('is deterministic for a fixed seed', () => { - const ownHole: [Card, Card] = [card(14, 'clubs'), card(14, 'diamonds')] - const a = estimateEquity(ownHole, [], createRng(7), 30) - const b = estimateEquity(ownHole, [], createRng(7), 30) - expect(a).toBe(b) - }) - - it('returns 0 for zero iterations', () => { - const ownHole: [Card, Card] = [card(2, 'clubs'), card(3, 'diamonds')] - expect(estimateEquity(ownHole, [], createRng(1), 0)).toBe(0) - }) - - it('rates pocket aces higher than a weak hand on average, over many samples', () => { - const strongHole: [Card, Card] = [card(14, 'clubs'), card(14, 'diamonds')] - const weakHole: [Card, Card] = [card(2, 'hearts'), card(7, 'spades')] - const strongEquity = estimateEquity(strongHole, [], createRng(3), 200) - const weakEquity = estimateEquity(weakHole, [], createRng(3), 200) - expect(strongEquity).toBeGreaterThan(weakEquity) - }) - - it('estimates equity with a partially dealt board (flop known)', () => { - const ownHole: [Card, Card] = [card(14, 'clubs'), card(13, 'clubs')] - const board: Card[] = [card(12, 'clubs'), card(11, 'clubs'), card(2, 'hearts')] - const equity = estimateEquity(ownHole, board, createRng(5), 40) - expect(equity).toBeGreaterThanOrEqual(0) - expect(equity).toBeLessThanOrEqual(1) - }) -}) - -describe('decideAction', () => { - it('always returns one of the offered legal actions', () => { - const s = betting({ betToCall: 0, minRaiseSize: 10, seats: { player: seat({ stack: 100 }), opponent: seat() } }) - const legal = legalActions(s) - const ownHole: [Card, Card] = [card(14, 'clubs'), card(14, 'diamonds')] - const action = decideAction(s, legal, ownHole, [], createRng(1), 20) - expect(legal).toContainEqual(action) - }) -}) diff --git a/poker-stakes/src/engine/opponentPolicy.ts b/poker-stakes/src/engine/opponentPolicy.ts deleted file mode 100644 index d7713688..00000000 --- a/poker-stakes/src/engine/opponentPolicy.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { Action, BettingState } from './bettingRound' -import { buildDeck, shuffleDeck } from './deck' -import { compareHandScore, evaluate7 } from './handEval' -import type { Rng } from './rng' -import type { Card } from './types' - -const cardKey = (card: Card): string => `${String(card.rank)}-${card.suit}` - -/** - * Monte Carlo equity estimate: samples `iterations` plausible completions of the unknown cards - * (opponent's hole cards drawn from this seat's perspective, plus any remaining board cards) and - * returns the fraction of samples this seat's final hand wins or ties. - * - * Only ever sees `ownHole` and the current `board` — never the real opponent's hole cards. - */ -export const estimateEquity = ( - ownHole: readonly [Card, Card], - board: readonly Card[], - rng: Rng, - iterations: number, -): number => { - const known = new Set([...ownHole, ...board].map(cardKey)) - const remaining = buildDeck().filter((card) => !known.has(cardKey(card))) - const boardCardsNeeded = 5 - board.length - - let wins = 0 - for (let i = 0; i < iterations; i += 1) { - const shuffled = shuffleDeck(rng, remaining) - const opponentHole = shuffled.slice(0, 2) as [Card, Card] - const remainingBoard = shuffled.slice(2, 2 + boardCardsNeeded) - const fullBoard = [...board, ...remainingBoard] - - const ownSeven = [...ownHole, ...fullBoard] as [Card, Card, Card, Card, Card, Card, Card] - const oppSeven = [...opponentHole, ...fullBoard] as [Card, Card, Card, Card, Card, Card, Card] - const cmp = compareHandScore(evaluate7(ownSeven), evaluate7(oppSeven)) - if (cmp >= 0) { - wins += 1 - } - } - return iterations === 0 ? 0 : wins / iterations -} - -/** - * Picks one of the legal actions using a hand-strength/pot-odds heuristic. Never constructs an - * action outside `legal` — the returned action is always a member of that list. - */ -export const chooseAction = (legal: readonly Action[], equity: number, potOdds: number): Action => { - const fold = legal.find((a) => a.type === 'fold') - const passive = legal.find((a) => a.type === 'check' || a.type === 'call') - const aggressiveActions = legal.filter((a) => a.type === 'bet' || a.type === 'raise') - const minAggressive = aggressiveActions[0] - const maxAggressive = aggressiveActions[aggressiveActions.length - 1] - - // Strong equity: bet/raise big when possible, otherwise take whatever passive/fold action is offered. - if (equity >= 0.7 && maxAggressive !== undefined) { - return maxAggressive - } - // Moderate equity: open/raise a standard (minimum) size when possible. - if (equity >= 0.55 && minAggressive !== undefined) { - return minAggressive - } - // Equity clears the price being laid: call/check rather than fold. - if (equity >= potOdds && passive !== undefined) { - return passive - } - // Equity doesn't justify continuing: fold if there's a cost to continue, else check for free. - if (fold !== undefined) { - return fold - } - if (passive !== undefined) { - return passive - } - const fallback = legal[0] - if (fallback === undefined) { - throw new Error('chooseAction: no legal actions to choose from') - } - return fallback -} - -/** Computes pot odds: the price (as equity fraction) required to profitably call the amount owed. */ -export const potOddsToCall = (betting: BettingState): number => { - const toCall = betting.betToCall - betting.seats[betting.toAct].committed - if (toCall <= 0) { - return 0 - } - const potAfterCall = betting.pot + betting.seats.player.committed + betting.seats.opponent.committed + toCall - return toCall / potAfterCall -} - -/** - * Composes estimateEquity + chooseAction into a full decision for the given betting state. - * `ownHole` must be the deciding seat's own hole cards only. - */ -export const decideAction = ( - betting: BettingState, - legal: readonly Action[], - ownHole: readonly [Card, Card], - board: readonly Card[], - rng: Rng, - iterations: number, -): Action => { - const equity = estimateEquity(ownHole, board, rng, iterations) - const potOdds = potOddsToCall(betting) - return chooseAction(legal, equity, potOdds) -} diff --git a/poker-stakes/src/engine/rng.test.ts b/poker-stakes/src/engine/rng.test.ts deleted file mode 100644 index 3867760c..00000000 --- a/poker-stakes/src/engine/rng.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { createRng, nextFloat, nextInt, pick } from './rng' - -describe('rng', () => { - it('is deterministic for a given seed', () => { - const a = createRng(123) - const b = createRng(123) - expect([nextFloat(a), nextFloat(a), nextFloat(a)]).toEqual([ - nextFloat(b), - nextFloat(b), - nextFloat(b), - ]) - }) - - it('produces different sequences for different seeds', () => { - expect(nextFloat(createRng(1))).not.toBe(nextFloat(createRng(2))) - }) - - it('stays within [0, 1)', () => { - const rng = createRng(77) - for (let i = 0; i < 200; i += 1) { - const v = nextFloat(rng) - expect(v).toBeGreaterThanOrEqual(0) - expect(v).toBeLessThan(1) - } - }) - - it('wraps negative seeds into unsigned space', () => { - expect(createRng(-1).s).toBe(0xffffffff) - }) - - it('nextInt stays within [0, max)', () => { - const rng = createRng(9) - for (let i = 0; i < 100; i += 1) { - const v = nextInt(rng, 4) - expect(v).toBeGreaterThanOrEqual(0) - expect(v).toBeLessThan(4) - } - }) - - it('pick eventually returns every element of a tuple', () => { - const rng = createRng(5) - const seen = new Set() - for (let i = 0; i < 64; i += 1) { - seen.add(pick(rng, ['a', 'b', 'c'] as const)) - } - expect(seen).toEqual(new Set(['a', 'b', 'c'])) - }) - - it('pick is deterministic', () => { - expect(pick(createRng(42), [10, 20, 30] as const)).toBe( - pick(createRng(42), [10, 20, 30] as const), - ) - }) -}) diff --git a/poker-stakes/src/engine/rng.ts b/poker-stakes/src/engine/rng.ts deleted file mode 100644 index 1c582101..00000000 --- a/poker-stakes/src/engine/rng.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** Mutable state for a mulberry32 generator. */ -export interface Rng { - s: number -} - -export const createRng = (seed: number): Rng => ({ s: seed >>> 0 }) - -/** Deterministic float in [0, 1). Advances the generator. */ -export const nextFloat = (rng: Rng): number => { - rng.s = (rng.s + 0x6d2b79f5) >>> 0 - let t = rng.s - t = Math.imul(t ^ (t >>> 15), t | 1) - t ^= t + Math.imul(t ^ (t >>> 7), t | 61) - return ((t ^ (t >>> 14)) >>> 0) / 4294967296 -} - -/** Deterministic integer in [0, maxExclusive). */ -export const nextInt = (rng: Rng, maxExclusive: number): number => - Math.floor(nextFloat(rng) * maxExclusive) - -/** Deterministic pick from a non-empty tuple. */ -export const pick = (rng: Rng, items: readonly [T, ...T[]]): T => { - const idx = nextInt(rng, items.length) - let out: T = items[0] - let i = 0 - for (const item of items) { - if (i === idx) { - out = item - } - i += 1 - } - return out -} diff --git a/poker-stakes/src/engine/session.test.ts b/poker-stakes/src/engine/session.test.ts deleted file mode 100644 index 3e9370c7..00000000 --- a/poker-stakes/src/engine/session.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { Action, BettingState } from './bettingRound' -import { legalActions } from './bettingRound' -import { createRng } from './rng' -import { cashOutSession, deriveBlindSize, playSessionHand, startSession } from './session' -import type { Seat } from './types' - -const enforceLegal = ( - decide: (betting: BettingState, seat: Seat) => Action, -): ((betting: BettingState, seat: Seat) => Action) => { - return (betting, seat) => { - const action = decide(betting, seat) - expect(legalActions(betting)).toContainEqual(action) - return action - } -} - -const alwaysCheckOrCall = enforceLegal((betting) => { - const toCall = betting.betToCall - betting.seats[betting.toAct].committed - return toCall > 0 ? { type: 'call' } : { type: 'check' } -}) - -/** Shoves all-in whenever a bet/raise is legal for the given seat, otherwise checks/calls. */ -const shoveForSeat = (targetSeat: Seat): ((betting: BettingState, seat: Seat) => Action) => - enforceLegal((betting, seat) => { - if (seat !== targetSeat) { - return alwaysCheckOrCall(betting, seat) - } - const actions = legalActions(betting) - const shove = actions.find((a) => a.type === 'bet' || a.type === 'raise') - return shove ?? alwaysCheckOrCall(betting, seat) - }) - -describe('deriveBlindSize', () => { - it('floors a tiny buy-in to the minimum viable blinds (BB=2, SB=1)', () => { - expect(deriveBlindSize(20)).toEqual({ bigBlind: 2, smallBlind: 1 }) - }) - - it('scales blinds proportionally for a large buy-in', () => { - expect(deriveBlindSize(1000)).toEqual({ bigBlind: 10, smallBlind: 5 }) - }) - - it('never produces a small blind equal to the big blind', () => { - for (const buyIn of [1, 2, 20, 99, 100, 250, 10_000]) { - const { bigBlind, smallBlind } = deriveBlindSize(buyIn) - expect(smallBlind).toBeLessThan(bigBlind) - expect(smallBlind).toBeGreaterThanOrEqual(1) - } - }) -}) - -describe('startSession', () => { - it('starts both stacks equal to the buy-in with the player on the button', () => { - const session = startSession({ buyInChips: 100 }) - expect(session.stacks).toEqual({ player: 100, opponent: 100 }) - expect(session.buttonSeat).toBe('player') - expect(session.handsPlayed).toBe(0) - expect(session.outcome).toBeNull() - }) -}) - -describe('playSessionHand', () => { - it('updates stacks, increments handsPlayed, and rotates the button', () => { - const session = startSession({ buyInChips: 200 }) - const { session: next } = playSessionHand(session, createRng(1), 200, { decide: alwaysCheckOrCall }) - expect(next.handsPlayed).toBe(1) - expect(next.buttonSeat).toBe('opponent') - expect(next.stacks.player + next.stacks.opponent).toBe(400) - }) - - it('throws when called on an already-ended session', () => { - const session = startSession({ buyInChips: 200 }) - const ended = cashOutSession(session) - expect(() => playSessionHand(ended, createRng(1), 200, { decide: alwaysCheckOrCall })).toThrow( - 'session has already ended', - ) - }) - - it('records a bust outcome when the player stack hits zero (deterministic seed)', () => { - // Seed 1: player always shoves all-in, opponent always checks/calls — player loses and busts. - const session = startSession({ buyInChips: 10 }) - const { session: after } = playSessionHand(session, createRng(1), 10, { decide: shoveForSeat('player') }) - expect(after.outcome).toEqual({ reason: 'bust', finalChips: 0 }) - expect(after.stacks.player).toBe(0) - }) - - it('records a cash-out-equivalent outcome when the opponent stack hits zero (deterministic seed)', () => { - // Seed 100: opponent always shoves all-in, player always checks/calls — opponent loses and busts. - const session = startSession({ buyInChips: 10 }) - const { session: after } = playSessionHand(session, createRng(100), 10, { decide: shoveForSeat('opponent') }) - expect(after.stacks.opponent).toBe(0) - expect(after.outcome).toEqual({ reason: 'cashOut', finalChips: after.stacks.player }) - expect(after.stacks.player).toBeGreaterThan(0) - }) - - it('conserves total chips across many hands until the session ends', () => { - let session = startSession({ buyInChips: 100 }) - let seed = 1 - while (session.outcome === null && session.handsPlayed < 30) { - const result = playSessionHand(session, createRng(seed), 100, { decide: alwaysCheckOrCall }) - session = result.session - seed += 1 - expect(session.stacks.player + session.stacks.opponent).toBe(200) - } - }) -}) - -describe('cashOutSession', () => { - it('records the current player stack as the outcome', () => { - const session = startSession({ buyInChips: 100 }) - const { session: afterHand } = playSessionHand(session, createRng(1), 100, { decide: alwaysCheckOrCall }) - const cashedOut = cashOutSession(afterHand) - expect(cashedOut.outcome).toEqual({ reason: 'cashOut', finalChips: afterHand.stacks.player }) - }) - - it('throws when called on an already-ended session', () => { - const session = startSession({ buyInChips: 100 }) - const cashedOut = cashOutSession(session) - expect(() => cashOutSession(cashedOut)).toThrow('session has already ended') - }) -}) diff --git a/poker-stakes/src/engine/session.ts b/poker-stakes/src/engine/session.ts deleted file mode 100644 index 6043c644..00000000 --- a/poker-stakes/src/engine/session.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { HandFlowCallbacks, HandResult, HeadsUpStacks } from './handFlow' -import { playHand } from './handFlow' -import type { Rng } from './rng' -import type { Seat } from './types' - -export interface SessionConfig { - buyInChips: number -} - -export type SessionEndReason = 'cashOut' | 'bust' - -export interface SessionOutcome { - reason: SessionEndReason - finalChips: number // the player's stack at the moment the session ended -} - -export interface SessionState { - stacks: HeadsUpStacks - buttonSeat: Seat // rotates each hand - handsPlayed: number - outcome: SessionOutcome | null // null while the session is still live -} - -/** Big blind derived from the buy-in; small blind is always ≥1 and strictly less than BB. */ -export const deriveBlindSize = (buyInChips: number): { bigBlind: number; smallBlind: number } => { - const bigBlind = Math.max(2, Math.floor(buyInChips / 100)) - const smallBlind = Math.floor(bigBlind / 2) - return { bigBlind, smallBlind } -} - -export const startSession = (config: SessionConfig): SessionState => ({ - stacks: { player: config.buyInChips, opponent: config.buyInChips }, - buttonSeat: 'player', - handsPlayed: 0, - outcome: null, -}) - -const other = (seat: Seat): Seat => (seat === 'player' ? 'opponent' : 'player') - -/** - * Folds a completed hand's result into the session: updates stacks, rotates the button, and - * detects bust/cash-out. Pure — shared by `playSessionHand` (the PC-only synchronous path) and - * any interactive driver that runs a hand via `playHandSteps` instead. - */ -export const applyHandResult = (session: SessionState, handResult: HandResult): SessionState => { - const nextStacks = handResult.finalStacks - // Player busting is a real debt event. Opponent busting means the player holds every chip in - // play, which settles to owing nothing under the settlement formula — model it the same as a - // cash-out (at the max chip count) rather than a special case, since no further hand is possible. - let outcome: SessionOutcome | null = null - if (nextStacks.player === 0) { - outcome = { reason: 'bust', finalChips: 0 } - } else if (nextStacks.opponent === 0) { - outcome = { reason: 'cashOut', finalChips: nextStacks.player } - } - - return { - stacks: nextStacks, - buttonSeat: other(session.buttonSeat), - handsPlayed: session.handsPlayed + 1, - outcome, - } -} - -/** Plays one hand, updates stacks, rotates the button, and detects bust. No-op if already ended. */ -export const playSessionHand = ( - session: SessionState, - rng: Rng, - buyInChips: number, - callbacks: HandFlowCallbacks, -): { session: SessionState; handResult: HandResult } => { - if (session.outcome !== null) { - throw new Error('playSessionHand: session has already ended') - } - const { bigBlind, smallBlind } = deriveBlindSize(buyInChips) - const handResult = playHand( - rng, - { buttonSeat: session.buttonSeat, bigBlind, smallBlind, stacks: session.stacks }, - callbacks, - ) - - return { session: applyHandResult(session, handResult), handResult } -} - -/** Ends a live session by cash-out, recording the player's current chip count. */ -export const cashOutSession = (session: SessionState): SessionState => { - if (session.outcome !== null) { - throw new Error('cashOutSession: session has already ended') - } - return { ...session, outcome: { reason: 'cashOut', finalChips: session.stacks.player } } -} diff --git a/poker-stakes/src/engine/types.ts b/poker-stakes/src/engine/types.ts deleted file mode 100644 index 2470bfd0..00000000 --- a/poker-stakes/src/engine/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** Card ranks, stored high (Ace = 14). Wheel straights are handled explicitly in handEval. */ -export type Rank = 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 - -export type Suit = 'clubs' | 'diamonds' | 'hearts' | 'spades' - -export interface Card { - rank: Rank - suit: Suit -} - -export type Seat = 'player' | 'opponent' - -export type Street = 'preflop' | 'flop' | 'turn' | 'river' | 'showdown' diff --git a/poker-stakes/src/main.test.tsx b/poker-stakes/src/main.test.tsx deleted file mode 100644 index bfdc538b..00000000 --- a/poker-stakes/src/main.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { act } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { vi } from 'vitest' - -beforeEach(() => { - vi.resetModules() - document.body.innerHTML = '' -}) - -afterEach(() => { - vi.unstubAllGlobals() -}) - -describe('main', () => { - it('refuses to boot without a #root element', async () => { - await expect(import('./main')).rejects.toThrow('root element not found') - }) - - it('mounts the app into #root', async () => { - document.body.innerHTML = '
' - await act(async () => { - await import('./main') - }) - // Asserts the mount happened, not the whole app's copy — App has its own tests for that. - expect(document.querySelector('#root')?.textContent).toContain('Poker Stakes') - }) -}) diff --git a/poker-stakes/src/main.tsx b/poker-stakes/src/main.tsx deleted file mode 100644 index 54e6318e..00000000 --- a/poker-stakes/src/main.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { App } from './App' -import './styles.css' - -const rootElement = document.getElementById('root') -if (rootElement === null) { - throw new Error('root element not found') -} - -createRoot(rootElement).render( - - - , -) diff --git a/poker-stakes/src/stakes/settlement.test.ts b/poker-stakes/src/stakes/settlement.test.ts deleted file mode 100644 index 27d2720a..00000000 --- a/poker-stakes/src/stakes/settlement.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { SessionOutcome } from '../engine/session' -import { settleSession } from './settlement' -import type { BuyIn } from './types' - -const buyIn: BuyIn = { taskTypeId: 'pushups', chips: 100 } - -describe('settleSession', () => { - const cases: { name: string; outcome: SessionOutcome; owedUnits: number }[] = [ - { name: 'doubled up', outcome: { reason: 'cashOut', finalChips: 200 }, owedUnits: 0 }, - { name: 'partial win', outcome: { reason: 'cashOut', finalChips: 150 }, owedUnits: 50 }, - { name: 'break-even', outcome: { reason: 'cashOut', finalChips: 100 }, owedUnits: 100 }, - { name: 'partial loss', outcome: { reason: 'cashOut', finalChips: 50 }, owedUnits: 150 }, - { name: 'bust to zero', outcome: { reason: 'bust', finalChips: 0 }, owedUnits: 200 }, - ] - - it.each(cases)('$name: owes $owedUnits units', ({ outcome, owedUnits }) => { - const debt = settleSession(buyIn, outcome, 'debt-1', 12345) - expect(debt.owedUnits).toBe(owedUnits) - }) - - it('carries the task type, reason, id, and settledAt through unchanged', () => { - const outcome: SessionOutcome = { reason: 'bust', finalChips: 0 } - const debt = settleSession(buyIn, outcome, 'debt-42', 999) - expect(debt).toEqual({ - id: 'debt-42', - taskTypeId: 'pushups', - owedUnits: 200, - reason: 'bust', - settledAt: 999, - paidUnits: 0, - }) - }) -}) diff --git a/poker-stakes/src/stakes/settlement.ts b/poker-stakes/src/stakes/settlement.ts deleted file mode 100644 index 3b81f71b..00000000 --- a/poker-stakes/src/stakes/settlement.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { SessionOutcome } from '../engine/session' -import type { BuyIn, SettledDebt } from './types' - -/** - * Converts a finished session's outcome into a debt record. `owedUnits` scales inversely with - * how well the player did: `2 × buyIn − finalChips`, floored at 0. Bust (finalChips === 0) needs - * no special case — it's just this formula's natural output (2 × buyIn, the cap) for that input. - */ -export const settleSession = (buyIn: BuyIn, outcome: SessionOutcome, id: string, settledAt: number): SettledDebt => ({ - id, - taskTypeId: buyIn.taskTypeId, - owedUnits: Math.max(0, 2 * buyIn.chips - outcome.finalChips), - reason: outcome.reason, - settledAt, - paidUnits: 0, -}) diff --git a/poker-stakes/src/stakes/storage.test.ts b/poker-stakes/src/stakes/storage.test.ts deleted file mode 100644 index 45d65a90..00000000 --- a/poker-stakes/src/stakes/storage.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { loadDebts, saveDebts } from './storage' -import type { SettledDebt } from './types' - -const debt: SettledDebt = { - id: 'd1', - taskTypeId: 'pushups', - owedUnits: 100, - reason: 'cashOut', - settledAt: 1, - paidUnits: 0, -} - -const makeStorage = (initial: Record = {}): Storage => { - const store = new Map(Object.entries(initial)) - return { - getItem: (key: string): string | null => store.get(key) ?? null, - setItem: (key: string, value: string): void => { - store.set(key, value) - }, - removeItem: (key: string): void => { - store.delete(key) - }, - clear: (): void => { - store.clear() - }, - key: (): null => null, - get length(): number { - return store.size - }, - } -} - -const throwingStorage = (): Storage => ({ - getItem: (): never => { - throw new Error('denied') - }, - setItem: (): never => { - throw new Error('denied') - }, - removeItem: (): undefined => undefined, - clear: (): undefined => undefined, - key: (): null => null, - length: 0, -}) - -describe('loadDebts', () => { - it('returns an empty ok list when nothing is stored yet', () => { - expect(loadDebts(makeStorage())).toEqual({ status: 'ok', debts: [] }) - }) - - it('returns the stored debts when the shape is valid', () => { - const storage = makeStorage({ 'poker-stakes:settledDebts': JSON.stringify([debt]) }) - expect(loadDebts(storage)).toEqual({ status: 'ok', debts: [debt] }) - }) - - it('is unreadable when getItem throws (storage disabled/denied)', () => { - expect(loadDebts(throwingStorage())).toEqual({ status: 'unreadable' }) - }) - - it('is unreadable on malformed JSON', () => { - const storage = makeStorage({ 'poker-stakes:settledDebts': '{not json' }) - expect(loadDebts(storage)).toEqual({ status: 'unreadable' }) - }) - - it('is unreadable when the stored value is not an array', () => { - const storage = makeStorage({ 'poker-stakes:settledDebts': JSON.stringify({ not: 'an array' }) }) - expect(loadDebts(storage)).toEqual({ status: 'unreadable' }) - }) - - it('is unreadable when an array entry is missing required fields', () => { - const storage = makeStorage({ 'poker-stakes:settledDebts': JSON.stringify([{ id: 'd1' }]) }) - expect(loadDebts(storage)).toEqual({ status: 'unreadable' }) - }) - - // Each field's guard is asserted in ISOLATION — all other fields stay valid. The - // missing-required-fields case above drops five fields at once, so it would still trip some - // other check if any single guard were deleted, leaving that guard effectively untested. - it.each([ - ['id', { id: 42 }], - ['taskTypeId', { taskTypeId: null }], - ['owedUnits', { owedUnits: 'ten' }], - ['reason', { reason: 'quit' }], - ['settledAt', { settledAt: '2026-01-01' }], - ['paidUnits', { paidUnits: [] }], - ])('is unreadable when only %s has the wrong type', (_field, override) => { - const storage = makeStorage({ 'poker-stakes:settledDebts': JSON.stringify([{ ...debt, ...override }]) }) - expect(loadDebts(storage)).toEqual({ status: 'unreadable' }) - }) - - it.each(['id', 'taskTypeId', 'owedUnits', 'reason', 'settledAt', 'paidUnits'])( - 'is unreadable when only %s is missing', - (field) => { - // Built by filtering rather than `delete`, which lint forbids on computed keys. - const partial = Object.fromEntries(Object.entries(debt).filter(([key]) => key !== field)) - const storage = makeStorage({ 'poker-stakes:settledDebts': JSON.stringify([partial]) }) - expect(loadDebts(storage)).toEqual({ status: 'unreadable' }) - }, - ) - - it('is unreadable when an array entry has an invalid reason value', () => { - const bad = { ...debt, reason: 'quit' } - const storage = makeStorage({ 'poker-stakes:settledDebts': JSON.stringify([bad]) }) - expect(loadDebts(storage)).toEqual({ status: 'unreadable' }) - }) - - it('is unreadable when an array entry is null', () => { - const storage = makeStorage({ 'poker-stakes:settledDebts': JSON.stringify([null]) }) - expect(loadDebts(storage)).toEqual({ status: 'unreadable' }) - }) -}) - -describe('saveDebts', () => { - it('round-trips through loadDebts', () => { - const storage = makeStorage() - saveDebts(storage, [debt]) - expect(loadDebts(storage)).toEqual({ status: 'ok', debts: [debt] }) - }) -}) diff --git a/poker-stakes/src/stakes/storage.ts b/poker-stakes/src/stakes/storage.ts deleted file mode 100644 index 4383c77b..00000000 --- a/poker-stakes/src/stakes/storage.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { SettledDebt } from './types' - -/** Exported so tests can seed a scripted ledger without duplicating the key string. */ -export const DEBTS_STORAGE_KEY = 'poker-stakes:settledDebts' - -export type DebtLoad = { status: 'ok'; debts: SettledDebt[] } | { status: 'unreadable' } - -const isSettledDebt = (value: unknown): value is SettledDebt => { - if (typeof value !== 'object' || value === null) { - return false - } - const v = value as Record - return ( - typeof v.id === 'string' && - typeof v.taskTypeId === 'string' && - typeof v.owedUnits === 'number' && - (v.reason === 'cashOut' || v.reason === 'bust') && - typeof v.settledAt === 'number' && - typeof v.paidUnits === 'number' - ) -} - -/** Fail-closed: any read/parse/shape error yields 'unreadable', never a silently-empty list. */ -export const loadDebts = (storage: Storage): DebtLoad => { - let raw: string | null - try { - raw = storage.getItem(DEBTS_STORAGE_KEY) - } catch { - return { status: 'unreadable' } - } - if (raw === null) { - return { status: 'ok', debts: [] } - } - let parsed: unknown - try { - parsed = JSON.parse(raw) - } catch { - return { status: 'unreadable' } - } - if (!Array.isArray(parsed) || !parsed.every(isSettledDebt)) { - return { status: 'unreadable' } - } - return { status: 'ok', debts: parsed } -} - -export const saveDebts = (storage: Storage, debts: SettledDebt[]): void => { - storage.setItem(DEBTS_STORAGE_KEY, JSON.stringify(debts)) -} diff --git a/poker-stakes/src/stakes/taskTypes.test.ts b/poker-stakes/src/stakes/taskTypes.test.ts deleted file mode 100644 index 68d8e604..00000000 --- a/poker-stakes/src/stakes/taskTypes.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { TASK_TYPES } from './taskTypes' - -describe('TASK_TYPES', () => { - it('is non-empty', () => { - expect(TASK_TYPES.length).toBeGreaterThan(0) - }) - - it('has unique ids', () => { - const ids = TASK_TYPES.map((t) => t.id) - expect(new Set(ids).size).toBe(ids.length) - }) - - it('gives every entry a non-empty id and label', () => { - for (const taskType of TASK_TYPES) { - expect(taskType.id.length).toBeGreaterThan(0) - expect(taskType.label.length).toBeGreaterThan(0) - } - }) -}) diff --git a/poker-stakes/src/stakes/taskTypes.ts b/poker-stakes/src/stakes/taskTypes.ts deleted file mode 100644 index d963b991..00000000 --- a/poker-stakes/src/stakes/taskTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { TaskType } from './types' - -export const TASK_TYPES: readonly [TaskType, ...TaskType[]] = [ - { id: 'pushups', label: 'Pushups' }, - { id: 'leetcode', label: 'LeetCode problems' }, - { id: 'anki', label: 'Anki reviews' }, -] diff --git a/poker-stakes/src/stakes/types.ts b/poker-stakes/src/stakes/types.ts deleted file mode 100644 index 9d9527fb..00000000 --- a/poker-stakes/src/stakes/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { SessionEndReason } from '../engine/session' - -export interface TaskType { - id: string - label: string -} - -export interface BuyIn { - taskTypeId: string - chips: number -} - -export interface SettledDebt { - id: string - taskTypeId: string - owedUnits: number // 2×buyIn − finalChips, floored at 0 - reason: SessionEndReason // display-only; never branches settlement math - settledAt: number - paidUnits: number // partial progress; starts at 0, incremented via UI -} diff --git a/poker-stakes/src/styles.css b/poker-stakes/src/styles.css deleted file mode 100644 index 36302e42..00000000 --- a/poker-stakes/src/styles.css +++ /dev/null @@ -1,215 +0,0 @@ -:root { - --ink: #211d1b; - --ink-raised-1: #2b2624; - --ink-raised-2: #38312e; - --line-dark: #463e3a; - --text-on-dark: #ecebe9; - --muted-on-dark: #aaa09a; - - --accent: #b8862e; - --success: #8a9a3c; - --warning: #e0a63c; - --danger: #e2585f; - --on-fill: #211d1b; - - --font-body: -apple-system, "Segoe UI", Roboto, system-ui, sans-serif; - --font-data: ui-monospace, "Cascadia Mono", "JetBrains Mono", Menlo, monospace; - - --space-xs: 4px; - --space-sm: 8px; - --space-md: 16px; - --space-lg: 24px; - --space-xl: 32px; - --space-xxl: 48px; - - --radius-sm: 8px; - --radius-md: 12px; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - background: var(--ink); - color: var(--text-on-dark); - font-family: var(--font-body); - min-height: 100vh; -} - -button { - font-family: inherit; - font-size: 16px; -} - -.frame { - max-width: 960px; - margin: 0 auto; - padding: var(--space-lg) var(--space-md) var(--space-xxl); - display: flex; - flex-direction: column; - gap: var(--space-lg); -} - -.masthead h1 { - font-size: 24px; - letter-spacing: -0.1px; - margin: 0 0 var(--space-xs); -} - -.masthead p { - margin: 0; - color: var(--muted-on-dark); - font-size: 14px; -} - -.panel { - background: var(--ink-raised-1); - border: 1px solid var(--line-dark); - border-radius: var(--radius-md); - padding: var(--space-lg); -} - -.button { - border: 1px solid var(--line-dark); - border-radius: var(--radius-sm); - padding: 12px 24px; - background: var(--ink-raised-2); - color: var(--text-on-dark); - cursor: pointer; -} - -.button:hover:not(:disabled) { - background: var(--line-dark); -} - -.button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.button--primary { - background: var(--accent); - color: var(--on-fill); - border-color: var(--accent); -} - -.button--danger { - background: var(--danger); - color: var(--on-fill); - border-color: var(--danger); -} - -.field { - display: flex; - flex-direction: column; - gap: var(--space-xs); - margin-bottom: var(--space-md); -} - -.field label { - font-size: 14px; - letter-spacing: 0.05em; - color: var(--muted-on-dark); -} - -.field input, -.field select { - font-family: inherit; - font-size: 16px; - padding: 12px; - border-radius: var(--radius-sm); - border: 1px solid var(--line-dark); - background: var(--ink-raised-2); - color: var(--text-on-dark); -} - -.table { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-md); -} - -.table__seat { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-sm); -} - -.table__cards { - display: flex; - gap: var(--space-xs); -} - -.table__pot { - font-family: var(--font-data); - font-size: 20px; - color: var(--accent); -} - -.card { - width: 44px; - height: 62px; - border-radius: var(--radius-sm); - background: var(--text-on-dark); - color: var(--ink); - display: flex; - align-items: center; - justify-content: center; - font-family: var(--font-data); - font-weight: 600; - font-size: 16px; - border: 1px solid var(--line-dark); -} - -.card--red { - color: var(--danger); -} - -.card--back { - background: var(--ink-raised-2); - color: var(--muted-on-dark); -} - -.controls { - display: flex; - gap: var(--space-sm); - flex-wrap: wrap; - justify-content: center; -} - -.debt-list { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: var(--space-sm); -} - -.debt-list li { - border: 1px solid var(--line-dark); - border-radius: var(--radius-sm); - padding: var(--space-md); - display: flex; - flex-direction: column; - gap: var(--space-xs); -} - -.log-progress { - display: flex; - gap: var(--space-sm); - align-items: center; -} - -.log-progress input { - width: 96px; -} - -.hand-log { - font-size: 14px; - color: var(--muted-on-dark); -} diff --git a/poker-stakes/src/test/handEvalFixtures.ts b/poker-stakes/src/test/handEvalFixtures.ts deleted file mode 100644 index ece72bcb..00000000 --- a/poker-stakes/src/test/handEvalFixtures.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Card } from '../engine/types' - -/** Shared helpers for the handEval test files, split to stay under the 250-line cap. */ - -export const card = (rank: Card['rank'], suit: Card['suit']): Card => ({ rank, suit }) - - -export const five = (cards: readonly Card[]): [Card, Card, Card, Card, Card] => { - if (cards.length !== 5) { - throw new Error('five: expected exactly 5 cards') - } - const [a, b, c, d, e] = cards - return [a, b, c, d, e] as [Card, Card, Card, Card, Card] -} - -export const seven = (cards: readonly Card[]): [Card, Card, Card, Card, Card, Card, Card] => { - if (cards.length !== 7) { - throw new Error('seven: expected exactly 7 cards') - } - const [a, b, c, d, e, f, g] = cards - return [a, b, c, d, e, f, g] as [Card, Card, Card, Card, Card, Card, Card] -} - -/** Generates all C(52,5) 5-card combinations from a 52-card deck as index tuples. */ -export function* combinations5(length: number): Generator { - for (let a = 0; a < length; a += 1) { - for (let b = a + 1; b < length; b += 1) { - for (let c = b + 1; c < length; c += 1) { - for (let d = c + 1; d < length; d += 1) { - for (let e = d + 1; e < length; e += 1) { - yield [a, b, c, d, e] - } - } - } - } - } -} diff --git a/poker-stakes/src/test/setup.ts b/poker-stakes/src/test/setup.ts deleted file mode 100644 index e87ca3f0..00000000 --- a/poker-stakes/src/test/setup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import '@testing-library/jest-dom/vitest' -import { cleanup } from '@testing-library/react' -import { afterEach } from 'vitest' - -afterEach(() => { - cleanup() -}) diff --git a/poker-stakes/src/test/useGameFixtures.tsx b/poker-stakes/src/test/useGameFixtures.tsx deleted file mode 100644 index 0748c0d5..00000000 --- a/poker-stakes/src/test/useGameFixtures.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { StrictMode } from 'react' -import type { ReactElement, ReactNode } from 'react' -import type { SettledDebt } from '../stakes/types' -import type { GameState } from '../ui/useGame' - -/** - * Shared fixtures for the useGame test files. - * - * useGame.test.tsx was split by describe-block to stay under the 250-line cap; - * these helpers are the part all three files need. - */ - -export const debt = (overrides: Partial = {}): SettledDebt => ({ - id: 'd1', - taskTypeId: 'pushups', - owedUnits: 100, - reason: 'cashOut', - settledAt: 1, - paidUnits: 0, - ...overrides, -}) - -export const initialState: GameState = { - debtLoad: { status: 'ok', debts: [] }, - buyIn: null, - session: null, - handPhase: null, -} - -export const wrapper = ({ children }: { children: ReactNode }): ReactElement => {children} - -/** An in-memory Storage stand-in, so tests never touch real localStorage. */ -export const makeStorage = (): Storage => { - const store = new Map() - return { - getItem: (key: string): string | null => store.get(key) ?? null, - setItem: (key: string, value: string): void => { - store.set(key, value) - }, - removeItem: (key: string): void => { - store.delete(key) - }, - clear: (): void => { - store.clear() - }, - key: (): null => null, - get length(): number { - return store.size - }, - } -} - -/** Storage that throws on every read/write, for the fail-closed paths. */ -export const unreadableStorage = (): Storage => ({ - getItem: (): never => { - throw new Error('denied') - }, - setItem: (): never => { - throw new Error('denied') - }, - removeItem: (): undefined => undefined, - clear: (): undefined => undefined, - key: (): null => null, - length: 0, -}) diff --git a/poker-stakes/src/ui/BettingControls.test.tsx b/poker-stakes/src/ui/BettingControls.test.tsx deleted file mode 100644 index 39ee78ce..00000000 --- a/poker-stakes/src/ui/BettingControls.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { describe, expect, it, vi } from 'vitest' -import type { BettingState, SeatState } from '../engine/bettingRound' -import { BettingControls } from './BettingControls' - -const seat = (overrides: Partial = {}): SeatState => ({ - stack: 100, - committed: 0, - hasActed: false, - folded: false, - allIn: false, - ...overrides, -}) - -const betting = (overrides: Partial = {}): BettingState => ({ - street: 'preflop', - pot: 0, - toAct: 'player', - seats: { player: seat(), opponent: seat() }, - betToCall: 0, - minRaiseSize: 10, - roundOver: false, - ...overrides, -}) - -describe('BettingControls', () => { - it('renders fold/check/bet when nothing is owed', () => { - render() - expect(screen.getByText('Fold')).toBeInTheDocument() - expect(screen.getByText('Check')).toBeInTheDocument() - expect(screen.getByText('Bet 10')).toBeInTheDocument() - expect(screen.getByText('Bet 100')).toBeInTheDocument() - expect(screen.queryByText('Call')).not.toBeInTheDocument() - }) - - it('renders fold/call/raise when a bet is owed', () => { - const state = betting({ betToCall: 20, seats: { player: seat(), opponent: seat({ committed: 20 }) } }) - render() - expect(screen.getByText('Fold')).toBeInTheDocument() - expect(screen.getByText('Call')).toBeInTheDocument() - expect(screen.getByText('Raise 30')).toBeInTheDocument() - }) - - it('renders no bet/raise option when the acting seat cannot cover more than a call', () => { - const state = betting({ - betToCall: 20, - seats: { player: seat({ stack: 20 }), opponent: seat({ committed: 20 }) }, - }) - render() - expect(screen.queryByText(/Raise/)).not.toBeInTheDocument() - }) - - it('calls onAction with the exact action when a button is clicked', async () => { - const user = userEvent.setup() - const onAction = vi.fn() - render() - await user.click(screen.getByText('Check')) - expect(onAction).toHaveBeenCalledWith({ type: 'check' }) - await user.click(screen.getByText('Bet 10')) - expect(onAction).toHaveBeenCalledWith({ type: 'bet', amount: 10 }) - }) -}) diff --git a/poker-stakes/src/ui/BettingControls.tsx b/poker-stakes/src/ui/BettingControls.tsx deleted file mode 100644 index 3dd9f73d..00000000 --- a/poker-stakes/src/ui/BettingControls.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { ReactElement } from 'react' -import type { Action, BettingState } from '../engine/bettingRound' -import { legalActions } from '../engine/bettingRound' - -const ACTION_LABELS: Record = { - fold: 'Fold', - check: 'Check', - call: 'Call', - bet: 'Bet', - raise: 'Raise', -} - -const actionKey = (action: Action): string => - action.type === 'bet' || action.type === 'raise' ? `${action.type}-${String(action.amount)}` : action.type - -const actionLabel = (action: Action): string => - action.type === 'bet' || action.type === 'raise' - ? `${ACTION_LABELS[action.type]} ${String(action.amount)}` - : ACTION_LABELS[action.type] - -interface BettingControlsProps { - betting: BettingState - onAction: (action: Action) => void -} - -/** Renders exactly the legal-action set for the current betting state — illegal actions are never representable. */ -export const BettingControls = ({ betting, onAction }: BettingControlsProps): ReactElement => { - const actions = legalActions(betting) - return ( -
- {actions.map((action) => ( - - ))} -
- ) -} diff --git a/poker-stakes/src/ui/BuyInScreen.test.tsx b/poker-stakes/src/ui/BuyInScreen.test.tsx deleted file mode 100644 index 0a8540b4..00000000 --- a/poker-stakes/src/ui/BuyInScreen.test.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { describe, expect, it, vi } from 'vitest' -import type { DebtLoad } from '../stakes/storage' -import type { SettledDebt } from '../stakes/types' -import { BuyInScreen } from './BuyInScreen' - -const debt = (overrides: Partial = {}): SettledDebt => ({ - id: 'd1', - taskTypeId: 'pushups', - owedUnits: 100, - reason: 'cashOut', - settledAt: 1, - paidUnits: 0, - ...overrides, -}) - -describe('BuyInScreen', () => { - it('renders the buy-in form when there is no outstanding debt', () => { - render() - expect(screen.getByText('Sit down')).toBeInTheDocument() - expect(screen.queryByText(/outstanding debt/)).not.toBeInTheDocument() - }) - - it('submits the selected task type and chip count', async () => { - const user = userEvent.setup() - const onBuyIn = vi.fn() - render() - const chipInput = screen.getByLabelText('Chip count') - await user.clear(chipInput) - await user.type(chipInput, '250') - await user.click(screen.getByText('Sit down')) - expect(onBuyIn).toHaveBeenCalledWith({ taskTypeId: 'pushups', chips: 250 }) - }) - - // Passes because native `min={1}` validation aborts the submit, NOT because the JS guard ran — - // the cleared-field test below is what actually exercises the guard. Keep both. - it('does not submit a non-positive or fractional chip count', async () => { - const user = userEvent.setup() - const onBuyIn = vi.fn() - render() - const chipInput = screen.getByLabelText('Chip count') - await user.clear(chipInput) - await user.type(chipInput, '0') - await user.click(screen.getByText('Sit down')) - expect(onBuyIn).not.toHaveBeenCalled() - }) - - // An empty field is the only non-positive value that actually reaches the JS guard: the - // browser's native `min={1}` constraint aborts submission for a typed "-5"/"0" before the - // handler fires, whereas an empty input is valid HTML-wise and parses to `Number('') === 0`. - it('does not submit when the chip count is cleared to empty', async () => { - const user = userEvent.setup() - const onBuyIn = vi.fn() - render() - await user.clear(screen.getByLabelText('Chip count')) - await user.click(screen.getByText('Sit down')) - expect(onBuyIn).not.toHaveBeenCalled() - }) - - it('buys in with the task type picked from the select', async () => { - const user = userEvent.setup() - const onBuyIn = vi.fn() - render() - await user.selectOptions(screen.getByLabelText('Task type'), 'leetcode') - await user.click(screen.getByText('Sit down')) - expect(onBuyIn).toHaveBeenCalledWith({ taskTypeId: 'leetcode', chips: 100 }) - }) - - it('replaces the buy-in form with outstanding debts when a debt is unpaid (hard gate)', () => { - const debtLoad: DebtLoad = { status: 'ok', debts: [debt({ owedUnits: 100, paidUnits: 40 })] } - render() - expect(screen.queryByText('Sit down')).not.toBeInTheDocument() - expect(screen.getByText(/outstanding debt/)).toBeInTheDocument() - expect(screen.getByText('Pushups: 40 / 100 done')).toBeInTheDocument() - }) - - it('does not list a fully paid debt among the outstanding ones', () => { - const debtLoad: DebtLoad = { - status: 'ok', - debts: [debt({ id: 'paid', owedUnits: 50, paidUnits: 50 }), debt({ id: 'owed', owedUnits: 100, paidUnits: 0 })], - } - render() - expect(screen.queryByText('Pushups: 50 / 50 done')).not.toBeInTheDocument() - expect(screen.getByText('Pushups: 0 / 100 done')).toBeInTheDocument() - }) - - it('a doubled-up debt (owedUnits 0) does not block a new buy-in', () => { - const debtLoad: DebtLoad = { status: 'ok', debts: [debt({ owedUnits: 0, paidUnits: 0 })] } - render() - expect(screen.getByText('Sit down')).toBeInTheDocument() - }) - - it('logs progress against an outstanding debt via the log-progress control', async () => { - const user = userEvent.setup() - const onLogDebtProgress = vi.fn() - const debtLoad: DebtLoad = { status: 'ok', debts: [debt({ owedUnits: 100, paidUnits: 40 })] } - render() - const input = screen.getByLabelText('Log units done for Pushups') - await user.type(input, '20') - await user.click(screen.getByText('Log progress')) - expect(onLogDebtProgress).toHaveBeenCalledWith('d1', 20) - }) - - // Passes because native `min={1}` validation aborts the submit, NOT because the JS guard ran — - // the empty-field test below is what actually exercises the guard. Keep both. - it('does not log a non-positive or fractional progress amount', async () => { - const user = userEvent.setup() - const onLogDebtProgress = vi.fn() - const debtLoad: DebtLoad = { status: 'ok', debts: [debt({ owedUnits: 100, paidUnits: 40 })] } - render() - const input = screen.getByLabelText('Log units done for Pushups') - await user.type(input, '-5') - await user.click(screen.getByText('Log progress')) - expect(onLogDebtProgress).not.toHaveBeenCalled() - }) - - // Same reasoning as the chip-count empty case: the empty field is what reaches the guard. - it('does not log progress when the units field is left empty', async () => { - const user = userEvent.setup() - const onLogDebtProgress = vi.fn() - const debtLoad: DebtLoad = { status: 'ok', debts: [debt({ owedUnits: 100, paidUnits: 40 })] } - render() - await user.click(screen.getByText('Log progress')) - expect(onLogDebtProgress).not.toHaveBeenCalled() - }) - - it('blocks with a fail-closed message when storage was unreadable', () => { - render() - expect(screen.queryByText('Sit down')).not.toBeInTheDocument() - expect(screen.getByText(/could not be read/)).toBeInTheDocument() - }) - - it('falls back to the raw taskTypeId label if it is not in the fixed list', () => { - const debtLoad: DebtLoad = { status: 'ok', debts: [debt({ taskTypeId: 'unknown-type', owedUnits: 10, paidUnits: 0 })] } - render() - expect(screen.getByText('unknown-type: 0 / 10 done')).toBeInTheDocument() - }) -}) diff --git a/poker-stakes/src/ui/BuyInScreen.tsx b/poker-stakes/src/ui/BuyInScreen.tsx deleted file mode 100644 index 74b929f6..00000000 --- a/poker-stakes/src/ui/BuyInScreen.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useState } from 'react' -import type { ChangeEvent, ReactElement, SubmitEvent } from 'react' -import { TASK_TYPES } from '../stakes/taskTypes' -import type { BuyIn } from '../stakes/types' -import type { DebtLoad } from '../stakes/storage' -import { OutstandingDebtRow } from './OutstandingDebtRow' -import { hasOutstandingDebt } from './useGame' - -interface BuyInScreenProps { - debtLoad: DebtLoad - onBuyIn: (buyIn: BuyIn) => void - onLogDebtProgress: (debtId: string, units: number) => void -} - -/** - * Hard gate, not a soft warning: while any debt is outstanding (or storage is unreadable, per the - * fail-closed rule), the buy-in form is replaced entirely by the outstanding-debt list and a - * log-progress control — matches the same gate `useGame.buyIn` enforces internally. - */ -export const BuyInScreen = ({ debtLoad, onBuyIn, onLogDebtProgress }: BuyInScreenProps): ReactElement => { - const [taskTypeId, setTaskTypeId] = useState(TASK_TYPES[0].id) - const [chips, setChips] = useState('100') - - if (debtLoad.status === 'unreadable') { - return ( -
-

Your debt history could not be read. Buying in is blocked until this is resolved.

-
- ) - } - - if (hasOutstandingDebt(debtLoad)) { - const outstanding = debtLoad.debts.filter((d) => d.paidUnits < d.owedUnits) - return ( -
-

You have outstanding debt. Pay it off before buying into a new session.

-
    - {outstanding.map((debt) => ( - - ))} -
-
- ) - } - - const submit = (event: SubmitEvent): void => { - event.preventDefault() - const parsedChips = Number(chips) - if (!Number.isInteger(parsedChips) || parsedChips <= 0) { - return - } - onBuyIn({ taskTypeId, chips: parsedChips }) - } - - return ( -
-
- - -
-
- - ) => { - setChips(event.target.value) - }} - /> -
- - - ) -} diff --git a/poker-stakes/src/ui/Card.test.tsx b/poker-stakes/src/ui/Card.test.tsx deleted file mode 100644 index 8ec223ba..00000000 --- a/poker-stakes/src/ui/Card.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import type { Card as CardModel } from '../engine/types' -import { Card, CardBack } from './Card' - -describe('Card', () => { - it('renders a black-suit card without the red modifier', () => { - const card: CardModel = { rank: 14, suit: 'spades' } - render() - const el = screen.getByLabelText('A of spades') - expect(el.className).not.toContain('card--red') - expect(el.textContent).toBe('A♠') - }) - - it('renders a red-suit card with the red modifier', () => { - const card: CardModel = { rank: 10, suit: 'hearts' } - render() - const el = screen.getByLabelText('10 of hearts') - expect(el.className).toContain('card--red') - expect(el.textContent).toBe('10♥') - }) - - it('renders every rank label correctly', () => { - const ranks: { rank: CardModel['rank']; label: string }[] = [ - { rank: 2, label: '2' }, - { rank: 9, label: '9' }, - { rank: 10, label: '10' }, - { rank: 11, label: 'J' }, - { rank: 12, label: 'Q' }, - { rank: 13, label: 'K' }, - { rank: 14, label: 'A' }, - ] - for (const { rank, label } of ranks) { - const { unmount } = render() - expect(screen.getByText(`${label}♣`)).toBeInTheDocument() - unmount() - } - }) -}) - -describe('CardBack', () => { - it('renders a face-down placeholder', () => { - render() - expect(screen.getByLabelText('face-down card')).toBeInTheDocument() - }) -}) diff --git a/poker-stakes/src/ui/Card.tsx b/poker-stakes/src/ui/Card.tsx deleted file mode 100644 index a2e6dfbe..00000000 --- a/poker-stakes/src/ui/Card.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type { ReactElement } from 'react' -import type { Card as CardModel } from '../engine/types' - -const RANK_LABELS: Record = { - 2: '2', - 3: '3', - 4: '4', - 5: '5', - 6: '6', - 7: '7', - 8: '8', - 9: '9', - 10: '10', - 11: 'J', - 12: 'Q', - 13: 'K', - 14: 'A', -} - -const SUIT_SYMBOLS: Record = { - clubs: '♣', - diamonds: '♦', - hearts: '♥', - spades: '♠', -} - -const RED_SUITS: ReadonlySet = new Set(['diamonds', 'hearts']) - -interface CardProps { - card: CardModel -} - -export const Card = ({ card }: CardProps): ReactElement => { - const isRed = RED_SUITS.has(card.suit) - return ( -
- {RANK_LABELS[card.rank]} - {SUIT_SYMBOLS[card.suit]} -
- ) -} - -export const CardBack = (): ReactElement => ( -
- ? -
-) diff --git a/poker-stakes/src/ui/CashOutPanel.test.tsx b/poker-stakes/src/ui/CashOutPanel.test.tsx deleted file mode 100644 index c0f51467..00000000 --- a/poker-stakes/src/ui/CashOutPanel.test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { describe, expect, it, vi } from 'vitest' -import type { HandStep } from '../engine/handFlow' -import type { SessionState } from '../engine/session' -import type { BuyIn } from '../stakes/types' -import type { HandPhase } from './useGame' -import { CashOutPanel } from './CashOutPanel' - -const session = (playerStack: number): SessionState => ({ - stacks: { player: playerStack, opponent: 200 }, - buttonSeat: 'player', - handsPlayed: 1, - outcome: null, -}) - -const buyIn: BuyIn = { taskTypeId: 'pushups', chips: 100 } - -describe('CashOutPanel', () => { - it('renders nothing while a hand is in progress (mirrors the mid-hand cashOut refusal)', () => { - const awaitingPhase: HandPhase = { kind: 'awaiting', step: {} as HandStep } - const { container } = render( - , - ) - expect(container.textContent).toBe('') - }) - - it('renders nothing when handPhase is null (no live session yet)', () => { - const { container } = render() - expect(container.textContent).toBe('') - }) - - it('shows a zero owed preview when the player has doubled their buy-in', () => { - const phase: HandPhase = { kind: 'betweenHands', lastResult: null } - render() - expect(screen.getByText(/owe 0 units/)).toBeInTheDocument() - }) - - it('shows the correct owed preview for a partial loss', () => { - const phase: HandPhase = { kind: 'betweenHands', lastResult: null } - render() - // owed = max(0, 2*100 - 50) = 150 - expect(screen.getByText(/owe 150 units/)).toBeInTheDocument() - }) - - it('calls onCashOut when the leave-table button is clicked', async () => { - const user = userEvent.setup() - const onCashOut = vi.fn() - const phase: HandPhase = { kind: 'betweenHands', lastResult: null } - render() - await user.click(screen.getByText('Leave table')) - expect(onCashOut).toHaveBeenCalledOnce() - }) -}) diff --git a/poker-stakes/src/ui/CashOutPanel.tsx b/poker-stakes/src/ui/CashOutPanel.tsx deleted file mode 100644 index 598adcd3..00000000 --- a/poker-stakes/src/ui/CashOutPanel.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { ReactElement } from 'react' -import type { SessionState } from '../engine/session' -import type { BuyIn } from '../stakes/types' -import type { HandPhase } from './useGame' - -interface CashOutPanelProps { - session: SessionState - buyIn: BuyIn - handPhase: HandPhase | null - onCashOut: () => void -} - -/** - * Visible only between hands — mid-hand `session.stacks` is a stale pre-hand snapshot, and - * `useGame.cashOut` itself refuses mid-hand (the settlement-exploit fix), so this mirrors that - * restriction rather than showing a button that would silently do nothing when clicked. - */ -export const CashOutPanel = ({ session, buyIn, handPhase, onCashOut }: CashOutPanelProps): ReactElement | null => { - if (handPhase?.kind !== 'betweenHands') { - return null - } - const owedIfCashedOutNow = Math.max(0, 2 * buyIn.chips - session.stacks.player) - return ( -
-

- Current stack: {session.stacks.player} chips. Cashing out now would owe {owedIfCashedOutNow} units. -

- -
- ) -} diff --git a/poker-stakes/src/ui/DebtHistoryPanel.test.tsx b/poker-stakes/src/ui/DebtHistoryPanel.test.tsx deleted file mode 100644 index 32089c88..00000000 --- a/poker-stakes/src/ui/DebtHistoryPanel.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { describe, expect, it, vi } from 'vitest' -import type { DebtLoad } from '../stakes/storage' -import type { SettledDebt } from '../stakes/types' -import { DebtHistoryPanel } from './DebtHistoryPanel' - -const debt = (overrides: Partial = {}): SettledDebt => ({ - id: 'd1', - taskTypeId: 'pushups', - owedUnits: 100, - reason: 'cashOut', - settledAt: 1, - paidUnits: 0, - ...overrides, -}) - -describe('DebtHistoryPanel', () => { - it('says so when nothing has been settled yet', () => { - render() - expect(screen.getByText('No debts settled yet.')).toBeInTheDocument() - }) - - it('reports unreadable storage instead of an empty ledger (fail-closed)', () => { - render() - expect(screen.getByText('Your debt history could not be read.')).toBeInTheDocument() - expect(screen.queryByText('No debts settled yet.')).not.toBeInTheDocument() - }) - - it('gives an outstanding debt the log-progress control', async () => { - const user = userEvent.setup() - const onLogDebtProgress = vi.fn() - const debtLoad: DebtLoad = { status: 'ok', debts: [debt({ owedUnits: 100, paidUnits: 40 })] } - render() - expect(screen.getByText('Pushups: 40 / 100 done')).toBeInTheDocument() - await user.type(screen.getByLabelText('Log units done for Pushups'), '10') - await user.click(screen.getByText('Log progress')) - expect(onLogDebtProgress).toHaveBeenCalledWith('d1', 10) - }) - - it('renders a cleared debt as read-only history with its reason', () => { - const debtLoad: DebtLoad = { status: 'ok', debts: [debt({ owedUnits: 50, paidUnits: 50, reason: 'bust' })] } - render() - expect(screen.getByText('Pushups: 50 owed (bust) — cleared')).toBeInTheDocument() - expect(screen.queryByText('Log progress')).not.toBeInTheDocument() - }) - - it('labels a cash-out reason distinctly from a bust', () => { - const debtLoad: DebtLoad = { status: 'ok', debts: [debt({ owedUnits: 50, paidUnits: 50, reason: 'cashOut' })] } - render() - expect(screen.getByText('Pushups: 50 owed (cashed out) — cleared')).toBeInTheDocument() - }) - - // A doubled-up session settles to owedUnits 0, which is already cleared at settle time. - it('treats a zero-owed debt as cleared', () => { - const debtLoad: DebtLoad = { status: 'ok', debts: [debt({ owedUnits: 0, paidUnits: 0 })] } - render() - expect(screen.getByText('Pushups: 0 owed (cashed out) — cleared')).toBeInTheDocument() - }) - - it('lists debts newest first regardless of stored order', () => { - const debtLoad: DebtLoad = { - status: 'ok', - debts: [ - debt({ id: 'old', taskTypeId: 'anki', owedUnits: 10, paidUnits: 10, settledAt: 100 }), - debt({ id: 'new', taskTypeId: 'leetcode', owedUnits: 20, paidUnits: 20, settledAt: 900 }), - ], - } - render() - const rows = screen.getAllByRole('listitem').map((li) => li.textContent) - expect(rows[0]).toContain('LeetCode problems') - expect(rows[1]).toContain('Anki reviews') - }) -}) diff --git a/poker-stakes/src/ui/DebtHistoryPanel.tsx b/poker-stakes/src/ui/DebtHistoryPanel.tsx deleted file mode 100644 index ac4ac5f8..00000000 --- a/poker-stakes/src/ui/DebtHistoryPanel.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { ReactElement } from 'react' -import type { DebtLoad } from '../stakes/storage' -import type { SettledDebt } from '../stakes/types' -import { OutstandingDebtRow } from './OutstandingDebtRow' -import { taskLabel } from './taskLabel' - -interface DebtHistoryPanelProps { - debtLoad: DebtLoad - onLogDebtProgress: (debtId: string, units: number) => void -} - -const reasonLabel = (reason: SettledDebt['reason']): string => (reason === 'bust' ? 'bust' : 'cashed out') - -/** Cleared debts are read-only history — there is no control to un-pay one. */ -const ClearedDebtRow = ({ debt }: { debt: SettledDebt }): ReactElement => ( -
  • - - {taskLabel(debt.taskTypeId)}: {debt.owedUnits} owed ({reasonLabel(debt.reason)}) — cleared - -
  • -) - -/** - * The durable ledger, separate from any single session: every `SettledDebt` ever recorded, newest - * first. Outstanding entries carry the same log-progress control `BuyInScreen`'s gate shows (the - * shared `OutstandingDebtRow`), so progress can be logged from either place with identical rules. - */ -export const DebtHistoryPanel = ({ debtLoad, onLogDebtProgress }: DebtHistoryPanelProps): ReactElement => { - if (debtLoad.status === 'unreadable') { - return ( -
    -

    Debt history

    -

    Your debt history could not be read.

    -
    - ) - } - - if (debtLoad.debts.length === 0) { - return ( -
    -

    Debt history

    -

    No debts settled yet.

    -
    - ) - } - - const newestFirst = [...debtLoad.debts].sort((a, b) => b.settledAt - a.settledAt) - - return ( -
    -

    Debt history

    -
      - {newestFirst.map((debt) => - debt.paidUnits < debt.owedUnits ? ( - - ) : ( - - ), - )} -
    -
    - ) -} diff --git a/poker-stakes/src/ui/HandLog.test.tsx b/poker-stakes/src/ui/HandLog.test.tsx deleted file mode 100644 index e263dd8c..00000000 --- a/poker-stakes/src/ui/HandLog.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import type { HandResult } from '../engine/handFlow' -import { HandLog } from './HandLog' - -const result = (overrides: Partial = {}): HandResult => ({ - winner: 'player', - potAwarded: 40, - wentToShowdown: true, - finalStacks: { player: 240, opponent: 160 }, - loserSeat: 'opponent', - board: [], - revealedHoleCards: null, - ...overrides, -}) - -describe('HandLog', () => { - it('renders nothing before any hand has completed', () => { - const { container } = render() - expect(container.textContent).toBe('') - }) - - it('summarizes a player win at showdown', () => { - render() - expect(screen.getByText('You won at showdown — 40 chips.')).toBeInTheDocument() - }) - - it('summarizes an opponent win by fold', () => { - render() - expect(screen.getByText('Opponent won by fold — 15 chips.')).toBeInTheDocument() - }) - - it('summarizes a split pot', () => { - render() - expect(screen.getByText('Split pot at showdown — 60 chips divided evenly.')).toBeInTheDocument() - }) -}) diff --git a/poker-stakes/src/ui/HandLog.tsx b/poker-stakes/src/ui/HandLog.tsx deleted file mode 100644 index aaf9a8f3..00000000 --- a/poker-stakes/src/ui/HandLog.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { ReactElement } from 'react' -import type { HandResult } from '../engine/handFlow' - -interface HandLogProps { - lastResult: HandResult | null -} - -const summarize = (result: HandResult): string => { - const outcome = result.wentToShowdown ? 'at showdown' : 'by fold' - const potAwarded = String(result.potAwarded) - if (result.winner === 'split') { - return `Split pot ${outcome} — ${potAwarded} chips divided evenly.` - } - const winnerLabel = result.winner === 'player' ? 'You won' : 'Opponent won' - return `${winnerLabel} ${outcome} — ${potAwarded} chips.` -} - -/** Shows the most recently completed hand's outcome — nothing until the first hand resolves. */ -export const HandLog = ({ lastResult }: HandLogProps): ReactElement | null => { - if (lastResult === null) { - return null - } - return

    {summarize(lastResult)}

    -} diff --git a/poker-stakes/src/ui/OutstandingDebtRow.test.tsx b/poker-stakes/src/ui/OutstandingDebtRow.test.tsx deleted file mode 100644 index b8d297fc..00000000 --- a/poker-stakes/src/ui/OutstandingDebtRow.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { describe, expect, it, vi } from 'vitest' -import type { SettledDebt } from '../stakes/types' -import { OutstandingDebtRow } from './OutstandingDebtRow' - -const debt = (overrides: Partial = {}): SettledDebt => ({ - id: 'd1', - taskTypeId: 'pushups', - owedUnits: 100, - reason: 'cashOut', - settledAt: 1, - paidUnits: 40, - ...overrides, -}) - -const renderRow = (overrides: Partial = {}): ReturnType => { - const onLogDebtProgress = vi.fn() - render( -
      - -
    , - ) - return onLogDebtProgress -} - -describe('OutstandingDebtRow', () => { - it('shows progress against the owed amount', () => { - renderRow() - expect(screen.getByText('Pushups: 40 / 100 done')).toBeInTheDocument() - }) - - it('logs a positive integer of units and clears the field afterwards', async () => { - const user = userEvent.setup() - const onLogDebtProgress = renderRow() - const input = screen.getByLabelText('Log units done for Pushups') - await user.type(input, '20') - await user.click(screen.getByText('Log progress')) - expect(onLogDebtProgress).toHaveBeenCalledWith('d1', 20) - expect(input).toHaveValue(null) - }) - - // Empty is the only non-positive value that reaches the JS guard — native `min={1}` validation - // aborts submission for a typed "-5" or "0" before the handler runs. - it('does not log when the units field is empty', async () => { - const user = userEvent.setup() - const onLogDebtProgress = renderRow() - await user.click(screen.getByText('Log progress')) - expect(onLogDebtProgress).not.toHaveBeenCalled() - }) - - it('does not log a fractional amount', async () => { - const user = userEvent.setup() - const onLogDebtProgress = renderRow() - await user.type(screen.getByLabelText('Log units done for Pushups'), '2.5') - await user.click(screen.getByText('Log progress')) - expect(onLogDebtProgress).not.toHaveBeenCalled() - }) - - it('labels the input with the raw id when the task type is unknown', () => { - renderRow({ taskTypeId: 'unknown-type' }) - expect(screen.getByLabelText('Log units done for unknown-type')).toBeInTheDocument() - }) -}) diff --git a/poker-stakes/src/ui/OutstandingDebtRow.tsx b/poker-stakes/src/ui/OutstandingDebtRow.tsx deleted file mode 100644 index 8f4d5519..00000000 --- a/poker-stakes/src/ui/OutstandingDebtRow.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useState } from 'react' -import type { ChangeEvent, ReactElement, SubmitEvent } from 'react' -import type { SettledDebt } from '../stakes/types' -import { taskLabel } from './taskLabel' - -interface OutstandingDebtRowProps { - debt: SettledDebt - onLogDebtProgress: (debtId: string, units: number) => void -} - -/** - * Shared by `BuyInScreen` (the hard gate) and `DebtHistoryPanel` (the ledger) so the two can never - * drift apart — the plan's "log N units done" control is the only way `paidUnits` advances, and - * there is deliberately no mark-paid checkbox to short-circuit it. - */ -export const OutstandingDebtRow = ({ debt, onLogDebtProgress }: OutstandingDebtRowProps): ReactElement => { - const [units, setUnits] = useState('') - - const submit = (event: SubmitEvent): void => { - event.preventDefault() - const parsed = Number(units) - if (!Number.isInteger(parsed) || parsed <= 0) { - return - } - onLogDebtProgress(debt.id, parsed) - setUnits('') - } - - return ( -
  • - - {taskLabel(debt.taskTypeId)}: {debt.paidUnits} / {debt.owedUnits} done - -
    - ) => { - setUnits(event.target.value) - }} - aria-label={`Log units done for ${taskLabel(debt.taskTypeId)}`} - /> - - -
  • - ) -} diff --git a/poker-stakes/src/ui/Table.test.tsx b/poker-stakes/src/ui/Table.test.tsx deleted file mode 100644 index c780e870..00000000 --- a/poker-stakes/src/ui/Table.test.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { startInteractiveHand } from '../engine/interactiveHand' -import { createRng } from '../engine/rng' -import type { SessionState } from '../engine/session' -import type { HandPhase } from './useGame' -import { Table } from './Table' - -const session: SessionState = { - stacks: { player: 200, opponent: 200 }, - buttonSeat: 'player', - handsPlayed: 0, - outcome: null, -} - -describe('Table', () => { - it('shows the player their own hole cards and hides the opponent behind card backs mid-hand', () => { - const hand = startInteractiveHand( - createRng(1), - createRng(1 ^ 0x9e3779b9), - { buttonSeat: 'player', bigBlind: 10, smallBlind: 5, stacks: { player: 200, opponent: 200 } }, - 1, - ) - if (hand.status.kind !== 'awaiting') { - throw new Error('expected an awaiting hand for this seed') - } - const handPhase: HandPhase = { kind: 'awaiting', step: hand.status.step } - render(
    ) - expect(screen.getAllByLabelText(/face-down card/)).toHaveLength(2) - // Two own hole cards render as real Card elements, not face-down. - const allCards = screen.getAllByLabelText(/ of /) - expect(allCards.length).toBeGreaterThanOrEqual(2) - }) - - it('hides the opponent when betweenHands after a fold (no reveal)', () => { - const handPhase: HandPhase = { - kind: 'betweenHands', - lastResult: { - winner: 'opponent', - potAwarded: 15, - wentToShowdown: false, - finalStacks: { player: 190, opponent: 210 }, - loserSeat: 'player', - board: [], - revealedHoleCards: null, - }, - } - render(
    ) - expect(screen.queryByLabelText(/face-down card/)).not.toBeInTheDocument() - expect(screen.queryByLabelText(/ of /)).not.toBeInTheDocument() - }) - - it('reveals both hole cards at a showdown result while betweenHands', () => { - const handPhase: HandPhase = { - kind: 'betweenHands', - lastResult: { - winner: 'player', - potAwarded: 40, - wentToShowdown: true, - finalStacks: { player: 220, opponent: 180 }, - loserSeat: 'opponent', - board: [ - { rank: 2, suit: 'clubs' }, - { rank: 5, suit: 'diamonds' }, - { rank: 9, suit: 'hearts' }, - ], - revealedHoleCards: { - player: [ - { rank: 14, suit: 'spades' }, - { rank: 14, suit: 'clubs' }, - ], - opponent: [ - { rank: 3, suit: 'hearts' }, - { rank: 4, suit: 'spades' }, - ], - }, - }, - } - render(
    ) - // The opponent's revealed hand. - expect(screen.getByLabelText('3 of hearts')).toBeInTheDocument() - expect(screen.getByLabelText('4 of spades')).toBeInTheDocument() - // ...and your OWN hand, which must stay visible after the hand resolves — otherwise a - // showdown shows the opponent's cards and the board while your seat sits empty and you - // cannot see what you won or lost with. This is what "both" in the test name means. - expect(screen.getByLabelText('A of spades')).toBeInTheDocument() - expect(screen.getByLabelText('A of clubs')).toBeInTheDocument() - // Board cards render too. - expect(screen.getByLabelText('9 of hearts')).toBeInTheDocument() - }) - - it('leaves your seat empty after a fold, since no hand is revealed then', () => { - const handPhase: HandPhase = { - kind: 'betweenHands', - lastResult: { - winner: 'opponent', - potAwarded: 15, - wentToShowdown: false, - finalStacks: { player: 190, opponent: 210 }, - loserSeat: 'player', - board: [], - revealedHoleCards: null, - }, - } - render(
    ) - expect(screen.queryByLabelText(/ of /)).not.toBeInTheDocument() - }) - - it('reads stacks and pot=0 from session when handPhase is null (pre-first-hand transient)', () => { - render(
    ) - expect(screen.getByText('Pot: 0')).toBeInTheDocument() - expect(screen.getByText('You — 200 chips')).toBeInTheDocument() - expect(screen.getByText('Opponent — 200 chips')).toBeInTheDocument() - }) -}) diff --git a/poker-stakes/src/ui/Table.tsx b/poker-stakes/src/ui/Table.tsx deleted file mode 100644 index 8907e15d..00000000 --- a/poker-stakes/src/ui/Table.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { ReactElement } from 'react' -import { potFromBetting } from '../engine/handFlow' -import type { SessionState } from '../engine/session' -import type { Card as CardModel } from '../engine/types' -import type { HandPhase } from './useGame' -import { Card, CardBack } from './Card' - -interface TableProps { - session: SessionState - handPhase: HandPhase | null -} - -/** Stable per-hand key: a hand never contains a duplicate card, so rank+suit uniquely identifies one. */ -const cardKey = (card: CardModel): string => `${String(card.rank)}-${card.suit}` - -/** - * Reads stacks/pot from the live source of truth for the current moment: `handPhase.step.betting` - * mid-hand (which reflects THIS hand's blinds/bets — `session.stacks` is still the pre-hand - * snapshot then), and `session.stacks` between hands (pot is 0, no betting state exists). - */ -export const Table = ({ session, handPhase }: TableProps): ReactElement => { - const isAwaiting = handPhase?.kind === 'awaiting' - const betting = isAwaiting ? handPhase.step.betting : null - const stacks = betting === null ? session.stacks : { player: betting.seats.player.stack, opponent: betting.seats.opponent.stack } - const pot = betting === null ? 0 : potFromBetting(betting) - const board = isAwaiting ? handPhase.step.board : (handPhase?.kind === 'betweenHands' ? handPhase.lastResult?.board : undefined) ?? [] - const revealed = handPhase?.kind === 'betweenHands' ? (handPhase.lastResult?.revealedHoleCards ?? null) : null - const revealedOpponentCards = revealed?.opponent ?? null - // Your own hand stays visible after the hand resolves, not just while a decision is pending — - // otherwise a showdown shows the opponent's cards and the board while your seat sits empty, and - // you cannot see what you actually won or lost with. - const playerHoleCards = isAwaiting ? handPhase.step.ownHoleCards : (revealed?.player ?? null) - - return ( -
    -
    - Opponent — {stacks.opponent} chips -
    - {revealedOpponentCards !== null - ? revealedOpponentCards.map((card) => ) - : isAwaiting && ( - <> - - - - )} -
    -
    - -
    Pot: {pot}
    - -
    - {board.map((card) => ( - - ))} -
    - -
    -
    - {playerHoleCards?.map((card) => )} -
    - You — {stacks.player} chips -
    -
    - ) -} diff --git a/poker-stakes/src/ui/gameState.ts b/poker-stakes/src/ui/gameState.ts deleted file mode 100644 index a0e69056..00000000 --- a/poker-stakes/src/ui/gameState.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { HandResult, HandStep } from '../engine/handFlow' -import type { SessionState } from '../engine/session' -import type { DebtLoad } from '../stakes/storage' -import type { BuyIn, SettledDebt } from '../stakes/types' -/** - * Only meaningful while `session !== null`. `betweenHands` is a real, UI-visible pause point — no - * hand is in progress, `session.stacks` is trustworthy, and `cashOut` is only ever legal here. A - * hand result stays visible (`lastResult`) until the next hand actually starts, so HandLog has - * something to render. This is deliberately NOT auto-advanced past by `submitPlayerAction` — see - * `dealHand` below for why cash-out mid-hand would be a settlement exploit if it were. - */ -export type HandPhase = { kind: 'awaiting'; step: HandStep } | { kind: 'betweenHands'; lastResult: HandResult | null } - -export interface GameState { - debtLoad: DebtLoad - buyIn: BuyIn | null - session: SessionState | null - handPhase: HandPhase | null -} - -type GameAction = - | { type: 'buyIn'; buyIn: BuyIn; session: SessionState } - | { type: 'handPhaseChanged'; session: SessionState; handPhase: HandPhase } - | { type: 'cashedOut'; session: SessionState } - | { type: 'debtSettled'; debts: SettledDebt[] } - | { type: 'debtProgressLogged'; debts: SettledDebt[] } - -export const initialGameState: GameState = { - debtLoad: { status: 'ok', debts: [] }, - buyIn: null, - session: null, - handPhase: null, -} - -/** - * Pure state transitions. All randomness/ids/clock reads happen in the hook, never in here. - * `GameAction` is exhaustively handled — TS errors on a missing case instead of a silent - * fallthrough, which is why there is no `default` branch. - */ -export const gameReducer = (state: GameState, action: GameAction): GameState => { - switch (action.type) { - case 'buyIn': - return { ...state, buyIn: action.buyIn, session: action.session, handPhase: null } - case 'handPhaseChanged': - return { ...state, session: action.session, handPhase: action.handPhase } - case 'cashedOut': - return { ...state, session: action.session } - case 'debtSettled': - return { - ...state, - debtLoad: { status: 'ok', debts: action.debts }, - buyIn: null, - session: null, - handPhase: null, - } - case 'debtProgressLogged': - return { ...state, debtLoad: { status: 'ok', debts: action.debts } } - } -} - -/** An unpaid debt (owedUnits not yet fully paid) blocks starting a new session — a hard gate. */ -export const hasOutstandingDebt = (debtLoad: DebtLoad): boolean => - debtLoad.status === 'unreadable' || debtLoad.debts.some((d) => d.paidUnits < d.owedUnits) diff --git a/poker-stakes/src/ui/taskLabel.test.ts b/poker-stakes/src/ui/taskLabel.test.ts deleted file mode 100644 index 120a7efc..00000000 --- a/poker-stakes/src/ui/taskLabel.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { taskLabel } from './taskLabel' - -describe('taskLabel', () => { - it('maps a known task type id to its label', () => { - expect(taskLabel('pushups')).toBe('Pushups') - }) - - it('falls back to the raw id for an unknown task type', () => { - expect(taskLabel('unknown-type')).toBe('unknown-type') - }) -}) diff --git a/poker-stakes/src/ui/taskLabel.ts b/poker-stakes/src/ui/taskLabel.ts deleted file mode 100644 index d4319ce1..00000000 --- a/poker-stakes/src/ui/taskLabel.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { TASK_TYPES } from '../stakes/taskTypes' - -/** Falls back to the raw id so a debt recorded under a since-removed task type still renders. */ -export const taskLabel = (taskTypeId: string): string => - TASK_TYPES.find((t) => t.id === taskTypeId)?.label ?? taskTypeId diff --git a/poker-stakes/src/ui/useGame.debt.test.tsx b/poker-stakes/src/ui/useGame.debt.test.tsx deleted file mode 100644 index 6fa4b7a9..00000000 --- a/poker-stakes/src/ui/useGame.debt.test.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { act, renderHook } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { hasOutstandingDebt, useGame } from './useGame' -import { makeStorage, wrapper } from '../test/useGameFixtures' - -describe('useGame: debt settlement and persistence', () => { - it('a zero-player-decision hand that the all-in player survives lands in betweenHands, not settled', () => { - // Seed 1, 4-chip buy-in, always folding when a decision exists: by hand 3 (handsPlayed=2) the - // player is down to 1 chip and posts it as the small blind — all-in, zero decision points. - // The all-in player wins/ties that hand and ends above 0, so the session must survive into - // `betweenHands` rather than settling — the sibling branch to the bust case above. - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 4 }) - }) - let guard = 0 - while (result.current.state.session !== null && result.current.state.session.handsPlayed < 3 && guard < 10) { - const phase = result.current.state.handPhase - if (phase?.kind === 'betweenHands') { - act(() => { - result.current.dealHand() - }) - } else if (phase?.kind === 'awaiting') { - act(() => { - result.current.submitPlayerAction({ type: 'fold' }) - }) - } else { - break - } - guard += 1 - } - expect(guard).toBeLessThan(10) - expect(result.current.state.session).not.toBeNull() - expect(result.current.state.session?.handsPlayed).toBe(3) - expect(result.current.state.handPhase?.kind).toBe('betweenHands') - expect(result.current.state.session?.stacks.player).toBeGreaterThan(0) - }) - - it('logDebtProgress with an unknown debt id leaves the debt list unchanged', () => { - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 200 }) - }) - act(() => { - result.current.submitPlayerAction({ type: 'fold' }) - }) - act(() => { - result.current.cashOut() - }) - const before = result.current.state.debtLoad.status === 'ok' ? result.current.state.debtLoad.debts : [] - act(() => { - result.current.logDebtProgress('no-such-id', 10) - }) - const after = result.current.state.debtLoad.status === 'ok' ? result.current.state.debtLoad.debts : [] - expect(after).toEqual(before) - }) - - it('cashing out between hands (never played a hand to showdown) settles a debt equal to the full buy-in', () => { - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 200 }) - }) - act(() => { - result.current.submitPlayerAction({ type: 'fold' }) - }) - expect(result.current.state.handPhase?.kind).toBe('betweenHands') - act(() => { - result.current.cashOut() - }) - expect(result.current.state.session).toBeNull() - expect(result.current.state.debtLoad.status).toBe('ok') - const debts = result.current.state.debtLoad.status === 'ok' ? result.current.state.debtLoad.debts : [] - expect(debts).toHaveLength(1) - expect(debts[0]?.reason).toBe('cashOut') - }) - - it('an unpaid debt persists to storage and blocks a subsequent load', () => { - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 200 }) - }) - act(() => { - result.current.submitPlayerAction({ type: 'fold' }) - }) - act(() => { - result.current.cashOut() - }) - const { result: second } = renderHook(() => useGame(storage, 2), { wrapper }) - expect(hasOutstandingDebt(second.current.state.debtLoad)).toBe(true) - }) - - it('logDebtProgress clamps paidUnits to owedUnits and persists', () => { - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 200 }) - }) - act(() => { - result.current.submitPlayerAction({ type: 'fold' }) - }) - act(() => { - result.current.cashOut() - }) - const debtId = result.current.state.debtLoad.status === 'ok' ? result.current.state.debtLoad.debts[0]?.id : undefined - if (debtId === undefined) { - throw new Error('expected a settled debt') - } - act(() => { - result.current.logDebtProgress(debtId, 500) - }) - const debts = result.current.state.debtLoad.status === 'ok' ? result.current.state.debtLoad.debts : [] - expect(debts[0]?.paidUnits).toBe(debts[0]?.owedUnits) - - const { result: reloaded } = renderHook(() => useGame(storage, 3), { wrapper }) - expect(hasOutstandingDebt(reloaded.current.state.debtLoad)).toBe(false) - }) - - it('bust ends the session with a debt of exactly 2×buyIn', () => { - const storage = makeStorage() - // Seed 4 at a 20-chip buy-in, always checking/calling as the player, deterministically busts - // the player within a handful of hands (found by probing seeds 1-10; every other seed either - // busts with the same owedUnits=40 result or ends in the opponent-bust/cashOut branch covered - // above). Deals explicitly via dealHand() between hands, matching the new no-auto-continue API. - const { result } = renderHook(() => useGame(storage, 4), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 20 }) - }) - let guard = 0 - while (result.current.state.session !== null && guard < 20) { - const phase = result.current.state.handPhase - if (phase?.kind === 'betweenHands') { - act(() => { - result.current.dealHand() - }) - } else if (phase?.kind === 'awaiting') { - const { betting } = phase.step - const toCall = betting.betToCall - betting.seats.player.committed - act(() => { - result.current.submitPlayerAction(toCall > 0 ? { type: 'call' } : { type: 'check' }) - }) - } else { - break - } - guard += 1 - } - expect(guard).toBeLessThan(20) - expect(result.current.state.session).toBeNull() - const debts = result.current.state.debtLoad.status === 'ok' ? result.current.state.debtLoad.debts : [] - expect(debts).toHaveLength(1) - expect(debts[0]).toMatchObject({ owedUnits: 40, reason: 'bust' }) - }) -}) diff --git a/poker-stakes/src/ui/useGame.reducer.test.tsx b/poker-stakes/src/ui/useGame.reducer.test.tsx deleted file mode 100644 index 4f498063..00000000 --- a/poker-stakes/src/ui/useGame.reducer.test.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { gameReducer, hasOutstandingDebt } from './useGame' -import type { GameState } from './useGame' -import { debt, initialState } from '../test/useGameFixtures' - -describe('hasOutstandingDebt', () => { - it('blocks when storage was unreadable', () => { - expect(hasOutstandingDebt({ status: 'unreadable' })).toBe(true) - }) - - it('does not block an empty debt list', () => { - expect(hasOutstandingDebt({ status: 'ok', debts: [] })).toBe(false) - }) - - it('does not block a fully paid debt', () => { - expect(hasOutstandingDebt({ status: 'ok', debts: [debt({ owedUnits: 50, paidUnits: 50 })] })).toBe(false) - }) - - it('does not block a debt that owes zero units (doubled-up cash-out)', () => { - expect(hasOutstandingDebt({ status: 'ok', debts: [debt({ owedUnits: 0, paidUnits: 0 })] })).toBe(false) - }) - - it('blocks a partially paid debt', () => { - expect(hasOutstandingDebt({ status: 'ok', debts: [debt({ owedUnits: 100, paidUnits: 40 })] })).toBe(true) - }) -}) - -describe('gameReducer', () => { - it('buyIn sets buyIn/session and clears handPhase', () => { - const session: GameState['session'] = { - stacks: { player: 100, opponent: 100 }, - buttonSeat: 'player', - handsPlayed: 0, - outcome: null, - } - const next = gameReducer(initialState, { - type: 'buyIn', - buyIn: { taskTypeId: 'pushups', chips: 100 }, - session, - }) - expect(next).toEqual({ - debtLoad: { status: 'ok', debts: [] }, - buyIn: { taskTypeId: 'pushups', chips: 100 }, - session, - handPhase: null, - }) - }) - - it('debtSettled clears the live session and resets to the buy-in gate', () => { - const mid: GameState = { - ...initialState, - buyIn: { taskTypeId: 'pushups', chips: 100 }, - session: { - stacks: { player: 0, opponent: 200 }, - buttonSeat: 'player', - handsPlayed: 3, - outcome: { reason: 'bust', finalChips: 0 }, - }, - handPhase: { kind: 'betweenHands', lastResult: null }, - } - const settled = [debt({ owedUnits: 200 })] - const next = gameReducer(mid, { type: 'debtSettled', debts: settled }) - expect(next).toEqual({ - debtLoad: { status: 'ok', debts: settled }, - buyIn: null, - session: null, - handPhase: null, - }) - }) - - it('debtProgressLogged updates debtLoad only', () => { - const next = gameReducer(initialState, { type: 'debtProgressLogged', debts: [debt({ paidUnits: 20 })] }) - expect(next.debtLoad).toEqual({ status: 'ok', debts: [debt({ paidUnits: 20 })] }) - }) -}) diff --git a/poker-stakes/src/ui/useGame.session.test.tsx b/poker-stakes/src/ui/useGame.session.test.tsx deleted file mode 100644 index 313d067d..00000000 --- a/poker-stakes/src/ui/useGame.session.test.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import { act, renderHook } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { hasOutstandingDebt, useGame } from './useGame' -import { makeStorage, unreadableStorage, wrapper } from '../test/useGameFixtures' - -describe('useGame', () => { - it('starts with no debt and no session, blocked=false', () => { - const { result } = renderHook(() => useGame(makeStorage(), 1), { wrapper }) - expect(hasOutstandingDebt(result.current.state.debtLoad)).toBe(false) - expect(result.current.state.session).toBeNull() - }) - - it('starts blocked when storage is unreadable, fail-closed', () => { - const { result } = renderHook(() => useGame(unreadableStorage(), 1), { wrapper }) - expect(result.current.state.debtLoad).toEqual({ status: 'unreadable' }) - expect(hasOutstandingDebt(result.current.state.debtLoad)).toBe(true) - }) - - it('buyIn is a no-op when storage is unreadable — the hard gate itself blocks, not just the UI', () => { - // Regression: an earlier version derived the gate's debt list via a helper that collapsed - // `{status: 'unreadable'}` to `[]`, which made `hasOutstandingDebt` see an empty (non-blocking) - // list and let a session start despite fail-closed storage. The hook's `buyIn` must itself - // refuse, not rely on a UI component to have already checked `state.debtLoad`. - const { result } = renderHook(() => useGame(unreadableStorage(), 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 200 }) - }) - expect(result.current.state.session).toBeNull() - }) - - it('buy-in starts a session and pauses on the player seat (StrictMode double-invoke safe)', () => { - const { result } = renderHook(() => useGame(makeStorage(), 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 200 }) - }) - expect(result.current.state.session).not.toBeNull() - expect(result.current.state.session?.handsPlayed).toBe(0) - expect(result.current.state.handPhase?.kind).toBe('awaiting') - // If StrictMode's double-invoke perturbed the generator, stacks would already be inconsistent. - const stacks = result.current.state.session?.stacks - expect(stacks?.player).toBeGreaterThan(0) - expect(stacks?.opponent).toBeGreaterThan(0) - }) - - it('buyIn is a no-op while a debt is outstanding (hard gate, not just BuyInScreen UI)', () => { - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 200 }) - }) - // Cash out immediately: `cashOut` requires `betweenHands`, but hand 1 just started as - // `awaiting`. Fold to reach `betweenHands` deterministically without depending on a specific - // showdown outcome. - act(() => { - result.current.submitPlayerAction({ type: 'fold' }) - }) - expect(result.current.state.handPhase?.kind).toBe('betweenHands') - act(() => { - result.current.cashOut() - }) - expect(result.current.state.session).toBeNull() - const debtsBefore = result.current.state.debtLoad.status === 'ok' ? result.current.state.debtLoad.debts : [] - expect(debtsBefore).toHaveLength(1) - - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 50 }) - }) - expect(result.current.state.session).toBeNull() - const debtsAfter = result.current.state.debtLoad.status === 'ok' ? result.current.state.debtLoad.debts : [] - expect(debtsAfter).toHaveLength(1) - }) - - it('buyIn rejects a non-positive or fractional chip count (chips are integers throughout)', () => { - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 0 }) - }) - expect(result.current.state.session).toBeNull() - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: -5 }) - }) - expect(result.current.state.session).toBeNull() - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 2.5 }) - }) - expect(result.current.state.session).toBeNull() - }) - - it('submitting a second action right after a hand resolves into betweenHands does not throw', () => { - // Regression: handRef used to only be cleared on settle, not on the betweenHands path, so a - // late/duplicate submitPlayerAction call after a hand resolved without ending the session hit - // `hand.submit()` on an already-complete InteractiveHand and threw out of the event handler. - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 200 }) - }) - act(() => { - result.current.submitPlayerAction({ type: 'fold' }) - }) - expect(result.current.state.handPhase?.kind).toBe('betweenHands') - expect(() => { - act(() => { - result.current.submitPlayerAction({ type: 'fold' }) - }) - }).not.toThrow() - // Still safely between hands — the stale submit was a no-op, not a state corruption. - expect(result.current.state.handPhase?.kind).toBe('betweenHands') - }) - - it('cashOut is a no-op when no session is live', () => { - const { result } = renderHook(() => useGame(makeStorage(), 1), { wrapper }) - act(() => { - result.current.cashOut() - }) - expect(result.current.state.session).toBeNull() - expect(result.current.state.debtLoad).toEqual({ status: 'ok', debts: [] }) - }) - - it('cashOut is a no-op mid-hand (only legal between hands, per the settlement-exploit fix)', () => { - // Buying in leaves a live hand `awaiting` a player decision. Cashing out here must be a - // no-op: `session.stacks` predates this hand's blinds/bets, so settling against it would let - // a player see the deal, cash out at the pre-hand stack, and have posted chips vanish. - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 200 }) - }) - expect(result.current.state.handPhase?.kind).toBe('awaiting') - act(() => { - result.current.cashOut() - }) - expect(result.current.state.session).not.toBeNull() - expect(result.current.state.debtLoad).toEqual({ status: 'ok', debts: [] }) - }) - - it('a hand that reaches showdown lands in betweenHands, not auto-dealing the next one', () => { - // Seed 1 at a 200-chip buy-in, always checking/calling: 4 submits carry the first hand to - // showdown. The session must land in `betweenHands` (handsPlayed incremented, no live hand) - // rather than silently starting hand 2 — that pause is what makes cashOut safe. - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 200 }) - }) - for (let i = 0; i < 4; i += 1) { - const phase = result.current.state.handPhase - if (phase?.kind !== 'awaiting') { - break - } - const { betting } = phase.step - const toCall = betting.betToCall - betting.seats.player.committed - act(() => { - result.current.submitPlayerAction(toCall > 0 ? { type: 'call' } : { type: 'check' }) - }) - } - expect(result.current.state.session?.handsPlayed).toBe(1) - expect(result.current.state.session?.outcome).toBeNull() - const phase = result.current.state.handPhase - expect(phase).toMatchObject({ kind: 'betweenHands' }) - expect(phase?.kind === 'betweenHands' ? phase.lastResult : undefined).not.toBeNull() - }) - - it('dealHand starts the next hand after betweenHands', () => { - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 200 }) - }) - act(() => { - result.current.submitPlayerAction({ type: 'fold' }) - }) - expect(result.current.state.handPhase?.kind).toBe('betweenHands') - act(() => { - result.current.dealHand() - }) - expect(result.current.state.handPhase?.kind).toBe('awaiting') - expect(result.current.state.session?.handsPlayed).toBe(1) - }) - - it('dealHand is a no-op before any buy-in', () => { - const { result } = renderHook(() => useGame(makeStorage(), 1), { wrapper }) - act(() => { - result.current.dealHand() - }) - expect(result.current.state.session).toBeNull() - expect(result.current.state.handPhase).toBeNull() - }) - - it('submitPlayerAction is a no-op before any buy-in (no live hand to advance)', () => { - const { result } = renderHook(() => useGame(makeStorage(), 1), { wrapper }) - act(() => { - result.current.submitPlayerAction({ type: 'fold' }) - }) - expect(result.current.state.session).toBeNull() - expect(result.current.state.handPhase).toBeNull() - }) - - it('a zero-player-decision hand (all-in from the blind post) settles instead of soft-locking', () => { - // Buy-in of 1 chip: deriveBlindSize floors BB at 2, SB at 1 — the button posts their entire - // 1-chip stack as the small blind, an immediate all-in with zero player decision points. - // `dealHand` must fold that hand's result into the session and settle rather than leaving a - // live, un-settled session with no pending decision (the soft-lock this session's advisor - // review caught: `initialBettingState` used to reset `allIn: false` on every street). - // Seed 1 deterministically busts the player on this exact first hand. - const storage = makeStorage() - const { result } = renderHook(() => useGame(storage, 1), { wrapper }) - act(() => { - result.current.buyIn({ taskTypeId: 'pushups', chips: 1 }) - }) - expect(result.current.state.session).toBeNull() - expect(result.current.state.handPhase).toBeNull() - const debts = result.current.state.debtLoad.status === 'ok' ? result.current.state.debtLoad.debts : [] - expect(debts).toHaveLength(1) - expect(debts[0]).toMatchObject({ owedUnits: 2, reason: 'bust' }) - }) - -}) diff --git a/poker-stakes/src/ui/useGame.ts b/poker-stakes/src/ui/useGame.ts deleted file mode 100644 index 56532f12..00000000 --- a/poker-stakes/src/ui/useGame.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { useCallback, useReducer, useRef } from 'react' -import type { Action as PokerAction } from '../engine/bettingRound' -import type { InteractiveHand } from '../engine/interactiveHand' -import { startInteractiveHand } from '../engine/interactiveHand' -import { createRng } from '../engine/rng' -import type { Rng } from '../engine/rng' -import { applyHandResult, cashOutSession, deriveBlindSize, startSession } from '../engine/session' -import type { SessionState } from '../engine/session' -import { settleSession } from '../stakes/settlement' -import { loadDebts, saveDebts } from '../stakes/storage' -import type { DebtLoad } from '../stakes/storage' -import type { BuyIn, SettledDebt } from '../stakes/types' - -/** XORed into the deal seed for the opponent's independent policy RNG, per the spec's seam. */ -const POLICY_SEED_SALT = 0x9e3779b9 - -const OPPONENT_ITERATIONS = 200 -import { gameReducer, hasOutstandingDebt, initialGameState } from './gameState' -import type { GameState } from './gameState' - -export type { GameState, HandPhase } from './gameState' -export { gameReducer, hasOutstandingDebt } from './gameState' - -export interface UseGameResult { - state: GameState - buyIn: (buyIn: BuyIn) => void - dealHand: () => void - submitPlayerAction: (action: PokerAction) => void - cashOut: () => void - logDebtProgress: (debtId: string, units: number) => void -} - -/** Computed once via lazy init on both `useReducer` and `useRef` below — pure given `storage`. */ -const readInitialDebts = (debtLoad: DebtLoad): SettledDebt[] => (debtLoad.status === 'ok' ? debtLoad.debts : []) - -export const useGame = (storage: Storage, dealSeed: number): UseGameResult => { - const [state, dispatch] = useReducer(gameReducer, initialGameState, (): GameState => ({ - ...initialGameState, - debtLoad: loadDebts(storage), - })) - - const handRef = useRef(null) - const dealRngRef = useRef(createRng(dealSeed)) - const policyRngRef = useRef(createRng(dealSeed ^ POLICY_SEED_SALT)) - const sessionRef = useRef(null) - const buyInRef = useRef(null) - // Authoritative debt list, independent of render state, so an unreadable-storage snapshot in - // `state.debtLoad` can never cause a settle to silently drop existing debts (fail-open bug). - // `useRef`'s initial-value argument is evaluated on every render (only the first is kept), so - // this and `storageUnreadableRef` below both see the same `loadDebts(storage)` read `state`'s - // own lazy initializer used — no separate read, no risk of the two disagreeing. - const debtsRef = useRef(readInitialDebts(loadDebts(storage))) - // Sticky: unreadable at load blocks buyIn for the lifetime of this hook instance, even though - // `debtsRef` itself only ever holds `SettledDebt[]` (never a nullable/unreadable marker) — this - // is what lets `buyIn`'s gate see 'unreadable' without a helper that collapses it to `[]` first - // (the exact bug this session's review caught: silently unblocking the gate). - const storageUnreadableRef = useRef(loadDebts(storage).status !== 'ok') - - const persistDebts = useCallback( - (debts: SettledDebt[]): SettledDebt[] => { - debtsRef.current = debts - saveDebts(storage, debts) - return debts - }, - [storage], - ) - - const settleAndPersist = useCallback( - // Caller (dealHand / cashOut) guarantees `session.outcome` is non-null and `buyInSnapshot` is - // the live buy-in; not re-guarded here. - (session: SessionState & { outcome: NonNullable }, buyInSnapshot: BuyIn): void => { - const debt = settleSession(buyInSnapshot, session.outcome, crypto.randomUUID(), Date.now()) - const nextDebts = persistDebts([...debtsRef.current, debt]) - handRef.current = null - buyInRef.current = null - sessionRef.current = null - dispatch({ type: 'debtSettled', debts: nextDebts }) - }, - [persistDebts], - ) - - /** - * Deals one hand. In the rare case a seat is already all-in from posting a blind (stack ≤ - * blind size), the hand can complete with ZERO player decision points — `startInteractiveHand` - * resolves every opponent turn internally and returns already `complete`. That result is folded - * into the session immediately (not left for a UI round-trip that would never come), and either - * settles the session or lands in `betweenHands` so the caller can deal again. - */ - const dealHand = useCallback((): void => { - const session = sessionRef.current - const buyInSnapshot = buyInRef.current - // Reachable only if called with no live session (before any buy-in, or after settlement) — - // the UI only exposes this action while `handPhase.kind === 'betweenHands'`. - if (session === null || buyInSnapshot === null) { - return - } - const { bigBlind, smallBlind } = deriveBlindSize(buyInSnapshot.chips) - const hand = startInteractiveHand( - dealRngRef.current, - policyRngRef.current, - { buttonSeat: session.buttonSeat, bigBlind, smallBlind, stacks: session.stacks }, - OPPONENT_ITERATIONS, - ) - const status = hand.status - if (status.kind === 'awaiting') { - handRef.current = hand - dispatch({ type: 'handPhaseChanged', session, handPhase: { kind: 'awaiting', step: status.step } }) - return - } - const nextSession = applyHandResult(session, status.result) - sessionRef.current = nextSession - if (nextSession.outcome !== null) { - settleAndPersist({ ...nextSession, outcome: nextSession.outcome }, buyInSnapshot) - return - } - dispatch({ - type: 'handPhaseChanged', - session: nextSession, - handPhase: { kind: 'betweenHands', lastResult: status.result }, - }) - }, [settleAndPersist]) - - const buyIn = useCallback( - (nextBuyIn: BuyIn): void => { - // Hard gate: an outstanding debt blocks starting a new session, per the confirmed stakes - // model — never just a UI-level warning. Chips are integers throughout (per the plan), so a - // non-positive or fractional buy-in is rejected here too, not just via an input `min`/`step`. - if ( - storageUnreadableRef.current || - hasOutstandingDebt({ status: 'ok', debts: debtsRef.current }) || - !Number.isInteger(nextBuyIn.chips) || - nextBuyIn.chips <= 0 - ) { - return - } - const session = startSession({ buyInChips: nextBuyIn.chips }) - buyInRef.current = nextBuyIn - sessionRef.current = session - dispatch({ type: 'buyIn', buyIn: nextBuyIn, session }) - dealHand() - }, - [dealHand], - ) - - const submitPlayerAction = useCallback( - (action: PokerAction): void => { - const hand = handRef.current - const session = sessionRef.current - const buyInSnapshot = buyInRef.current - // Reachable only via a submit before any buy-in — `hand.submit` itself throws if no - // decision is pending, which per this session's review is otherwise unreachable: `dealHand` - // never leaves `handRef` holding an already-`complete` hand. - if (hand === null || session === null || buyInSnapshot === null) { - return - } - hand.submit(action) - const status = hand.status - if (status.kind === 'awaiting') { - dispatch({ type: 'handPhaseChanged', session, handPhase: { kind: 'awaiting', step: status.step } }) - return - } - // Hand resolved. Land in `betweenHands` — do NOT auto-deal the next hand: `session.stacks` - // must reflect a just-completed hand so `cashOut` (only legal in `betweenHands`) settles - // against real chips, and mid-hand cash-out would let a player see a bad flop, cash out at - // the PRE-hand stack, and have the posted blinds/bets vanish with the discarded generator. - handRef.current = null - const nextSession = applyHandResult(session, status.result) - sessionRef.current = nextSession - if (nextSession.outcome !== null) { - settleAndPersist({ ...nextSession, outcome: nextSession.outcome }, buyInSnapshot) - return - } - dispatch({ - type: 'handPhaseChanged', - session: nextSession, - handPhase: { kind: 'betweenHands', lastResult: status.result }, - }) - }, - [settleAndPersist], - ) - - const cashOut = useCallback((): void => { - const session = sessionRef.current - const buyInSnapshot = buyInRef.current - // Only legal between hands: `handRef.current !== null` means a hand is in progress, and - // mid-hand `session.stacks` predates that hand's blinds/bets — cashing out then would settle - // against a stale stack and let a player see the deal before deciding to cash out. handRef is - // read (a synchronous ref) rather than `state.handPhase` (React state) because this guard is - // security-critical: it must reflect what actually happened, not what has rendered yet. - if (session?.outcome !== null || buyInSnapshot === null || handRef.current !== null) { - return - } - const nextSession = cashOutSession(session) - sessionRef.current = nextSession - dispatch({ type: 'cashedOut', session: nextSession }) - // cashOutSession always sets outcome, so this cast just restates that fact for the type. - settleAndPersist(nextSession as SessionState & { outcome: NonNullable }, buyInSnapshot) - }, [settleAndPersist]) - - const logDebtProgress = useCallback( - (debtId: string, units: number): void => { - const nextDebts = debtsRef.current.map((d) => - d.id === debtId ? { ...d, paidUnits: Math.min(d.owedUnits, d.paidUnits + units) } : d, - ) - persistDebts(nextDebts) - dispatch({ type: 'debtProgressLogged', debts: nextDebts }) - }, - [persistDebts], - ) - - return { state, buyIn, dealHand, submitPlayerAction, cashOut, logDebtProgress } -} diff --git a/poker-stakes/tsconfig.json b/poker-stakes/tsconfig.json deleted file mode 100644 index 7bf5ff42..00000000 --- a/poker-stakes/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2023", - "lib": ["ES2023", "DOM", "DOM.Iterable"], - "module": "ESNext", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "strict": true, - "noUncheckedIndexedAccess": true, - "exactOptionalPropertyTypes": true, - "noImplicitOverride": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "verbatimModuleSyntax": true, - "isolatedModules": true, - "useDefineForClassFields": true, - "skipLibCheck": true, - "noEmit": true, - "types": ["vite/client"] - }, - "include": ["src", "vite.config.ts"] -} diff --git a/poker-stakes/vite.config.ts b/poker-stakes/vite.config.ts deleted file mode 100644 index f2279beb..00000000 --- a/poker-stakes/vite.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -/// -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' - -export default defineConfig({ - plugins: [react()], - test: { - environment: 'jsdom', - setupFiles: ['./src/test/setup.ts'], - coverage: { - provider: 'v8', - include: ['src/**/*.{ts,tsx}'], - exclude: ['src/**/*.test.{ts,tsx}', 'src/test/**'], - thresholds: { lines: 100, functions: 100, branches: 100, statements: 100 }, - reporter: ['text', 'json-summary'], - }, - }, -})