diff --git a/.claude/frontend-poc/.env.development b/.claude/frontend-poc/.env.development new file mode 100644 index 0000000..9c487dc --- /dev/null +++ b/.claude/frontend-poc/.env.development @@ -0,0 +1,11 @@ +# Development environment configuration +# These values are used when running `npm run dev` + +# Backend API base URL (FastAPI server) +VITE_API_BASE_URL=http://localhost:8001 + +# WebSocket URL (Socket.IO server - same as API in development) +VITE_WS_URL=http://localhost:8001 + +# OAuth redirect URI (must match OAuth provider configuration) +VITE_OAUTH_REDIRECT_URI=http://localhost:3001/auth/callback diff --git a/.claude/frontend-poc/.env.production b/.claude/frontend-poc/.env.production new file mode 100644 index 0000000..94f4ed1 --- /dev/null +++ b/.claude/frontend-poc/.env.production @@ -0,0 +1,11 @@ +# Production environment configuration +# These values are used when running `npm run build` + +# Backend API base URL +VITE_API_BASE_URL=https://api.pocket.manticorum.com + +# WebSocket URL (Socket.IO server) +VITE_WS_URL=https://api.pocket.manticorum.com + +# OAuth redirect URI (must match OAuth provider configuration) +VITE_OAUTH_REDIRECT_URI=https://pocket.manticorum.com/auth/callback diff --git a/.claude/frontend-poc/.gitignore b/.claude/frontend-poc/.gitignore new file mode 100644 index 0000000..8a38d8f --- /dev/null +++ b/.claude/frontend-poc/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +*.vite + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.claude/frontend-poc/CLAUDE.md b/.claude/frontend-poc/CLAUDE.md new file mode 100644 index 0000000..327f1de --- /dev/null +++ b/.claude/frontend-poc/CLAUDE.md @@ -0,0 +1,553 @@ +# Mantimon TCG Frontend - AI Agent Guidelines + +Guidelines for AI agents working on the frontend codebase. + +## Tech Stack + +| Technology | Purpose | +|------------|---------| +| Vue 3 | UI framework (Composition API + ` + + + + +``` + +### TypeScript + +**Use strict typing:** +```typescript +// Good - explicit types +interface CardProps { + cardId: string + size?: 'sm' | 'md' | 'lg' +} + +// Good - type imports +import type { Card, GameState } from '@/types' + +// Good - const assertions for literals +const SIZES = ['sm', 'md', 'lg'] as const +type Size = typeof SIZES[number] + +// Avoid - any +const data: any = response // NO +const data: unknown = response // OK if you'll narrow it +``` + +### Naming Conventions + +| Type | Convention | Example | +|------|------------|---------| +| Components | PascalCase | `CardDisplay.vue`, `DeckBuilder.vue` | +| Composables | camelCase with `use` prefix | `useAuth.ts`, `useDeckValidation.ts` | +| Stores | camelCase with descriptive name | `auth.ts`, `game.ts` | +| Types/Interfaces | PascalCase | `Card`, `GameState`, `ApiResponse` | +| Constants | UPPER_SNAKE_CASE | `MAX_HAND_SIZE`, `API_BASE_URL` | +| CSS classes | kebab-case (BEM optional) | `card-display`, `card-display--active` | + +--- + +## Pinia Stores + +**Use setup store syntax:** +```typescript +// stores/game.ts +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { GameState } from '@/types' + +export const useGameStore = defineStore('game', () => { + // State + const gameState = ref(null) + const isConnected = ref(false) + + // Getters (computed) + const myHand = computed(() => gameState.value?.myHand ?? []) + const isMyTurn = computed(() => gameState.value?.currentPlayer === 'me') + + // Actions + function setGameState(state: GameState) { + gameState.value = state + } + + function clearGame() { + gameState.value = null + isConnected.value = false + } + + return { + // State + gameState, + isConnected, + // Getters + myHand, + isMyTurn, + // Actions + setGameState, + clearGame, + } +}) +``` + +--- + +## Composables + +**Pattern for API composables:** +```typescript +// composables/useDecks.ts +import { ref } from 'vue' +import { apiClient } from '@/api/client' +import type { Deck, DeckCreate } from '@/types' + +export function useDecks() { + const decks = ref([]) + const isLoading = ref(false) + const error = ref(null) + + async function fetchDecks() { + isLoading.value = true + error.value = null + try { + decks.value = await apiClient.get('/api/decks') + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to fetch decks' + } finally { + isLoading.value = false + } + } + + async function createDeck(data: DeckCreate) { + isLoading.value = true + error.value = null + try { + const deck = await apiClient.post('/api/decks', data) + decks.value.push(deck) + return deck + } catch (e) { + error.value = e instanceof Error ? e.message : 'Failed to create deck' + throw e + } finally { + isLoading.value = false + } + } + + return { + decks, + isLoading, + error, + fetchDecks, + createDeck, + } +} +``` + +--- + +## Vue-Phaser Integration + +**Phaser is for rendering only. Game logic lives in backend.** + +### Communication Pattern + +```typescript +// Vue -> Phaser (intentions) +phaserGame.value?.events.emit('card:play', { cardId, targetZone }) +phaserGame.value?.events.emit('attack:select', { attackIndex }) + +// Phaser -> Vue (completions/UI requests) +phaserGame.value?.events.on('animation:complete', handleAnimationComplete) +phaserGame.value?.events.on('card:clicked', handleCardClicked) +``` + +### State Sync + +```typescript +// Phaser scene reads from Pinia store +import { useGameStore } from '@/stores/game' + +class MatchScene extends Phaser.Scene { + private gameStore = useGameStore() + + update() { + // React to store changes + if (this.gameStore.gameState) { + this.renderState(this.gameStore.gameState) + } + } +} +``` + +--- + +## Tailwind Guidelines + +**Mobile-first responsive:** +```html + +
+
+ +
+
+``` + +**Use design system colors:** +```html + +
Fire Type
+
Water Type
+ + + +``` + +--- + +## Testing + +**Component tests with Vitest + Vue Test Utils:** +```typescript +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import CardDisplay from '@/components/cards/CardDisplay.vue' + +describe('CardDisplay', () => { + it('renders card name', () => { + /** + * Test that the card component displays the card name. + * + * Card names must be visible for players to identify cards + * in their hand and on the board. + */ + const wrapper = mount(CardDisplay, { + props: { card: { id: '1', name: 'Pikachu', hp: 60 } } + }) + expect(wrapper.text()).toContain('Pikachu') + }) + + it('emits click event with card id', async () => { + /** + * Test that clicking a card emits the correct event. + * + * Card clicks drive all game interactions - playing cards, + * selecting targets, viewing details. + */ + const wrapper = mount(CardDisplay, { + props: { card: { id: '1', name: 'Pikachu', hp: 60 } } + }) + await wrapper.trigger('click') + expect(wrapper.emitted('click')?.[0]).toEqual(['1']) + }) +}) +``` + +--- + +## Critical Rules + +1. **Mobile-first** - Base styles for mobile, use `md:` and `lg:` for larger screens +2. **TypeScript strict** - No `any`, explicit types for props/emits/returns +3. **Composition API only** - No Options API, use ` + + diff --git a/.claude/frontend-poc/package-lock.json b/.claude/frontend-poc/package-lock.json new file mode 100644 index 0000000..115bf5f --- /dev/null +++ b/.claude/frontend-poc/package-lock.json @@ -0,0 +1,5683 @@ +{ + "name": "mantimon-tcg-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mantimon-tcg-frontend", + "version": "0.1.0", + "dependencies": { + "@tailwindcss/postcss": "^4.1.18", + "@vueuse/core": "^14.2.0", + "mitt": "^3.0.1", + "phaser": "^3.90.0", + "pinia": "^3.0.2", + "pinia-plugin-persistedstate": "^4.2.0", + "socket.io-client": "^4.8.1", + "vue": "^3.5.24", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.2", + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.8.1", + "autoprefixer": "^10.4.21", + "eslint": "^9.21.0", + "eslint-plugin-vue": "^10.1.0", + "globals": "^17.2.0", + "jsdom": "^27.4.0", + "postcss": "^8.5.3", + "tailwindcss": "^4.1.4", + "typescript": "~5.9.3", + "typescript-eslint": "^8.54.0", + "vite": "^7.2.4", + "vitest": "^3.1.2", + "vue-tsc": "^3.1.4" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", + "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", + "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.26.tgz", + "integrity": "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.10.0.tgz", + "integrity": "sha512-tf8YdcbirXdPnJ+Nd4UN1EXnz+IP2DI45YVEr3vvzcVTOyrApkmIB4zvOQVd3XPr7RXnfBtAx+PXImXOIU0Ajg==", + "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.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "postcss": "^8.4.41", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "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/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.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.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "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.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", + "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.53" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.27", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.27.tgz", + "integrity": "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.27" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.27", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.27.tgz", + "integrity": "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.27", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.27.tgz", + "integrity": "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.27", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.4.tgz", + "integrity": "sha512-bqBGuSG4KZM45KKTXzGtoCl9cWju5jsaBKaJJe3h5hRAAWpZUuj5G+L+eI01sPIkm4H6setKRlw7E85wLdDNew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.27", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "license": "MIT" + }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, + "node_modules/@vue/tsconfig": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz", + "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.0.tgz", + "integrity": "sha512-tpjzVl7KCQNVd/qcaCE9XbejL38V6KJAEq/tVXj7mDPtl6JtzmUdnXelSS+ULRkkrDgzYVK7EerQJvd2jR794Q==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.2.0", + "@vueuse/shared": "14.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.0.tgz", + "integrity": "sha512-i3axTGjU8b13FtyR4Keeama+43iD+BwX9C2TmzBVKqjSHArF03hjkp2SBZ1m72Jk2UtrX0aYCugBq2R1fhkuAQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.0.tgz", + "integrity": "sha512-Z0bmluZTlAXgUcJ4uAFaML16JcD8V0QG00Db3quR642I99JXIDRa2MI2LGxiLVhcBjVnL1jOzIvT5TT2lqJlkA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "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/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/alien-signals": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "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/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "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/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "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": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "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-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "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": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.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", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.7.0.tgz", + "integrity": "sha512-r2XFCK4qlo1sxEoAMIoTTX0PZAdla0JJDt1fmYiworZUX67WeEGqm+JbyAg3M+pGiJ5U6Mp5WQbontXWtIW7TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^7.1.0", + "semver": "^7.6.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "vue-eslint-parser": "^10.0.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "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": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "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/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/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.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "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/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.2.0.tgz", + "integrity": "sha512-tovnCz/fEq+Ripoq+p/gN1u7l6A7wwkoBT9pRCzTHzsD/LvADIzXZdjmRymh5Ztf0DYC3Rwg5cZRYjxzBmzbWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "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/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "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/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/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/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/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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "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.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "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/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "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/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/phaser": { + "version": "3.90.0", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz", + "integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia-plugin-persistedstate": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.7.1.tgz", + "integrity": "sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4" + }, + "peerDependencies": { + "@nuxt/kit": ">=3.0.0", + "@pinia/nuxt": ">=0.10.0", + "pinia": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@pinia/nuxt": { + "optional": true + }, + "pinia": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "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/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/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "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/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "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": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "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/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "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.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz", + "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-eslint-parser": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz", + "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.4.tgz", + "integrity": "sha512-xj3YCvSLNDKt1iF9OcImWHhmYcihVu9p4b9s4PGR/qp6yhW+tZJaypGxHScRyOrdnHvaOeF+YkZOdKwbgGvp5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.27", + "@vue/language-core": "3.2.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/w3c-xmlserializer/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/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": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/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/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "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/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "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" + } + } + } +} diff --git a/.claude/frontend-poc/package.json b/.claude/frontend-poc/package.json new file mode 100644 index 0000000..f169c8b --- /dev/null +++ b/.claude/frontend-poc/package.json @@ -0,0 +1,45 @@ +{ + "name": "mantimon-tcg-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview", + "test": "vitest", + "test:watch": "vitest --watch", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "@tailwindcss/postcss": "^4.1.18", + "@vueuse/core": "^14.2.0", + "mitt": "^3.0.1", + "phaser": "^3.90.0", + "pinia": "^3.0.2", + "pinia-plugin-persistedstate": "^4.2.0", + "socket.io-client": "^4.8.1", + "vue": "^3.5.24", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.2", + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.8.1", + "autoprefixer": "^10.4.21", + "eslint": "^9.21.0", + "eslint-plugin-vue": "^10.1.0", + "globals": "^17.2.0", + "jsdom": "^27.4.0", + "postcss": "^8.5.3", + "tailwindcss": "^4.1.4", + "typescript": "~5.9.3", + "typescript-eslint": "^8.54.0", + "vite": "^7.2.4", + "vitest": "^3.1.2", + "vue-tsc": "^3.1.4" + } +} diff --git a/.claude/frontend-poc/postcss.config.js b/.claude/frontend-poc/postcss.config.js new file mode 100644 index 0000000..1c87846 --- /dev/null +++ b/.claude/frontend-poc/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} diff --git a/.claude/frontend-poc/project_plans/PHASE_F0_foundation.json b/.claude/frontend-poc/project_plans/PHASE_F0_foundation.json new file mode 100644 index 0000000..ea29ce3 --- /dev/null +++ b/.claude/frontend-poc/project_plans/PHASE_F0_foundation.json @@ -0,0 +1,195 @@ +{ + "meta": { + "phaseId": "PHASE_F0", + "name": "Project Foundation", + "version": "1.0.0", + "created": "2026-01-30", + "lastUpdated": "2026-01-30", + "totalTasks": 8, + "completedTasks": 8, + "status": "completed" + }, + "tasks": [ + { + "id": "F0-001", + "name": "Initialize Vite project", + "description": "Create Vue 3 + TypeScript project with Vite", + "category": "setup", + "priority": 1, + "completed": true, + "tested": true, + "dependencies": [], + "files": [ + {"path": "package.json", "status": "create"}, + {"path": "vite.config.ts", "status": "create"}, + {"path": "tsconfig.json", "status": "create"} + ], + "details": [ + "npm create vite@latest . -- --template vue-ts", + "Configure path aliases (@/ for src/)", + "Set up TypeScript strict mode" + ], + "notes": "Completed in initial scaffold commit b9b803d" + }, + { + "id": "F0-002", + "name": "Install and configure Tailwind", + "description": "Set up Tailwind CSS with custom theme", + "category": "styling", + "priority": 2, + "completed": true, + "tested": true, + "dependencies": ["F0-001"], + "files": [ + {"path": "tailwind.config.js", "status": "create"}, + {"path": "src/assets/main.css", "status": "create"} + ], + "details": [ + "Install tailwindcss, postcss, autoprefixer", + "Configure content paths", + "Add Mantimon color palette (Pokemon-inspired)" + ], + "notes": "Completed in initial scaffold - using Tailwind v4 with @tailwindcss/postcss" + }, + { + "id": "F0-003", + "name": "Set up Vue Router", + "description": "Configure routing with guards and lazy loading", + "category": "setup", + "priority": 3, + "completed": true, + "tested": true, + "dependencies": ["F0-001"], + "files": [ + {"path": "src/router/index.ts", "status": "modify"}, + {"path": "src/router/guards.ts", "status": "create"} + ], + "details": [ + "Define route structure per sitePlan", + "Create auth guard (requireAuth, requireGuest)", + "Create starter guard (redirect if no starter deck)", + "Configure lazy loading for heavy routes", + "Extract guards to separate file" + ], + "notes": "Partial - router/index.ts exists with basic routes and inline guard. Needs guards.ts extraction and starter deck guard." + }, + { + "id": "F0-004", + "name": "Set up Pinia stores", + "description": "Create store structure with persistence", + "category": "stores", + "priority": 4, + "completed": true, + "tested": true, + "dependencies": ["F0-001"], + "files": [ + {"path": "src/stores/auth.ts", "status": "modify"}, + {"path": "src/stores/user.ts", "status": "create"}, + {"path": "src/stores/ui.ts", "status": "create"} + ], + "details": [ + "Install pinia and pinia-plugin-persistedstate (done)", + "Update auth store for OAuth flow (not username/password)", + "Create user store skeleton (display_name, avatar, linked accounts)", + "Create UI store (loading states, toasts, modals)" + ], + "notes": "Partial - auth.ts exists but uses username/password pattern instead of OAuth. Needs user.ts and ui.ts." + }, + { + "id": "F0-005", + "name": "Create API client", + "description": "HTTP client with auth token injection and refresh", + "category": "api", + "priority": 5, + "completed": true, + "tested": true, + "dependencies": ["F0-001", "F0-004"], + "files": [ + {"path": "src/api/client.ts", "status": "create"}, + {"path": "src/api/types.ts", "status": "create"} + ], + "details": [ + "Create fetch wrapper with base URL from config", + "Inject Authorization header from auth store", + "Handle 401 responses with automatic token refresh", + "Type API responses (ApiError, ApiResponse)", + "Add request/response interceptor pattern" + ], + "notes": "Not started. Critical for all backend communication." + }, + { + "id": "F0-006", + "name": "Create Socket.IO client", + "description": "WebSocket connection manager", + "category": "api", + "priority": 6, + "completed": true, + "tested": true, + "dependencies": ["F0-001", "F0-004"], + "files": [ + {"path": "src/socket/client.ts", "status": "create"}, + {"path": "src/socket/types.ts", "status": "create"} + ], + "details": [ + "Install socket.io-client (done)", + "Create connection manager singleton", + "Configure auth token in handshake", + "Set up reconnection with exponential backoff", + "Create typed event emitters for game namespace" + ], + "notes": "Not started. socket.io-client is installed." + }, + { + "id": "F0-007", + "name": "Create app shell", + "description": "Basic layout with navigation", + "category": "components", + "priority": 7, + "completed": true, + "tested": true, + "dependencies": ["F0-001", "F0-002", "F0-003"], + "files": [ + {"path": "src/App.vue", "status": "modify"}, + {"path": "src/layouts/DefaultLayout.vue", "status": "create"}, + {"path": "src/layouts/MinimalLayout.vue", "status": "create"}, + {"path": "src/layouts/GameLayout.vue", "status": "create"}, + {"path": "src/components/NavSidebar.vue", "status": "create"}, + {"path": "src/components/NavBottomTabs.vue", "status": "create"}, + {"path": "src/components/ui/LoadingOverlay.vue", "status": "create"}, + {"path": "src/components/ui/ToastContainer.vue", "status": "create"}, + {"path": "src/types/vue-router.d.ts", "status": "create"} + ], + "details": [ + "Create DefaultLayout with sidebar (desktop) / bottom tabs (mobile)", + "Create MinimalLayout for login/auth pages (centered, no nav)", + "Create GameLayout for match page (full viewport, no nav)", + "Responsive nav: NavSidebar for md+, NavBottomTabs for mobile", + "Loading overlay component tied to UI store", + "Toast notification container tied to UI store" + ], + "notes": "Completed - App.vue uses dynamic layout switching based on route meta. All layouts and navigation components created with tests." + }, + { + "id": "F0-008", + "name": "Environment configuration", + "description": "Configure environment variables", + "category": "setup", + "priority": 8, + "completed": true, + "tested": true, + "dependencies": ["F0-001"], + "files": [ + {"path": ".env.development", "status": "create"}, + {"path": ".env.production", "status": "create"}, + {"path": "src/config.ts", "status": "create"} + ], + "details": [ + "Create .env.development with local API URLs", + "Create .env.production with production API URLs", + "Create type-safe config.ts that reads VITE_* vars", + "Define: VITE_API_BASE_URL, VITE_WS_URL, VITE_OAUTH_REDIRECT_URI" + ], + "notes": "Not started. Needed before API client can work properly." + } + ] +} diff --git a/.claude/frontend-poc/project_plans/PHASE_F1_authentication.json b/.claude/frontend-poc/project_plans/PHASE_F1_authentication.json new file mode 100644 index 0000000..103d60b --- /dev/null +++ b/.claude/frontend-poc/project_plans/PHASE_F1_authentication.json @@ -0,0 +1,382 @@ +{ + "meta": { + "phaseId": "PHASE_F1", + "name": "Authentication Flow", + "version": "1.0.0", + "created": "2026-01-30", + "lastUpdated": "2026-01-30T23:35:00Z", + "totalTasks": 10, + "completedTasks": 10, + "status": "COMPLETE", + "description": "Complete OAuth authentication flow including login, callback handling, starter deck selection, profile management, and app initialization." + }, + "dependencies": { + "phases": ["PHASE_F0"], + "backend": [ + "GET /api/auth/google - Start Google OAuth", + "GET /api/auth/discord - Start Discord OAuth", + "GET /api/auth/{provider}/callback - OAuth callback (returns tokens in URL fragment)", + "POST /api/auth/refresh - Refresh access token", + "POST /api/auth/logout - Revoke refresh token", + "POST /api/auth/logout-all - Revoke all tokens (requires auth)", + "GET /api/users/me - Get current user profile", + "PATCH /api/users/me - Update profile", + "GET /api/users/me/linked-accounts - List linked OAuth accounts", + "GET /api/users/me/starter-status - Check if user has starter deck", + "POST /api/users/me/starter-deck - Select starter deck", + "GET /api/auth/link/google - Link Google account (requires auth)", + "GET /api/auth/link/discord - Link Discord account (requires auth)", + "DELETE /api/users/me/link/{provider} - Unlink OAuth provider" + ] + }, + "tasks": [ + { + "id": "F1-001", + "name": "Update LoginPage for OAuth", + "description": "Replace username/password form with OAuth provider buttons", + "category": "pages", + "priority": 1, + "completed": true, + "tested": true, + "dependencies": [], + "files": [ + {"path": "src/pages/LoginPage.vue", "status": "modify"} + ], + "details": [ + "Remove username/password form (not used - OAuth only)", + "Add Google OAuth button with branded styling", + "Add Discord OAuth button with branded styling", + "Handle redirect to OAuth provider via auth store", + "Show error messages from URL query params (oauth_failed)", + "Responsive design for mobile/desktop", + "Add loading state during redirect" + ], + "acceptance": [ + "Page shows two OAuth buttons: Google and Discord", + "Clicking button redirects to backend OAuth endpoint", + "Error messages from failed OAuth are displayed", + "No username/password fields visible" + ] + }, + { + "id": "F1-002", + "name": "Implement AuthCallbackPage", + "description": "Handle OAuth callback and extract tokens from URL fragment", + "category": "pages", + "priority": 2, + "completed": true, + "tested": true, + "dependencies": ["F1-001"], + "files": [ + {"path": "src/pages/AuthCallbackPage.vue", "status": "modify"} + ], + "details": [ + "Parse URL hash fragment for access_token, refresh_token, expires_in", + "Handle error query params (error, message)", + "Store tokens in auth store using setTokens()", + "Fetch user profile after successful auth", + "Check if user needs starter deck selection", + "Redirect to starter selection if no starter, else to dashboard", + "Show appropriate loading/error states", + "Handle edge cases (missing tokens, network errors)" + ], + "acceptance": [ + "Successfully extracts tokens from URL fragment", + "Stores tokens in auth store (persisted)", + "Fetches user profile after auth", + "Redirects to /starter if user has no starter deck", + "Redirects to / (dashboard) if user has starter deck", + "Shows error message if OAuth failed" + ] + }, + { + "id": "F1-003", + "name": "Create useAuth composable", + "description": "Vue composable for auth operations with loading/error states", + "category": "composables", + "priority": 3, + "completed": true, + "tested": true, + "dependencies": ["F1-002"], + "files": [ + {"path": "src/composables/useAuth.ts", "status": "create"}, + {"path": "src/composables/useAuth.spec.ts", "status": "create"} + ], + "details": [ + "Wrap auth store operations with loading/error handling", + "Provide initiateOAuth(provider) helper", + "Provide handleCallback() for AuthCallbackPage", + "Provide logout() with redirect to login", + "Provide logoutAll() for all-device logout", + "Track isInitialized state for app startup", + "Auto-fetch profile on initialization if tokens exist" + ], + "acceptance": [ + "initiateOAuth() redirects to correct OAuth URL", + "handleCallback() extracts tokens and fetches profile", + "logout() clears state and redirects to login", + "Loading and error states are properly tracked" + ] + }, + { + "id": "F1-004", + "name": "Implement app auth initialization", + "description": "Initialize auth state on app startup", + "category": "setup", + "priority": 4, + "completed": true, + "tested": true, + "dependencies": ["F1-003"], + "files": [ + {"path": "src/App.vue", "status": "modify"}, + {"path": "src/main.ts", "status": "modify"} + ], + "details": [ + "Call auth.init() on app startup (in main.ts or App.vue)", + "Show loading state while initializing auth", + "Validate existing tokens by refreshing if expired", + "Fetch user profile if authenticated", + "Handle initialization errors gracefully", + "Block navigation until auth is initialized" + ], + "acceptance": [ + "App shows loading spinner during auth init", + "Expired tokens are refreshed automatically", + "User profile is fetched if authenticated", + "Invalid/expired refresh tokens trigger logout", + "Navigation guards work after init completes" + ] + }, + { + "id": "F1-005", + "name": "Implement StarterSelectionPage", + "description": "Complete starter deck selection with API integration", + "category": "pages", + "priority": 5, + "completed": true, + "tested": true, + "dependencies": ["F1-003"], + "files": [ + {"path": "src/pages/StarterSelectionPage.vue", "status": "modify"}, + {"path": "src/composables/useStarter.ts", "status": "create"}, + {"path": "src/composables/useStarter.spec.ts", "status": "create"} + ], + "details": [ + "Display 5 starter deck options: grass, fire, water, psychic, lightning", + "Show deck preview (card count, theme description)", + "Handle deck selection with confirmation", + "Call POST /api/users/me/starter-deck on selection", + "Show loading state during API call", + "Handle errors (already selected, network error)", + "Redirect to dashboard on success", + "Update auth store hasStarterDeck flag" + ], + "starterTypes": [ + {"type": "grass", "name": "Forest Guardians", "description": "Growth and healing focused deck"}, + {"type": "fire", "name": "Flame Warriors", "description": "Aggressive damage-focused deck"}, + {"type": "water", "name": "Tidal Force", "description": "Balanced control and damage"}, + {"type": "psychic", "name": "Mind Masters", "description": "Status effects and manipulation"}, + {"type": "lightning", "name": "Storm Riders", "description": "Fast, high-damage strikes"} + ], + "acceptance": [ + "5 starter deck options displayed with themes", + "Selection calls API with correct starter_type", + "Success updates user state and redirects to /", + "Errors are displayed to user", + "Already-selected error handled gracefully" + ] + }, + { + "id": "F1-006", + "name": "Implement ProfilePage", + "description": "User profile management with linked accounts", + "category": "pages", + "priority": 6, + "completed": true, + "tested": true, + "dependencies": ["F1-003"], + "files": [ + {"path": "src/pages/ProfilePage.vue", "status": "modify"}, + {"path": "src/composables/useProfile.ts", "status": "create"}, + {"path": "src/composables/useProfile.spec.ts", "status": "create"}, + {"path": "src/components/profile/LinkedAccountCard.vue", "status": "create"}, + {"path": "src/components/profile/DisplayNameEditor.vue", "status": "create"} + ], + "details": [ + "Display user avatar and display name", + "Editable display name with save button", + "List linked OAuth accounts (Google, Discord)", + "Link additional OAuth provider button", + "Unlink OAuth provider (if not primary)", + "Logout button (current session)", + "Logout All button (all devices)", + "Show active session count" + ], + "acceptance": [ + "Profile displays user info correctly", + "Display name can be edited and saved", + "Linked accounts are displayed", + "Can link additional OAuth provider", + "Can unlink non-primary provider", + "Logout works correctly", + "Logout All works correctly" + ] + }, + { + "id": "F1-007", + "name": "Update navigation for auth state", + "description": "Update NavSidebar and NavBottomTabs for auth state", + "category": "components", + "priority": 7, + "completed": true, + "tested": true, + "dependencies": ["F1-003"], + "files": [ + {"path": "src/components/NavSidebar.vue", "status": "modify"}, + {"path": "src/components/NavBottomTabs.vue", "status": "modify"}, + {"path": "src/components/NavSidebar.spec.ts", "status": "create"}, + {"path": "src/components/NavBottomTabs.spec.ts", "status": "create"} + ], + "details": [ + "Show user avatar in nav if available", + "Use actual display name instead of placeholder", + "Ensure logout button triggers proper logout flow", + "Handle loading state during logout" + ], + "acceptance": [ + "Nav shows actual user avatar if available", + "Nav shows actual display name", + "Logout triggers full logout flow with redirect" + ] + }, + { + "id": "F1-008", + "name": "Implement account linking flow", + "description": "Allow users to link additional OAuth providers", + "category": "features", + "priority": 8, + "completed": true, + "tested": true, + "dependencies": ["F1-006"], + "files": [ + {"path": "src/composables/useAccountLinking.ts", "status": "create"}, + {"path": "src/composables/useAccountLinking.spec.ts", "status": "create"}, + {"path": "src/pages/LinkCallbackPage.vue", "status": "create"}, + {"path": "src/router/index.ts", "status": "modify"} + ], + "details": [ + "Add route for /auth/link/callback to handle linking callbacks", + "Initiate linking via GET /api/auth/link/{provider}", + "Handle success/error query params on callback", + "Refresh linked accounts list after linking", + "Show success toast on link complete", + "Handle errors (already linked, etc.)" + ], + "acceptance": [ + "Can initiate link from profile page", + "Link callback handles success and error", + "Linked accounts list updates after linking", + "Appropriate feedback shown to user" + ] + }, + { + "id": "F1-009", + "name": "Add requireStarter guard implementation", + "description": "Implement the starter deck navigation guard", + "category": "router", + "priority": 9, + "completed": true, + "tested": true, + "dependencies": ["F1-005"], + "files": [ + {"path": "src/router/guards.ts", "status": "modify"}, + {"path": "src/router/guards.spec.ts", "status": "modify"} + ], + "details": [ + "requireStarter checks if user has selected starter deck", + "If no starter, redirect to /starter page", + "Check auth.user?.hasStarterDeck flag", + "If flag is undefined, fetch starter status from API", + "Cache result to avoid repeated API calls" + ], + "acceptance": [ + "Users without starter deck are redirected to /starter", + "Users with starter deck can access protected routes", + "Guard waits for auth initialization before checking", + "API is called only when needed" + ] + }, + { + "id": "F1-010", + "name": "Write integration tests for auth flow", + "description": "End-to-end tests for complete auth flow", + "category": "testing", + "priority": 10, + "completed": true, + "tested": true, + "dependencies": ["F1-001", "F1-002", "F1-003", "F1-004", "F1-005"], + "files": [ + {"path": "src/pages/LoginPage.spec.ts", "status": "create"}, + {"path": "src/pages/AuthCallbackPage.spec.ts", "status": "create"}, + {"path": "src/pages/StarterSelectionPage.spec.ts", "status": "create"}, + {"path": "src/pages/ProfilePage.spec.ts", "status": "create"} + ], + "details": [ + "Test LoginPage OAuth button redirects", + "Test AuthCallbackPage token extraction", + "Test AuthCallbackPage error handling", + "Test StarterSelectionPage deck selection flow", + "Test ProfilePage display and edit operations", + "Test navigation guards with various auth states", + "Mock API responses for all tests" + ], + "acceptance": [ + "All page components have test files", + "Tests cover happy path and error cases", + "Tests mock API calls appropriately", + "All tests pass" + ] + } + ], + "apiContracts": { + "oauthCallback": { + "description": "Backend redirects to frontend with tokens in URL fragment", + "format": "/auth/callback#access_token={token}&refresh_token={token}&expires_in={seconds}", + "errorFormat": "/auth/callback?error={code}&message={message}" + }, + "tokenResponse": { + "accessToken": "JWT access token (short-lived)", + "refreshToken": "Opaque refresh token (long-lived)", + "expiresIn": "Access token expiry in seconds" + }, + "userProfile": { + "id": "UUID", + "display_name": "string", + "avatar_url": "string | null", + "has_starter_deck": "boolean", + "created_at": "ISO datetime", + "linked_accounts": [ + { + "provider": "google | discord", + "email": "string | null", + "linked_at": "ISO datetime" + } + ] + }, + "starterDeck": { + "request": { + "starter_type": "grass | fire | water | psychic | lightning" + }, + "response": "DeckResponse with is_starter=true" + } + }, + "notes": [ + "OAuth flow uses URL fragment (hash) for tokens, not query params, for security", + "Tokens are persisted via pinia-plugin-persistedstate", + "Auth store already has most functionality, composables add loading/error handling", + "LoginPage currently has username/password form which needs to be replaced", + "AuthCallbackPage currently just redirects to home - needs full implementation", + "StarterSelectionPage has placeholder UI - needs API integration", + "ProfilePage needs to be created from scratch" + ] +} diff --git a/.claude/frontend-poc/project_plans/PHASE_F2_deck_management.json b/.claude/frontend-poc/project_plans/PHASE_F2_deck_management.json new file mode 100644 index 0000000..3978fa2 --- /dev/null +++ b/.claude/frontend-poc/project_plans/PHASE_F2_deck_management.json @@ -0,0 +1,624 @@ +{ + "meta": { + "phaseId": "PHASE_F2", + "name": "Deck Management", + "version": "1.3.0", + "created": "2026-01-30", + "lastUpdated": "2026-01-31", + "totalTasks": 12, + "completedTasks": 12, + "status": "COMPLETE", + "completedDate": "2026-01-31", + "auditStatus": "PASSED", + "auditDate": "2026-01-31", + "auditNotes": "0 issues, 3 minor warnings. All previous issues resolved: DeckContents refactored (527->292 lines), hardcoded values replaced with useGameConfig, toast feedback added, validation errors surfaced.", + "description": "Collection viewing, deck building, and card management with drag-and-drop deck editor and real-time validation.", + "designReference": "src/styles/DESIGN_REFERENCE.md" + }, + "stylingPrinciples": [ + "Follow patterns in DESIGN_REFERENCE.md for visual consistency", + "Type-colored accents on all card components (borders, badges)", + "Hover states with scale/shadow transitions on interactive cards", + "Skeleton loaders for all loading states (not spinners)", + "Empty states with icon, message, and CTA", + "Mobile-first responsive grids", + "Business rules (deck size, card limits) from constants/API, never hardcoded" + ], + "dependencies": { + "phases": ["PHASE_F0", "PHASE_F1"], + "backend": [ + "GET /api/collections/me - Get user's card collection", + "GET /api/collections/me/cards/{card_id} - Get specific card quantity", + "GET /api/decks - List user's decks", + "POST /api/decks - Create new deck", + "GET /api/decks/{id} - Get deck by ID", + "PUT /api/decks/{id} - Update deck", + "DELETE /api/decks/{id} - Delete deck", + "POST /api/decks/validate - Validate deck without saving", + "GET /api/cards/definitions - Get card definitions (for display)" + ] + }, + "tasks": [ + { + "id": "F2-001", + "name": "Create collection and deck stores", + "description": "Create Pinia stores for managing collection and deck state", + "category": "stores", + "priority": 1, + "completed": true, + "tested": true, + "dependencies": [], + "files": [ + {"path": "src/stores/collection.ts", "status": "create"}, + {"path": "src/stores/collection.spec.ts", "status": "create"}, + {"path": "src/stores/deck.ts", "status": "create"}, + {"path": "src/stores/deck.spec.ts", "status": "create"} + ], + "details": [ + "Create useCollectionStore with state: cards (CollectionCard[]), isLoading, error", + "Add getters: totalCards, uniqueCards, getCardQuantity(definitionId)", + "Add actions: fetchCollection(), clearCollection()", + "Create useDeckStore with state: decks (Deck[]), currentDeck, isLoading, error", + "Add getters: deckCount, getDeckById(id), starterDeck", + "Add actions: fetchDecks(), fetchDeck(id), createDeck(), updateDeck(), deleteDeck(), setCurrentDeck()", + "Follow setup store pattern from auth.ts", + "Do NOT fetch via apiClient directly in store - just state management" + ], + "acceptance": [ + "Both stores created with typed state", + "Stores follow setup store pattern", + "Unit tests cover all getters and actions", + "Stores integrate with existing type definitions" + ] + }, + { + "id": "F2-002", + "name": "Create useCollection composable", + "description": "Composable for fetching and managing the user's card collection", + "category": "composables", + "priority": 2, + "completed": true, + "tested": true, + "dependencies": ["F2-001"], + "files": [ + {"path": "src/composables/useCollection.ts", "status": "create"}, + {"path": "src/composables/useCollection.spec.ts", "status": "create"} + ], + "details": [ + "Wrap collection store with loading/error handling", + "Implement fetchCollection() calling GET /api/collections/me", + "Parse response into CollectionCard[] format", + "Handle empty collection gracefully", + "Provide filtering helpers: byType(type), byCategory(category), byRarity(rarity)", + "Provide search helper: searchByName(query)", + "Return readonly state wrappers per composable pattern", + "Handle network errors with user-friendly messages" + ], + "acceptance": [ + "Composable fetches collection from API", + "Filtering and search work correctly", + "Loading and error states properly managed", + "Tests cover API success, empty collection, and errors" + ] + }, + { + "id": "F2-003", + "name": "Create useDecks composable", + "description": "Composable for CRUD operations on user decks", + "category": "composables", + "priority": 3, + "completed": true, + "tested": true, + "dependencies": ["F2-001"], + "files": [ + {"path": "src/composables/useDecks.ts", "status": "create"}, + {"path": "src/composables/useDecks.spec.ts", "status": "create"} + ], + "details": [ + "Wrap deck store with loading/error handling", + "Implement fetchDecks() calling GET /api/decks", + "Implement fetchDeck(id) calling GET /api/decks/{id}", + "Implement createDeck(data) calling POST /api/decks", + "Implement updateDeck(id, data) calling PUT /api/decks/{id}", + "Implement deleteDeck(id) calling DELETE /api/decks/{id} with confirmation", + "Return result objects: { success, error?, data? }", + "Handle 404 (deck not found), 403 (not owner), validation errors", + "Track separate loading states for different operations if needed" + ], + "acceptance": [ + "All CRUD operations work correctly", + "Error handling covers common cases", + "Result objects returned consistently", + "Tests cover success and error paths for each operation" + ] + }, + { + "id": "F2-004", + "name": "Create card display components", + "description": "Reusable components for displaying cards at various sizes", + "category": "components", + "priority": 4, + "completed": true, + "tested": true, + "dependencies": [], + "files": [ + {"path": "src/components/cards/CardImage.vue", "status": "create"}, + {"path": "src/components/cards/CardDisplay.vue", "status": "create"}, + {"path": "src/components/cards/CardDisplay.spec.ts", "status": "create"}, + {"path": "src/components/cards/TypeBadge.vue", "status": "create"} + ], + "details": [ + "CardImage: Simple img wrapper with loading state and fallback", + "CardImage props: src (string), alt (string), size ('sm' | 'md' | 'lg')", + "CardImage: Handle image load error with placeholder", + "CardDisplay: Full card component with name, HP, type badge", + "CardDisplay props: card (CardDefinition), size, showQuantity (number), selectable, selected, disabled", + "CardDisplay emits: click, select", + "TypeBadge: Reusable type indicator with icon and colored background", + "Apply type-colored border based on card.type (use type color mapping from DESIGN_REFERENCE)", + "Mobile-friendly touch targets (min 44px)", + "Support thumbnail (grid), medium (list), large (detail) sizes" + ], + "styling": [ + "Use 'Card with Type Accent' pattern from DESIGN_REFERENCE.md", + "Hover: scale-[1.02], -translate-y-1, shadow-xl with transition-all duration-200", + "Selected: ring-2 ring-primary ring-offset-2 ring-offset-background", + "Disabled: opacity-50 grayscale cursor-not-allowed", + "Quantity badge: absolute positioned, bg-primary, rounded-full (see 'Quantity Badge' pattern)", + "Type badge: inline-flex with type background color (see 'Type Badge' pattern)", + "Card container: bg-surface rounded-xl border-2 shadow-md", + "Image loading: pulse animation placeholder, smooth fade-in on load" + ], + "acceptance": [ + "CardImage handles loading and errors gracefully", + "CardDisplay shows card info with type-appropriate styling", + "Three sizes work correctly", + "Hover and selection states visible and smooth", + "Disabled state clearly distinguishable", + "Tests verify rendering and events" + ] + }, + { + "id": "F2-005", + "name": "Create card detail modal", + "description": "Full card view with all details in a modal overlay", + "category": "components", + "priority": 5, + "completed": true, + "tested": true, + "dependencies": ["F2-004"], + "files": [ + {"path": "src/components/cards/CardDetailModal.vue", "status": "create"}, + {"path": "src/components/cards/CardDetailModal.spec.ts", "status": "create"}, + {"path": "src/components/cards/AttackDisplay.vue", "status": "create"}, + {"path": "src/components/cards/EnergyCost.vue", "status": "create"} + ], + "details": [ + "Props: card (CardDefinition | null), isOpen (boolean), ownedQuantity (number)", + "Emits: close", + "Large card image with CardImage component", + "Display all card fields: name, HP, type, category, rarity", + "AttackDisplay: Reusable attack row with energy cost, name, damage, effect", + "EnergyCost: Row of energy type icons for attack costs", + "Display weakness, resistance, retreat cost", + "Display owned quantity badge", + "Display set info (setId, setNumber)", + "Use Teleport to render in body", + "Close on backdrop click, Escape key, or close button", + "Trap focus within modal for accessibility", + "Animate in/out with transition" + ], + "styling": [ + "Backdrop: bg-black/60 backdrop-blur-sm (see 'Modal Backdrop' pattern)", + "Container: bg-surface rounded-2xl shadow-2xl max-w-lg (see 'Modal Container' pattern)", + "Header: border-b border-surface-light with close button (see 'Modal Header' pattern)", + "Enter animation: fade-in zoom-in-95 duration-200", + "Card image: centered with type-colored border glow effect", + "Attack rows: bg-surface-light/50 rounded-lg p-3 with hover highlight", + "Energy icons: small colored circles or actual energy symbols", + "Stats section: grid layout for weakness/resistance/retreat", + "Owned quantity: prominent badge in corner of card image area" + ], + "acceptance": [ + "Modal displays all card information", + "Attacks rendered with energy costs", + "Owned quantity visible", + "Close methods all work", + "Smooth enter/exit animations", + "Keyboard accessible (Escape closes, focus trapped)" + ] + }, + { + "id": "F2-006", + "name": "Create collection page", + "description": "Grid view of owned cards with filtering and search", + "category": "pages", + "priority": 6, + "completed": true, + "tested": true, + "dependencies": ["F2-002", "F2-004", "F2-005"], + "files": [ + {"path": "src/pages/CollectionPage.vue", "status": "create"}, + {"path": "src/pages/CollectionPage.spec.ts", "status": "create"}, + {"path": "src/components/ui/FilterBar.vue", "status": "create"}, + {"path": "src/components/ui/SkeletonCard.vue", "status": "create"}, + {"path": "src/components/ui/EmptyState.vue", "status": "create"} + ], + "details": [ + "Fetch collection on mount using useCollection()", + "Display cards in responsive grid (2 cols mobile, 3 md, 4 lg, 5 xl)", + "FilterBar component: type dropdown, category dropdown, rarity dropdown, search input", + "Show card count: 'Showing X of Y cards'", + "Each card shows quantity badge overlay", + "Click card to open CardDetailModal", + "EmptyState component: reusable empty state with icon, message, CTA", + "SkeletonCard component: reusable loading placeholder", + "Loading state: grid of SkeletonCards (match expected count or 8-12)", + "Error state: inline error with retry button", + "Debounce search input (300ms)", + "Filters persist in URL query params for sharing/bookmarking" + ], + "styling": [ + "Page header: text-2xl font-bold with card count subtitle", + "Filter bar: bg-surface rounded-xl p-4 mb-6 (see 'Filter Bar' pattern)", + "Search input: with SearchIcon, focus:border-primary focus:ring-1", + "Dropdowns: matching input styling for consistency", + "Grid: gap-3 md:gap-4, cards fill width of column", + "Skeleton cards: pulse animation, match card aspect ratio (see 'Skeleton Card' pattern)", + "Empty state: centered, py-16, icon + message + CTA (see 'Empty States' pattern)", + "Error state: bg-error/10 border-error/20 with AlertIcon (see 'Inline Error' pattern)", + "Smooth fade-in when cards load (transition-opacity)" + ], + "acceptance": [ + "Collection grid displays all owned cards", + "All filters work correctly", + "Search filters by card name", + "Card modal opens on click", + "Loading state shows skeleton grid", + "Empty state is visually polished", + "Error state allows retry", + "Mobile responsive" + ] + }, + { + "id": "F2-007", + "name": "Create deck list page", + "description": "View all user decks with create/edit/delete actions", + "category": "pages", + "priority": 7, + "completed": true, + "tested": true, + "dependencies": ["F2-003"], + "files": [ + {"path": "src/pages/DecksPage.vue", "status": "create"}, + {"path": "src/pages/DecksPage.spec.ts", "status": "create"}, + {"path": "src/components/deck/DeckCard.vue", "status": "create"}, + {"path": "src/components/deck/DeckCard.spec.ts", "status": "create"}, + {"path": "src/components/ui/ConfirmDialog.vue", "status": "create"} + ], + "details": [ + "Fetch decks on mount using useDecks()", + "Display decks as cards in a grid (1 col mobile, 2 sm, 3 lg)", + "DeckCard component shows: name, card count (from API), validation status icon, starter badge", + "DeckCard shows mini preview of 3-4 Pokemon thumbnails from deck", + "Click deck to navigate to deck editor (/decks/:id)", + "'Create New Deck' button in header -> navigate to /decks/new", + "Delete button (trash icon) with ConfirmDialog", + "ConfirmDialog: reusable confirmation modal component", + "Show deck limit from API response: 'X / Y decks'", + "Empty state: 'No decks yet. Create your first deck!' with + button", + "Loading state: skeleton DeckCards", + "Starter deck: show badge, hide/disable delete button" + ], + "styling": [ + "Page header: flex justify-between with title and 'New Deck' button", + "New Deck button: btn-primary with PlusIcon", + "Grid: gap-4, cards have consistent height", + "DeckCard: bg-surface rounded-xl p-4 shadow-md hover:shadow-lg transition", + "DeckCard hover: subtle lift effect (translate-y, shadow)", + "Card preview: row of 3-4 mini card images (32x44px) with overlap", + "Validity icon: CheckCircle (text-success) or XCircle (text-error)", + "Starter badge: small pill 'Starter' with bg-primary/20 text-primary", + "Delete button: icon-only, text-text-muted hover:text-error, positioned top-right", + "ConfirmDialog: danger variant with red confirm button", + "Skeleton: match DeckCard dimensions with pulse animation", + "Empty state: centered with DeckIcon, use EmptyState component" + ], + "acceptance": [ + "All user decks displayed in polished cards", + "Can create new deck (navigates to builder)", + "Can delete non-starter decks with confirmation", + "Deck validity status clearly visible", + "Starter deck visually distinguished", + "Loading and empty states polished" + ] + }, + { + "id": "F2-008", + "name": "Create useDeckBuilder composable", + "description": "Composable for deck editing state and validation", + "category": "composables", + "priority": 8, + "completed": true, + "tested": true, + "dependencies": ["F2-003"], + "files": [ + {"path": "src/composables/useDeckBuilder.ts", "status": "create"}, + {"path": "src/composables/useDeckBuilder.spec.ts", "status": "create"} + ], + "details": [ + "Manage draft deck state (not yet saved)", + "State: deckName, deckCards (Map), energyConfig", + "Track isDirty (has unsaved changes)", + "addCard(cardDefinitionId, quantity=1) - respect 4-card limit per card (except basic energy)", + "removeCard(cardDefinitionId, quantity=1)", + "setDeckName(name)", + "clearDeck() - reset to empty", + "loadDeck(deck) - populate from existing deck for editing", + "Computed: totalCards, cardList (sorted), canAddCard(cardId)", + "Validation: call POST /api/decks/validate with debounce (500ms)", + "Track validationErrors array, isValid computed", + "save() - calls createDeck or updateDeck based on isNew flag" + ], + "acceptance": [ + "Can add/remove cards with limits enforced", + "Draft state separate from persisted decks", + "Validation runs on changes with debounce", + "isDirty tracks unsaved changes", + "save() creates or updates correctly" + ] + }, + { + "id": "F2-009", + "name": "Create deck builder page", + "description": "Two-panel deck editor with collection picker and deck contents", + "category": "pages", + "priority": 9, + "completed": true, + "tested": true, + "dependencies": ["F2-002", "F2-004", "F2-008"], + "files": [ + {"path": "src/pages/DeckBuilderPage.vue", "status": "create"}, + {"path": "src/pages/DeckBuilderPage.spec.ts", "status": "create"}, + {"path": "src/components/deck/DeckEditor.vue", "status": "create"}, + {"path": "src/components/deck/DeckContents.vue", "status": "create"}, + {"path": "src/components/deck/CollectionPicker.vue", "status": "create"}, + {"path": "src/components/deck/DeckCardRow.vue", "status": "create"}, + {"path": "src/components/ui/ProgressBar.vue", "status": "create"} + ], + "details": [ + "Route: /decks/new (create) or /decks/:id (edit)", + "Load existing deck if editing (from route param)", + "Two-panel layout: CollectionPicker (left/top) + DeckContents (right/bottom)", + "Mobile: Stack panels vertically with tab switcher (Collection | Deck)", + "CollectionPicker: Filterable collection grid, click/tap to add card to deck", + "DeckContents: Scrollable list of cards with quantity controls (+/-)", + "DeckCardRow: Single card row with thumbnail, name, quantity stepper", + "Show cards grayed out if not available (quantity in collection exhausted)", + "Deck name input at top (editable)", + "ProgressBar: Reusable progress component with current/target props", + "Card count progress bar showing current/target from API/config", + "Validation errors displayed inline below progress bar", + "Save button (disabled if invalid or not dirty)", + "Cancel button with unsaved changes confirmation", + "Energy configuration section (collapsible, basic energy type buttons)" + ], + "styling": [ + "Page layout: sticky header with name input + actions, scrollable content below", + "Header: bg-surface border-b, flex items-center justify-between p-4", + "Deck name input: text-xl font-bold, minimal border (border-transparent focus:border-primary)", + "Two-panel: lg:flex lg:gap-6, CollectionPicker lg:w-2/3, DeckContents lg:w-1/3", + "Mobile tabs: sticky below header, bg-surface-light rounded-lg p-1, active tab bg-surface", + "CollectionPicker: bg-surface rounded-xl p-4, includes FilterBar and card grid", + "Cards in picker: show remaining quantity badge, disabled state if exhausted", + "DeckContents: bg-surface rounded-xl p-4, sticky on desktop", + "DeckCardRow: flex items-center gap-3, small thumbnail, name, quantity stepper", + "Quantity stepper: rounded-lg border, - and + buttons with number between", + "Progress bar: use 'Card Count Bar' pattern from DESIGN_REFERENCE (current/target props)", + "Validation errors: use 'Inline Error' pattern, list format if multiple", + "Save button: btn-primary, Cancel: btn-secondary", + "Energy section: collapsible with ChevronIcon, grid of energy type buttons" + ], + "acceptance": [ + "Can create new deck from scratch", + "Can edit existing deck", + "Collection filtering works in picker", + "Cannot add more cards than owned", + "Card limits enforced (from API validation)", + "Validation errors shown clearly with proper styling", + "Save/cancel work correctly with appropriate feedback", + "Unsaved changes prompt on navigation", + "Mobile tab switching works smoothly" + ] + }, + { + "id": "F2-010", + "name": "Add drag-and-drop support", + "description": "Optional drag-and-drop for adding/removing cards", + "category": "components", + "priority": 10, + "completed": true, + "tested": true, + "dependencies": ["F2-009"], + "files": [ + {"path": "src/composables/useDragDrop.ts", "status": "create"}, + {"path": "src/composables/useDragDrop.spec.ts", "status": "create"}, + {"path": "src/components/deck/DeckEditor.vue", "status": "modify"}, + {"path": "src/components/deck/CollectionPicker.vue", "status": "modify"}, + {"path": "src/components/deck/DeckContents.vue", "status": "modify"} + ], + "details": [ + "Use HTML5 Drag and Drop API (no library needed)", + "Create useDragDrop composable for drag state management", + "Track: isDragging, draggedCard, dropTarget", + "Make collection cards draggable (draggable='true')", + "DeckContents as drop target", + "Touch support: use long-press (500ms) to initiate drag on mobile", + "Fallback: click-to-add still works (drag is enhancement)", + "Drop on collection area to remove from deck (or drag out of drop zone)", + "Accessible: all actions available via click as well" + ], + "styling": [ + "Dragging card: opacity-50 on original, cursor-grabbing", + "Drag ghost: slightly rotated (rotate-3), scale-105, shadow-2xl", + "Valid drop target: ring-2 ring-primary ring-dashed, bg-primary/5", + "Invalid drop target (card limit reached): ring-2 ring-error ring-dashed, bg-error/5", + "Drop zone highlight: animate pulse when dragging over", + "Card being dragged over deck: subtle insert indicator line", + "Long-press feedback on touch: scale down slightly before drag starts", + "Transition all visual states smoothly (duration-150)" + ], + "acceptance": [ + "Can drag cards from collection to deck", + "Can drag cards out of deck to remove", + "Visual feedback clearly indicates valid/invalid drops", + "Click-to-add still works as fallback", + "Touch devices can use long-press", + "All animations are smooth" + ], + "notes": "This is an enhancement - if time constrained, click-to-add/remove is sufficient for MVP" + }, + { + "id": "F2-011", + "name": "Add routes and navigation", + "description": "Configure routes for collection and deck pages", + "category": "setup", + "priority": 11, + "completed": true, + "tested": true, + "dependencies": ["F2-006", "F2-007", "F2-009"], + "files": [ + {"path": "src/router/index.ts", "status": "modify"} + ], + "details": [ + "Add /collection route -> CollectionPage", + "Add /decks route -> DecksPage", + "Add /decks/new route -> DeckBuilderPage (create mode)", + "Add /decks/:id route -> DeckBuilderPage (edit mode)", + "All routes require auth and starter deck (meta: requiresAuth, requiresStarter)", + "Lazy load pages for code splitting", + "Add navigation guard for unsaved deck changes" + ], + "acceptance": [ + "All routes work correctly", + "Routes protected by auth", + "Lazy loading configured", + "Navigation warns about unsaved changes in deck builder" + ] + }, + { + "id": "F2-012", + "name": "Update types and API contracts", + "description": "Ensure frontend types match backend API contracts", + "category": "api", + "priority": 12, + "completed": true, + "tested": true, + "dependencies": ["F2-001"], + "files": [ + {"path": "src/types/index.ts", "status": "modify"}, + {"path": "src/types/api.ts", "status": "create"} + ], + "details": [ + "Review backend schemas: deck.py, collection.py, card.py", + "Ensure Deck, DeckCard, CollectionCard types match response shapes", + "Add API request/response types: DeckCreateRequest, DeckUpdateRequest", + "Add validation types: DeckValidationResponse, ValidationError", + "Add energy configuration types: EnergyConfig, EnergyCard", + "Export all types from index.ts", + "Document any frontend-only derived types" + ], + "acceptance": [ + "Frontend types match backend API exactly", + "All API operations are type-safe", + "No runtime type mismatches" + ] + } + ], + "apiContracts": { + "collection": { + "get": { + "endpoint": "GET /api/collections/me", + "response": { + "total_unique_cards": "number", + "total_card_count": "number", + "entries": [ + { + "card_definition_id": "string", + "quantity": "number", + "source": "string", + "obtained_at": "ISO datetime" + } + ] + } + } + }, + "decks": { + "list": { + "endpoint": "GET /api/decks", + "response": { + "decks": ["DeckResponse[]"], + "deck_count": "number", + "deck_limit": "number" + } + }, + "get": { + "endpoint": "GET /api/decks/{id}", + "response": "DeckResponse" + }, + "create": { + "endpoint": "POST /api/decks", + "request": { + "name": "string", + "description": "string | null", + "cards": "Record", + "energy_cards": "Record", + "deck_config": "DeckConfig | null" + }, + "response": "DeckResponse" + }, + "update": { + "endpoint": "PUT /api/decks/{id}", + "request": "Partial", + "response": "DeckResponse" + }, + "delete": { + "endpoint": "DELETE /api/decks/{id}", + "response": "204 No Content" + }, + "validate": { + "endpoint": "POST /api/decks/validate", + "request": { + "cards": "Record", + "energy_cards": "Record" + }, + "response": { + "is_valid": "boolean", + "errors": ["string[]"] + } + } + }, + "deckResponse": { + "id": "UUID", + "name": "string", + "description": "string | null", + "cards": "Record", + "energy_cards": "Record", + "is_valid": "boolean", + "validation_errors": ["string[]"], + "is_starter": "boolean", + "starter_type": "string | null", + "created_at": "ISO datetime", + "updated_at": "ISO datetime" + } + }, + "notes": [ + "F2-003 (starter selection) was already implemented in F1 - this phase focuses on post-starter deck management", + "Drag-and-drop (F2-010) is an enhancement - click-based editing is sufficient for MVP", + "Energy configuration may be simplified if backend doesn't require it yet", + "Card images may need placeholder/fallback handling if CDN not yet configured", + "Consider virtualized scrolling for large collections in future optimization", + "All UI tasks include 'styling' arrays - follow patterns in src/styles/DESIGN_REFERENCE.md", + "Create reusable UI components (EmptyState, SkeletonCard, ConfirmDialog, ProgressBar, FilterBar) for consistency" + ] +} diff --git a/.claude/frontend-poc/project_plans/PHASE_F3_phaser_integration.json b/.claude/frontend-poc/project_plans/PHASE_F3_phaser_integration.json new file mode 100644 index 0000000..06c5e17 --- /dev/null +++ b/.claude/frontend-poc/project_plans/PHASE_F3_phaser_integration.json @@ -0,0 +1,580 @@ +{ + "meta": { + "phaseId": "PHASE_F3", + "name": "Phaser Integration", + "version": "1.0.0", + "created": "2026-01-31", + "lastUpdated": "2026-01-31", + "totalTasks": 12, + "completedTasks": 12, + "status": "COMPLETED", + "description": "Game rendering foundation - Phaser setup, board layout, card objects, Vue-Phaser communication bridge. This phase establishes the core infrastructure for rendering TCG matches.", + "designReference": "src/styles/DESIGN_REFERENCE.md" + }, + "stylingPrinciples": [ + "Game canvas fills viewport on game page (no scrollbars during match)", + "Card objects use type-colored borders matching design system", + "All interactive elements have clear hover/focus states", + "Touch targets minimum 44px for mobile play", + "Smooth 60fps animations - use Phaser tweens, not CSS", + "Loading states with progress indicator during asset loading", + "Responsive scaling maintains playable experience on all devices" + ], + "dependencies": { + "phases": ["PHASE_F0", "PHASE_F1", "PHASE_F2"], + "backend": [ + "POST /api/games - Create new game (for testing)", + "GET /api/games/{id} - Get game info", + "WebSocket game:state events - For state rendering tests" + ], + "external": [ + "Phaser 3 library", + "Card image assets (placeholder/CDN)" + ] + }, + "architectureNotes": { + "vuePhaser": { + "principle": "Phaser handles RENDERING ONLY. All game logic lives on backend.", + "pattern": "Vue component mounts Phaser game, communicates via typed EventEmitter bridge", + "state": "Pinia game store is source of truth. Phaser reads from store, never modifies.", + "cleanup": "Phaser game instance destroyed on component unmount to prevent memory leaks" + }, + "eventBridge": { + "vueToPhaser": [ + "game:state_updated - New state from server, Phaser should re-render", + "card:highlight - Highlight specific card (for tutorials, hints)", + "animation:request - Request Phaser to play specific animation", + "resize - Viewport resized, Phaser should rescale" + ], + "phaserToVue": [ + "card:clicked - User clicked a card (cardId, zone)", + "zone:clicked - User clicked a zone (zoneType, slotIndex)", + "animation:complete - Animation finished, Vue can proceed", + "ready - Phaser scene is ready and loaded" + ] + }, + "sceneFlow": { + "PreloadScene": "Load all required assets, show progress bar", + "MatchScene": "Main gameplay rendering, handles all card/board display" + }, + "fileStructure": { + "src/game/": "All Phaser code lives here", + "src/game/config.ts": "Phaser game configuration", + "src/game/scenes/": "Scene classes", + "src/game/objects/": "Game objects (Card, Board, Zone)", + "src/game/bridge.ts": "Vue-Phaser event bridge", + "src/game/assets/": "Asset loading utilities", + "src/game/layout.ts": "Board layout calculations", + "src/game/scale.ts": "Responsive scaling logic" + } + }, + "tasks": [ + { + "id": "F3-001", + "name": "Install and configure Phaser", + "description": "Add Phaser 3 to the project with proper TypeScript configuration", + "category": "setup", + "priority": 1, + "completed": true, + "tested": true, + "dependencies": [], + "files": [ + {"path": "package.json", "status": "modify"}, + {"path": "src/game/config.ts", "status": "create"}, + {"path": "src/game/index.ts", "status": "create"}, + {"path": "tsconfig.json", "status": "modify"} + ], + "details": [ + "Install phaser: npm install phaser", + "Create src/game/config.ts with Phaser.Types.Core.GameConfig", + "Configure WebGL renderer with canvas fallback", + "Set transparent: true to allow CSS background styling", + "Configure physics: 'arcade' (minimal, for tween support)", + "Set parent element ID for mounting ('phaser-game')", + "Configure scale mode: Phaser.Scale.RESIZE for responsive", + "Export createGame(container: HTMLElement) factory function", + "Add DOM config to allow DOM elements in Phaser if needed" + ], + "acceptance": [ + "Phaser installed and importable", + "Config creates valid Phaser game instance", + "TypeScript types work correctly", + "No console errors on import" + ], + "estimatedHours": 1 + }, + { + "id": "F3-002", + "name": "Create PhaserGame Vue component", + "description": "Vue component that mounts and manages Phaser game lifecycle", + "category": "components", + "priority": 2, + "completed": true, + "tested": true, + "dependencies": ["F3-001"], + "files": [ + {"path": "src/components/game/PhaserGame.vue", "status": "create"}, + {"path": "src/components/game/PhaserGame.spec.ts", "status": "create"} + ], + "details": [ + "Template: single div with ref='container' for Phaser mounting", + "Props: none initially (scenes loaded via config)", + "Emits: ready, error", + "onMounted: create Phaser game instance with createGame(container)", + "onUnmounted: call game.destroy(true) to cleanup", + "Expose game instance via defineExpose for parent access", + "Handle creation errors with error emit and console.error", + "Use CSS: width: 100%, height: 100%, position: relative", + "Add ResizeObserver to detect container size changes", + "Emit 'ready' when Phaser emits READY event" + ], + "styling": [ + "Container: w-full h-full relative overflow-hidden", + "Canvas will be absolutely positioned by Phaser", + "Background: bg-background (matches design system)" + ], + "acceptance": [ + "Component renders empty Phaser canvas", + "Game instance properly destroyed on unmount", + "No memory leaks (verified via dev tools)", + "ready event fires when Phaser initializes", + "Test verifies mount/unmount lifecycle" + ], + "estimatedHours": 2 + }, + { + "id": "F3-003", + "name": "Create Vue-Phaser event bridge", + "description": "Typed bidirectional communication system between Vue and Phaser", + "category": "composables", + "priority": 3, + "completed": true, + "tested": true, + "dependencies": ["F3-002"], + "files": [ + {"path": "src/game/bridge.ts", "status": "create"}, + {"path": "src/game/bridge.spec.ts", "status": "create"}, + {"path": "src/composables/useGameBridge.ts", "status": "create"}, + {"path": "src/composables/useGameBridge.spec.ts", "status": "create"} + ], + "details": [ + "Create GameBridge class extending Phaser.Events.EventEmitter", + "Define typed events interface: GameBridgeEvents", + "Vue->Phaser events: state_updated, card_highlight, animation_request, resize", + "Phaser->Vue events: card_clicked, zone_clicked, animation_complete, ready, error", + "Export singleton: gameBridge = new GameBridge()", + "Create useGameBridge() composable for Vue components", + "Composable provides: emit(event, data), on(event, handler), off(event, handler)", + "Composable auto-cleans up listeners on component unmount (onUnmounted)", + "Phaser scenes access gameBridge directly via import", + "All event data types explicitly defined in bridge.ts" + ], + "acceptance": [ + "Events flow Vue -> Phaser correctly", + "Events flow Phaser -> Vue correctly", + "TypeScript enforces correct event payloads", + "Listeners auto-removed on Vue component unmount", + "Tests verify bidirectional communication" + ], + "estimatedHours": 3 + }, + { + "id": "F3-004", + "name": "Create PreloadScene", + "description": "Phaser scene for loading game assets with progress display", + "category": "game", + "priority": 4, + "completed": true, + "tested": true, + "dependencies": ["F3-001"], + "files": [ + {"path": "src/game/scenes/PreloadScene.ts", "status": "create"}, + {"path": "src/game/assets/manifest.ts", "status": "create"}, + {"path": "src/game/assets/loader.ts", "status": "create"} + ], + "details": [ + "Extend Phaser.Scene with key 'PreloadScene'", + "preload(): Load assets defined in manifest.ts", + "Assets to load: card back image, board background, UI sprites, type icons", + "Create loading progress bar using Phaser graphics", + "Listen to 'progress' event to update progress bar", + "Listen to 'complete' event to transition to MatchScene", + "manifest.ts: Export ASSET_MANIFEST with paths and keys", + "loader.ts: Helper functions for dynamic card image loading", + "Card images loaded lazily in MatchScene (not all upfront)", + "Handle load errors gracefully with fallback placeholder" + ], + "acceptance": [ + "Progress bar displays during loading", + "All core assets load successfully", + "Scene transitions to MatchScene after load", + "Load errors handled without crash", + "Progress reaches 100% before transition" + ], + "estimatedHours": 3, + "notes": "Card images may use placeholders initially if CDN not configured" + }, + { + "id": "F3-005", + "name": "Create MatchScene foundation", + "description": "Main game scene that renders the board and responds to state changes", + "category": "game", + "priority": 5, + "completed": true, + "tested": true, + "dependencies": ["F3-003", "F3-004"], + "files": [ + {"path": "src/game/scenes/MatchScene.ts", "status": "create"}, + {"path": "src/game/scenes/index.ts", "status": "create"} + ], + "details": [ + "Extend Phaser.Scene with key 'MatchScene'", + "create(): Set up board layout, subscribe to bridge events", + "Subscribe to gameBridge 'state_updated' event", + "Implement renderState(gameState: GameState) method", + "Track game objects in Maps: cards, zones, etc.", + "update(): Minimal - most updates are event-driven", + "Handle resize events from bridge to rescale board", + "Emit 'ready' to bridge when scene is fully initialized", + "Implement clearBoard() for state reset", + "scenes/index.ts: Export array of scene classes for game config" + ], + "acceptance": [ + "Scene creates and displays empty board area", + "Scene responds to state_updated events", + "Resize events properly handled", + "ready event emitted after initialization", + "Scene can be destroyed and recreated cleanly" + ], + "estimatedHours": 4 + }, + { + "id": "F3-006", + "name": "Create board layout system", + "description": "Calculate positions for all game zones based on screen size", + "category": "game", + "priority": 6, + "completed": true, + "tested": true, + "dependencies": ["F3-005"], + "files": [ + {"path": "src/game/layout.ts", "status": "create"}, + {"path": "src/game/layout.spec.ts", "status": "create"}, + {"path": "src/game/objects/Board.ts", "status": "create"} + ], + "details": [ + "Define BoardLayout interface with zone positions", + "Zones: myActive, myBench (5 slots), myDeck, myDiscard, myPrizes (6)", + "Mirror zones for opponent: oppActive, oppBench, oppDeck, oppDiscard, oppPrizes", + "Hand zone: bottom of screen, fan layout", + "calculateLayout(width: number, height: number): BoardLayout", + "Layout adapts to portrait (mobile) vs landscape (desktop)", + "Each zone has: x, y, width, height, angle (for fanning)", + "Board.ts: Game object that renders zone backgrounds/outlines", + "Highlight zones when they're valid drop/play targets", + "Zone positions exported as constants for consistent reference" + ], + "styling": [ + "Zone outlines: subtle border, slightly lighter than background", + "Active zones: prominent position, larger card area", + "Bench: horizontal row of 5 slots with spacing", + "Prizes: 2x3 grid or stacked display", + "Opponent zones: mirrored at top of screen (inverted orientation)" + ], + "acceptance": [ + "Layout calculation produces valid positions", + "All zones have distinct, non-overlapping areas", + "Layout works for both portrait and landscape", + "Board renders zone outlines correctly", + "Tests verify layout calculations" + ], + "estimatedHours": 4 + }, + { + "id": "F3-007", + "name": "Create Card game object", + "description": "Phaser game object representing a card that can be displayed and interacted with", + "category": "game", + "priority": 7, + "completed": true, + "tested": true, + "dependencies": ["F3-005"], + "files": [ + {"path": "src/game/objects/Card.ts", "status": "create"}, + {"path": "src/game/objects/CardBack.ts", "status": "create"}, + {"path": "src/game/objects/DamageCounter.ts", "status": "create"} + ], + "details": [ + "Card extends Phaser.GameObjects.Container", + "Contains: card image sprite, type border, optional damage counter", + "Constructor params: scene, x, y, cardData (from game state)", + "setCard(cardData): Update displayed card", + "setFaceDown(isFaceDown): Toggle card back vs face", + "CardBack: Simple game object for face-down cards", + "DamageCounter: Displays damage on card (red circle with number)", + "Interactive: setInteractive(), on('pointerdown'), on('pointerover'), etc.", + "Click emits event to bridge: card_clicked with cardId and zone", + "Support multiple sizes: hand (medium), board (large), thumbnail (small)", + "Lazy load card images - use placeholder until image loads" + ], + "styling": [ + "Card border: 3px colored border matching card type", + "Hover: scale to 1.05, slight shadow, raise z-index", + "Selected: pulsing glow effect (tween)", + "Disabled: grayscale + reduced alpha", + "Damage counter: red circle, white text, positioned bottom-right" + ], + "acceptance": [ + "Card displays correctly with image and border", + "Face-down cards show card back", + "Click events fire and reach bridge", + "Hover effects work on desktop", + "Damage counter displays when card has damage" + ], + "estimatedHours": 5 + }, + { + "id": "F3-008", + "name": "Create zone game objects", + "description": "Game objects for each board zone that contain and arrange cards", + "category": "game", + "priority": 8, + "completed": true, + "tested": true, + "dependencies": ["F3-006", "F3-007"], + "files": [ + {"path": "src/game/objects/Zone.ts", "status": "create"}, + {"path": "src/game/objects/ActiveZone.ts", "status": "create"}, + {"path": "src/game/objects/BenchZone.ts", "status": "create"}, + {"path": "src/game/objects/HandZone.ts", "status": "create"}, + {"path": "src/game/objects/PileZone.ts", "status": "create"}, + {"path": "src/game/objects/PrizeZone.ts", "status": "create"} + ], + "details": [ + "Zone: Base class extending Phaser.GameObjects.Container", + "Zone tracks contained Card objects", + "ActiveZone: Single card slot, larger display", + "BenchZone: Row of 5 card slots, evenly spaced", + "HandZone: Fan of cards at bottom, cards slightly overlap", + "PileZone: For deck/discard - shows top card and count", + "PrizeZone: 6 face-down cards in 2x3 grid", + "setCards(cards[]): Update zone contents, animate changes", + "highlight(enabled: boolean): Visual indicator for valid targets", + "Each zone type handles its own card arrangement logic", + "Zones are interactive for targeting (emit zone_clicked)" + ], + "acceptance": [ + "All zone types render correctly", + "Zones arrange cards appropriately", + "Zone highlights toggle correctly", + "Zone click events reach bridge", + "Card updates animate smoothly" + ], + "estimatedHours": 6 + }, + { + "id": "F3-009", + "name": "Implement responsive canvas scaling", + "description": "Make game canvas scale properly to any screen size", + "category": "game", + "priority": 9, + "completed": true, + "tested": true, + "dependencies": ["F3-006"], + "files": [ + {"path": "src/game/scale.ts", "status": "create"}, + {"path": "src/game/scale.spec.ts", "status": "create"}, + {"path": "src/game/config.ts", "status": "modify"} + ], + "details": [ + "Use Phaser.Scale.RESIZE mode (or FIT for fixed aspect)", + "Define design resolution: 1920x1080 (landscape base)", + "calculateScale(width, height): Get scale factor and offsets", + "Handle orientation changes on mobile", + "ResizeManager class to coordinate resize events", + "Emit resize event to bridge when size changes", + "Debounce resize handling (100ms) to prevent excessive recalc", + "Update BoardLayout on resize", + "Ensure touch areas scale appropriately (min 44px equivalent)" + ], + "acceptance": [ + "Canvas scales to fill container", + "Aspect ratio maintained (letterbox if needed)", + "Resize events trigger layout recalculation", + "Touch targets remain usable on mobile", + "Tests verify scale calculations" + ], + "estimatedHours": 3 + }, + { + "id": "F3-010", + "name": "Implement state rendering", + "description": "Render game state from Pinia store to Phaser scene", + "category": "game", + "priority": 10, + "completed": true, + "tested": true, + "dependencies": ["F3-008"], + "files": [ + {"path": "src/game/sync/StateRenderer.ts", "status": "create"}, + {"path": "src/game/sync/index.ts", "status": "create"} + ], + "details": [ + "StateRenderer class coordinates state -> scene updates", + "render(state: GameState): Main entry point", + "Compare previous state to new state to minimize updates", + "Update my zones: active, bench, hand, deck, discard, prizes", + "Update opponent zones: active, bench, deck (count), discard (count), prizes", + "Handle visibility: opponent hand shows card backs only", + "updateCard(card, zone): Position card in correct zone", + "createCard(card): Create new Card game object", + "removeCard(cardId): Destroy Card game object", + "Track cards by ID in Map for efficient updates" + ], + "acceptance": [ + "State renders correctly to scene", + "All zones populated from state", + "Opponent hidden zones show appropriate info", + "State updates are efficient (diff-based)", + "Cards created/removed as state changes" + ], + "estimatedHours": 5 + }, + { + "id": "F3-011", + "name": "Create GamePage with Phaser integration", + "description": "Vue page that hosts the Phaser game during matches", + "category": "pages", + "priority": 11, + "completed": true, + "tested": true, + "dependencies": ["F3-002", "F3-003", "F3-010"], + "files": [ + {"path": "src/pages/GamePage.vue", "status": "create"}, + {"path": "src/pages/GamePage.spec.ts", "status": "create"}, + {"path": "src/router/index.ts", "status": "modify"} + ], + "details": [ + "Full viewport page (no nav) using game layout", + "Mount PhaserGame component filling container", + "Route: /game/:id with id param", + "On mount: connect to socket, join game room", + "Subscribe to game:state events -> update store", + "Watch game store state -> emit to bridge", + "Handle game:error events with toast/overlay", + "Handle disconnect/reconnect (overlay during reconnect)", + "Exit button -> confirm, leave game, navigate away", + "Route guard: redirect if not authenticated" + ], + "styling": [ + "Container: fixed inset-0, bg-background", + "No scrolling, overflow-hidden", + "Exit button: absolute top-right, icon-only", + "Reconnecting overlay: centered with spinner" + ], + "acceptance": [ + "Page renders Phaser game full viewport", + "WebSocket connection established", + "Game state flows to Phaser", + "Can exit game with confirmation", + "Auth guard works correctly" + ], + "estimatedHours": 4 + }, + { + "id": "F3-012", + "name": "Add game types and API types", + "description": "TypeScript types for game state, events, and Phaser integration", + "category": "api", + "priority": 12, + "completed": true, + "tested": true, + "dependencies": [], + "files": [ + {"path": "src/types/game.ts", "status": "create"}, + {"path": "src/types/phaser.ts", "status": "create"}, + {"path": "src/types/index.ts", "status": "modify"} + ], + "details": [ + "game.ts: GameState, Player, Card, Zone, Phase types (match backend)", + "Card types: id, definitionId, name, hp, currentHp, type, attacks, etc.", + "Zone types: ZoneType enum, ZoneState interface", + "Phase types: TurnPhase enum (draw, main, attack, end)", + "phaser.ts: GameBridgeEvents interface for typed bridge events", + "phaser.ts: CardClickEvent, ZoneClickEvent payloads", + "phaser.ts: Scene data interfaces for scene transitions", + "Ensure types align with backend/app/schemas/game.py", + "Export all from types/index.ts" + ], + "acceptance": [ + "All game types defined and exported", + "Types match backend API contracts", + "Bridge events fully typed", + "No any types in game code" + ], + "estimatedHours": 2, + "notes": "This task can be done early and in parallel with others" + } + ], + "testingApproach": { + "unitTests": [ + "Bridge event emission and reception", + "Layout calculations for various screen sizes", + "Scale calculations", + "State renderer diffing logic" + ], + "componentTests": [ + "PhaserGame mount/unmount lifecycle", + "GamePage integration with mocked store/socket" + ], + "manualTests": [ + "Visual inspection of board layout", + "Card rendering at different sizes", + "Touch interactions on mobile", + "Resize behavior", + "Scene transitions" + ], + "note": "Phaser rendering is hard to unit test - focus on logic tests and manual visual verification" + }, + "assetRequirements": { + "required": [ + {"name": "card_back.png", "size": "500x700", "description": "Generic card back for face-down cards"}, + {"name": "board_background.png", "size": "1920x1080", "description": "Optional board background texture"} + ], + "optional": [ + {"name": "type_icons.png", "description": "Sprite atlas of type icons"}, + {"name": "ui_sprites.png", "description": "UI elements (buttons, indicators)"} + ], + "cardImages": { + "source": "CDN or local /public/cards/ directory", + "pattern": "{setId}/{cardNumber}.png or {cardDefinitionId}.png", + "fallback": "placeholder_card.png for missing images" + } + }, + "riskMitigation": [ + { + "risk": "Phaser learning curve", + "mitigation": "Start with F3-001 and F3-002 to get basic rendering working quickly. Reference Phaser docs and examples." + }, + { + "risk": "Vue-Phaser state sync complexity", + "mitigation": "Keep bridge events minimal and explicit. Phaser only reads state, never modifies." + }, + { + "risk": "Memory leaks from game objects", + "mitigation": "Always call destroy() on game objects when removing. Test with Chrome DevTools memory profiler." + }, + { + "risk": "Mobile performance", + "mitigation": "Use object pooling for cards if performance issues arise. Lazy load card images." + } + ], + "notes": [ + "F3-012 (types) can be worked on early and in parallel", + "Card images may use placeholders until CDN is configured", + "This phase focuses on RENDERING infrastructure - actual gameplay interactions come in F4", + "All Phaser code should be in src/game/ to keep it isolated from Vue code", + "The game store already exists from F0 - this phase adds the Phaser visualization layer", + "Consider using Phaser's built-in tween system for all animations (smoother than CSS)" + ] +} diff --git a/.claude/frontend-poc/project_plans/TASK_F3-012_types_detailed.md b/.claude/frontend-poc/project_plans/TASK_F3-012_types_detailed.md new file mode 100644 index 0000000..e8723df --- /dev/null +++ b/.claude/frontend-poc/project_plans/TASK_F3-012_types_detailed.md @@ -0,0 +1,2167 @@ +# F3-012: Game Types and API Types - Detailed Implementation Plan + +**Phase:** F3 - Phaser Integration +**Task ID:** F3-012 +**Status:** Planned +**Estimated Time:** ~3 hours +**Created:** 2026-01-31 + +## Overview + +Align frontend TypeScript types with backend Pydantic schemas for game state, WebSocket messages, and Phaser bridge communication. This task establishes the type foundation for all game-related features. + +### Key Decisions + +1. **Full replacement** of old types (no backward compatibility aliases) - nothing in production yet +2. **Include RulesConfig** for game creation +3. **DTO approach** - define minimal types for display needs, expand as needed + +### Files to Create/Modify + +| File | Action | Purpose | +|------|--------|---------| +| `src/types/rules.ts` | Create | RulesConfig and sub-configs | +| `src/types/game.ts` | Create | Core game state types aligned with backend | +| `src/types/phaser.ts` | Create | Phaser bridge event types | +| `src/socket/types.ts` | Replace | Align with backend WS messages | +| `src/types/api.ts` | Modify | Add game API types | +| `src/types/index.ts` | Modify | Update exports | +| `src/stores/game.ts` | Modify | Update to use new types | + +### Backend Reference Files + +| Backend File | Frontend Equivalent | +|--------------|---------------------| +| `app/core/config.py` | `src/types/rules.ts` | +| `app/core/visibility.py` | `src/types/game.ts` | +| `app/core/enums.py` | Enums in `src/types/game.ts` | +| `app/core/models/card.py` | `CardDefinition`, `CardInstance` in `src/types/game.ts` | +| `app/schemas/ws_messages.py` | `src/socket/types.ts` | +| `app/schemas/game.py` | Game API types in `src/types/api.ts` | + +--- + +## Task 1: Create `src/types/rules.ts` - Game Rules Configuration + +**Goal:** Define TypeScript equivalents of backend's `RulesConfig` from `app/core/config.py`. + +**Estimated Time:** 30 min + +### Types to Define + +```typescript +/** + * Game rules configuration types. + * + * These types mirror the backend's RulesConfig from app/core/config.py. + * The frontend sends these to the backend when creating games to specify + * which rules should apply (campaign mode, freeplay, custom, etc.). + */ + +import type { EnergyType } from './game' + +// ============================================================================= +// Modifier Mode +// ============================================================================= + +/** + * How damage modifiers (weakness/resistance) are calculated. + * - multiplicative: Multiply damage by value (e.g., x2 for weakness) + * - additive: Add value to damage (e.g., +20 or -30) + */ +export type ModifierMode = 'multiplicative' | 'additive' + +// ============================================================================= +// Sub-Configurations +// ============================================================================= + +/** + * Configuration for deck building rules. + */ +export interface DeckConfig { + /** Minimum number of cards in the main deck */ + min_size: number + /** Maximum number of cards in the main deck */ + max_size: number + /** If true, deck must be exactly min_size cards */ + exact_size_required: boolean + /** Maximum copies of any single card (by name) */ + max_copies_per_card: number + /** Max copies of basic energy in energy deck. null = unlimited */ + max_copies_basic_energy: number | null + /** Minimum number of Basic Pokemon required */ + min_basic_pokemon: number + /** If true, use separate energy deck (Pokemon Pocket style) */ + energy_deck_enabled: boolean + /** Size of the separate energy deck */ + energy_deck_size: number + /** Number of cards drawn at game start */ + starting_hand_size: number +} + +/** + * Configuration for active Pokemon slot rules. + */ +export interface ActiveConfig { + /** Maximum Pokemon in active position. 1 = standard, 2 = double battle */ + max_active: number +} + +/** + * Configuration for bench rules. + */ +export interface BenchConfig { + /** Maximum number of Pokemon on the bench */ + max_size: number +} + +/** + * Configuration for energy attachment rules. + */ +export interface EnergyConfig { + /** Number of energy cards that can be attached per turn */ + attachments_per_turn: number + /** List of energy types available in this game */ + types_enabled: EnergyType[] + /** If true, flip top card of energy deck at turn start */ + auto_flip_from_deck: boolean +} + +/** + * Configuration for prize/scoring rules. + * + * In Mantimon TCG, "prizes" are replaced with "points" - players score + * points instead of taking prize cards. + */ +export interface PrizeConfig { + /** Number of points needed to win */ + count: number + /** Points for knocking out a normal Pokemon */ + per_knockout_normal: number + /** Points for knocking out an EX Pokemon */ + per_knockout_ex: number + /** Points for knocking out a GX Pokemon */ + per_knockout_gx: number + /** Points for knocking out a V Pokemon */ + per_knockout_v: number + /** Points for knocking out a VMAX Pokemon */ + per_knockout_vmax: number + /** Points for knocking out a VSTAR Pokemon */ + per_knockout_vstar: number + /** Points for knocking out a Radiant Pokemon */ + per_knockout_radiant: number + /** Points for knocking out a Prism Star Pokemon */ + per_knockout_prism_star: number + /** If true, use classic prize card mechanic instead of points */ + use_prize_cards: boolean + /** If true, prize cards are taken randomly (classic) */ + prize_selection_random: boolean +} + +/** + * Configuration for first turn restrictions. + * These apply only to the very first turn of the game. + */ +export interface FirstTurnConfig { + /** Whether the first player draws a card on turn 1 */ + can_draw: boolean + /** Whether the first player can attack on turn 1 */ + can_attack: boolean + /** Whether the first player can play Supporter cards on turn 1 */ + can_play_supporter: boolean + /** Whether the first player can attach energy on turn 1 */ + can_attach_energy: boolean + /** Whether the first player can evolve Pokemon on turn 1 */ + can_evolve: boolean +} + +/** + * Configuration for win/loss conditions. + */ +export interface WinConditionsConfig { + /** Win when a player scores the required number of points */ + all_prizes_taken: boolean + /** Win when opponent has no Pokemon in play */ + no_pokemon_in_play: boolean + /** Win when opponent cannot draw a card at turn start */ + cannot_draw: boolean + /** Enable maximum turn count */ + turn_limit_enabled: boolean + /** Maximum number of turns before game ends */ + turn_limit: number + /** Enable per-turn time limits */ + turn_timer_enabled: boolean + /** Seconds per turn before timeout */ + turn_timer_seconds: number + /** Percentage thresholds to send warnings (e.g., [50, 25]) */ + turn_timer_warning_thresholds: number[] + /** Extra seconds granted on reconnection */ + turn_timer_grace_seconds: number + /** Enable total game time limit */ + game_timer_enabled: boolean + /** Total game time in minutes */ + game_timer_minutes: number +} + +/** + * Configuration for status condition effects. + */ +export interface StatusConfig { + /** Damage dealt by Poison between turns */ + poison_damage: number + /** Damage dealt by Burn between turns */ + burn_damage: number + /** Flip coin between turns; heads removes Burn */ + burn_flip_to_remove: boolean + /** Flip coin between turns; heads removes Sleep */ + sleep_flip_to_wake: boolean + /** Damage dealt to self on failed confusion flip */ + confusion_self_damage: number +} + +/** + * Configuration for Trainer card rules. + */ +export interface TrainerConfig { + /** Maximum Supporter cards playable per turn */ + supporters_per_turn: number + /** Maximum Stadium cards playable per turn */ + stadiums_per_turn: number + /** Maximum Item cards per turn. null = unlimited */ + items_per_turn: number | null + /** Maximum Tool cards attachable to one Pokemon */ + tools_per_pokemon: number + /** Can a stadium replace another stadium with the same name */ + stadium_same_name_replace: boolean +} + +/** + * Configuration for evolution rules. + */ +export interface EvolutionConfig { + /** Can evolve a Pokemon the same turn it was played */ + same_turn_as_played: boolean + /** Can evolve a Pokemon the same turn it evolved */ + same_turn_as_evolution: boolean + /** Can evolve on the very first turn of the game */ + first_turn_of_game: boolean +} + +/** + * Configuration for retreat rules. + */ +export interface RetreatConfig { + /** Maximum number of retreats allowed per turn */ + retreats_per_turn: number + /** If true, retreating doesn't require discarding energy */ + free_retreat_cost: boolean +} + +/** + * Configuration for combat damage calculations. + */ +export interface CombatConfig { + /** How weakness modifies damage */ + weakness_mode: ModifierMode + /** Default weakness modifier value */ + weakness_value: number + /** How resistance modifies damage */ + resistance_mode: ModifierMode + /** Default resistance modifier value */ + resistance_value: number +} + +// ============================================================================= +// Master Configuration +// ============================================================================= + +/** + * Master configuration for all game rules. + * + * Default values are based on Mantimon TCG house rules (Pokemon Pocket-inspired + * with 40-card decks, separate energy deck, and 4 points to win). + */ +export interface RulesConfig { + deck: DeckConfig + active: ActiveConfig + bench: BenchConfig + energy: EnergyConfig + prizes: PrizeConfig + first_turn: FirstTurnConfig + win_conditions: WinConditionsConfig + status: StatusConfig + trainer: TrainerConfig + evolution: EvolutionConfig + retreat: RetreatConfig + combat: CombatConfig +} + +// ============================================================================= +// Factory Functions +// ============================================================================= + +/** + * Create default Mantimon TCG rules configuration. + * + * 40-card deck, 20-card energy deck, 4 points to win. + */ +export function createDefaultRulesConfig(): RulesConfig { + return { + deck: { + min_size: 40, + max_size: 40, + exact_size_required: true, + max_copies_per_card: 4, + max_copies_basic_energy: null, + min_basic_pokemon: 1, + energy_deck_enabled: true, + energy_deck_size: 20, + starting_hand_size: 7, + }, + active: { + max_active: 1, + }, + bench: { + max_size: 5, + }, + energy: { + attachments_per_turn: 1, + types_enabled: [ + 'colorless', 'darkness', 'dragon', 'fighting', 'fire', + 'grass', 'lightning', 'metal', 'psychic', 'water', + ], + auto_flip_from_deck: true, + }, + prizes: { + count: 4, + per_knockout_normal: 1, + per_knockout_ex: 2, + per_knockout_gx: 2, + per_knockout_v: 2, + per_knockout_vmax: 3, + per_knockout_vstar: 3, + per_knockout_radiant: 2, + per_knockout_prism_star: 2, + use_prize_cards: false, + prize_selection_random: true, + }, + first_turn: { + can_draw: true, + can_attack: true, + can_play_supporter: true, + can_attach_energy: false, + can_evolve: false, + }, + win_conditions: { + all_prizes_taken: true, + no_pokemon_in_play: true, + cannot_draw: true, + turn_limit_enabled: true, + turn_limit: 30, + turn_timer_enabled: false, + turn_timer_seconds: 90, + turn_timer_warning_thresholds: [50, 25], + turn_timer_grace_seconds: 15, + game_timer_enabled: false, + game_timer_minutes: 30, + }, + status: { + poison_damage: 10, + burn_damage: 20, + burn_flip_to_remove: true, + sleep_flip_to_wake: true, + confusion_self_damage: 30, + }, + trainer: { + supporters_per_turn: 1, + stadiums_per_turn: 1, + items_per_turn: null, + tools_per_pokemon: 1, + stadium_same_name_replace: false, + }, + evolution: { + same_turn_as_played: false, + same_turn_as_evolution: false, + first_turn_of_game: false, + }, + retreat: { + retreats_per_turn: 1, + free_retreat_cost: false, + }, + combat: { + weakness_mode: 'multiplicative', + weakness_value: 2, + resistance_mode: 'additive', + resistance_value: -30, + }, + } +} + +/** + * Create standard Pokemon TCG rules configuration. + * + * 60-card deck, prize cards, no first-turn attack. + */ +export function createStandardPokemonTCGConfig(): RulesConfig { + const config = createDefaultRulesConfig() + return { + ...config, + deck: { + ...config.deck, + min_size: 60, + max_size: 60, + energy_deck_enabled: false, + }, + prizes: { + ...config.prizes, + count: 6, + use_prize_cards: true, + }, + first_turn: { + ...config.first_turn, + can_attack: false, + can_play_supporter: false, + can_attach_energy: true, + }, + } +} +``` + +--- + +## Task 2: Create `src/types/game.ts` - Core Game State Types + +**Goal:** Define types matching backend's `app/core/visibility.py`. + +**Estimated Time:** 45 min + +### Types to Define + +```typescript +/** + * Core game state types for Mantimon TCG. + * + * These types align with the backend's VisibleGameState from app/core/visibility.py. + * The backend filters game state for each player's view (hiding opponent's hand, etc.) + * and sends this structure via WebSocket. + */ + +import type { ModifierMode } from './rules' + +// ============================================================================= +// Enums and Constants +// ============================================================================= + +/** + * Zone types in the game. + */ +export type ZoneType = + | 'hand' + | 'active' + | 'bench' + | 'deck' + | 'discard' + | 'prizes' + | 'energy_zone' + | 'energy_deck' + +/** + * Turn phases within a player's turn. + */ +export type TurnPhase = 'setup' | 'draw' | 'main' | 'attack' | 'end' + +/** + * Reasons why a game ended. + */ +export type GameEndReason = + | 'prizes_taken' + | 'no_pokemon' + | 'deck_empty' + | 'resignation' + | 'timeout' + | 'turn_limit' + | 'draw' + +/** + * Status conditions that can affect Pokemon in play. + */ +export type StatusCondition = + | 'poisoned' + | 'burned' + | 'asleep' + | 'paralyzed' + | 'confused' + +/** + * Primary card type. + */ +export type CardType = 'pokemon' | 'trainer' | 'energy' + +/** + * Evolution stage of a Pokemon card. + */ +export type PokemonStage = 'basic' | 'stage_1' | 'stage_2' + +/** + * Special variant classification for Pokemon cards. + * Affects knockout points. + */ +export type PokemonVariant = + | 'normal' + | 'ex' + | 'gx' + | 'v' + | 'vmax' + | 'vstar' + | 'radiant' + | 'prism_star' + +/** + * Subtypes of Trainer cards. + */ +export type TrainerType = 'item' | 'supporter' | 'stadium' | 'tool' + +/** + * Energy types available in the game. + */ +export type EnergyType = + | 'colorless' + | 'darkness' + | 'dragon' + | 'fighting' + | 'fire' + | 'grass' + | 'lightning' + | 'metal' + | 'psychic' + | 'water' + +/** + * All valid energy type values for iteration. + */ +export const ENERGY_TYPES: readonly EnergyType[] = [ + 'colorless', + 'darkness', + 'dragon', + 'fighting', + 'fire', + 'grass', + 'lightning', + 'metal', + 'psychic', + 'water', +] as const + +// ============================================================================= +// Card Types +// ============================================================================= + +/** + * An attack that a Pokemon can use. + */ +export interface Attack { + /** Display name of the attack */ + name: string + /** Energy cost to use this attack */ + cost: EnergyType[] + /** Base damage dealt */ + damage: number + /** UI display for variable damage (e.g., "30+", "50x") */ + damage_display?: string + /** Effect handler ID for special effects */ + effect_id?: string + /** Human-readable description of the effect */ + effect_description?: string + /** If true, this is a GX attack (once per game) */ + is_gx_attack?: boolean +} + +/** + * A special ability that a Pokemon has. + */ +export interface Ability { + /** Display name of the ability */ + name: string + /** Effect handler ID */ + effect_id: string + /** Human-readable description of the ability */ + effect_description?: string + /** Maximum uses per turn. null = unlimited */ + uses_per_turn?: number + /** If true, this is a VSTAR Power (once per game) */ + is_vstar_power?: boolean +} + +/** + * Weakness or resistance to a specific energy type. + */ +export interface WeaknessResistance { + /** The energy type this applies to */ + energy_type: EnergyType + /** How to apply the modifier */ + mode?: ModifierMode + /** The modifier value */ + value?: number +} + +/** + * Card definition - the immutable template for a card. + * + * This is a DTO containing the minimal fields needed for display. + * Additional fields can be added as needed. + */ +export interface CardDefinition { + /** Unique card identifier (e.g., "pikachu_base_001") */ + id: string + /** Display name of the card */ + name: string + /** Primary card type */ + card_type: CardType + /** URL to card image */ + image_url?: string + /** Set identifier */ + set_id?: string + /** Card number within the set */ + set_number?: number + + // Pokemon-specific fields + /** Evolution stage */ + stage?: PokemonStage + /** Special variant (EX, V, etc.) */ + variant?: PokemonVariant + /** Hit points */ + hp?: number + /** Pokemon's energy type */ + pokemon_type?: EnergyType + /** Available attacks */ + attacks?: Attack[] + /** Available abilities */ + abilities?: Ability[] + /** Weakness to an energy type */ + weakness?: WeaknessResistance + /** Resistance to an energy type */ + resistance?: WeaknessResistance + /** Number of energy to discard to retreat */ + retreat_cost?: number + /** Name of Pokemon this evolves from */ + evolves_from?: string + + // Trainer-specific fields + /** Subtype of trainer card */ + trainer_type?: TrainerType + /** Human-readable effect description */ + effect_description?: string + + // Energy-specific fields + /** Energy type provided by this card */ + energy_type?: EnergyType +} + +/** + * Card instance - a specific copy of a card during gameplay. + * + * Contains mutable state like damage, attached energy, and status effects. + * References a CardDefinition by definition_id. + */ +export interface CardInstance { + /** Unique instance ID for this game */ + instance_id: string + /** References CardDefinition.id */ + definition_id: string + /** Current damage on the card */ + damage: number + /** Attached energy cards */ + attached_energy: CardInstance[] + /** Attached tool cards */ + attached_tools: CardInstance[] + /** Active status conditions */ + status_conditions: StatusCondition[] + /** Ability uses this turn (ability_id -> use_count) */ + ability_uses_this_turn: Record + /** Turn number when this card was evolved (null if not evolved) */ + evolution_turn: number | null +} + +// ============================================================================= +// Zone Types +// ============================================================================= + +/** + * A zone with visibility-appropriate information. + * + * For hidden zones (deck, hand, prizes), only the count is shown to opponents. + * For public zones (active, bench, discard), full card data is included. + */ +export interface VisibleZone { + /** Number of cards in the zone */ + count: number + /** Visible cards (empty for hidden zones when viewed by opponent) */ + cards: CardInstance[] + /** Type of zone for identification */ + zone_type: ZoneType +} + +// ============================================================================= +// Player State +// ============================================================================= + +/** + * Player state with visibility filtering applied. + * + * Contains full information for zones the viewing player can see, + * and only counts for hidden zones. + */ +export interface VisiblePlayerState { + /** The player's ID */ + player_id: string + /** Whether this is the viewing player */ + is_current_player: boolean + + // Hidden zones - counts only for opponent, full for self + /** Number of cards in deck */ + deck_count: number + /** Hand zone (full if self, count only if opponent) */ + hand: VisibleZone + /** Number of prize cards remaining */ + prizes_count: number + /** Number of cards in energy deck */ + energy_deck_count: number + + // Public zones - full details always + /** Active Pokemon zone */ + active: VisibleZone + /** Bench zone */ + bench: VisibleZone + /** Discard pile */ + discard: VisibleZone + /** Energy zone (available to attach) */ + energy_zone: VisibleZone + + // Game state + /** Current score/points */ + score: number + /** Whether GX attack has been used this game */ + gx_attack_used: boolean + /** Whether VSTAR power has been used this game */ + vstar_power_used: boolean +} + +// ============================================================================= +// Game State +// ============================================================================= + +/** + * Game state filtered for a specific player's view. + * + * This is the main state object sent via WebSocket. It contains only + * information that the viewing player is allowed to see. + */ +export interface VisibleGameState { + /** Unique game identifier */ + game_id: string + /** The player ID this view is for */ + viewer_id: string + + /** Player states keyed by player_id */ + players: Record + + /** Whose turn it is */ + current_player_id: string + /** Current turn number */ + turn_number: number + /** Current turn phase */ + phase: TurnPhase + /** Whether it's the viewer's turn */ + is_my_turn: boolean + + /** Winner if game is over */ + winner_id: string | null + /** Why the game ended */ + end_reason: GameEndReason | null + + /** Current stadium card in play */ + stadium_in_play: CardInstance | null + /** Player who played the current stadium */ + stadium_owner_id: string | null + + /** Forced action - player who must act */ + forced_action_player: string | null + /** Forced action - type of action required */ + forced_action_type: string | null + /** Forced action - reason/description */ + forced_action_reason: string | null + + /** Card definitions for display (definition_id -> CardDefinition) */ + card_registry: Record +} + +// ============================================================================= +// Helper Functions +// ============================================================================= + +/** + * Get the viewing player's state from a VisibleGameState. + */ +export function getMyPlayerState(state: VisibleGameState): VisiblePlayerState | null { + return state.players[state.viewer_id] ?? null +} + +/** + * Get the opponent's state from a VisibleGameState. + */ +export function getOpponentState(state: VisibleGameState): VisiblePlayerState | null { + const opponentId = Object.keys(state.players).find(id => id !== state.viewer_id) + return opponentId ? state.players[opponentId] : null +} + +/** + * Look up a card definition from the registry. + */ +export function getCardDefinition( + state: VisibleGameState, + definitionId: string +): CardDefinition | null { + return state.card_registry[definitionId] ?? null +} +``` + +--- + +## Task 3: Create `src/types/phaser.ts` - Phaser Bridge Types + +**Goal:** Define types for Vue-Phaser communication. + +**Estimated Time:** 20 min + +### Types to Define + +```typescript +/** + * Phaser bridge types for Vue-Phaser communication. + * + * The game bridge allows bidirectional communication between Vue components + * and Phaser scenes using typed events. + */ + +import type { VisibleGameState, ZoneType } from './game' + +// ============================================================================= +// Event Payload Types +// ============================================================================= + +/** + * Payload when a card is clicked in Phaser. + */ +export interface CardClickEvent { + /** Unique instance ID of the clicked card */ + instanceId: string + /** Definition ID for looking up card info */ + definitionId: string + /** Which zone the card is in */ + zone: ZoneType + /** Owner of the card */ + playerId: string +} + +/** + * Payload when a zone is clicked in Phaser. + */ +export interface ZoneClickEvent { + /** Which zone was clicked */ + zone: ZoneType + /** Owner of the zone */ + playerId: string + /** Slot index for multi-slot zones (bench, prizes) */ + slotIndex?: number +} + +/** + * Payload to highlight a card. + */ +export interface CardHighlightEvent { + /** Instance ID of card to highlight */ + instanceId: string + /** Whether to enable or disable highlight */ + highlight: boolean + /** Optional highlight color (hex) */ + color?: string +} + +/** + * Animation types available in the game. + */ +export type AnimationType = + | 'card_play' + | 'card_draw' + | 'attack' + | 'damage' + | 'knockout' + | 'evolve' + | 'attach_energy' + | 'retreat' + | 'shuffle' + | 'prize_take' + +/** + * Payload to request an animation. + */ +export interface AnimationRequestEvent { + /** Type of animation to play */ + type: AnimationType + /** Source card/zone instance ID */ + sourceId?: string + /** Target card/zone instance ID */ + targetId?: string + /** Animation-specific data */ + data?: Record +} + +/** + * Payload when an animation completes. + */ +export interface AnimationCompleteEvent { + /** Type of animation that completed */ + type: AnimationType + /** Whether animation completed successfully */ + success: boolean +} + +/** + * Payload for resize events. + */ +export interface ResizeEvent { + /** New canvas width */ + width: number + /** New canvas height */ + height: number +} + +// ============================================================================= +// Bridge Event Map +// ============================================================================= + +/** + * Typed event map for the Vue-Phaser bridge. + * + * This interface defines all events that can flow between Vue and Phaser, + * with their expected payload types. + */ +export interface GameBridgeEvents { + // ------------------------------------------------------------------------- + // Vue -> Phaser Events + // ------------------------------------------------------------------------- + + /** Game state has been updated - Phaser should re-render */ + 'state:updated': (state: VisibleGameState) => void + + /** Highlight/unhighlight a specific card */ + 'card:highlight': (data: CardHighlightEvent) => void + + /** Request Phaser to play an animation */ + 'animation:request': (data: AnimationRequestEvent) => void + + /** Viewport has been resized */ + 'resize': (data: ResizeEvent) => void + + // ------------------------------------------------------------------------- + // Phaser -> Vue Events + // ------------------------------------------------------------------------- + + /** User clicked a card in the game */ + 'card:clicked': (data: CardClickEvent) => void + + /** User clicked a zone (not a specific card) */ + 'zone:clicked': (data: ZoneClickEvent) => void + + /** Animation has completed */ + 'animation:complete': (data: AnimationCompleteEvent) => void + + /** Phaser scene is ready and loaded */ + 'ready': () => void + + /** Error occurred in Phaser */ + 'error': (error: Error) => void +} + +// ============================================================================= +// Layout Types +// ============================================================================= + +/** + * Position and dimensions for a zone on the board. + */ +export interface ZonePosition { + /** X coordinate (center) */ + x: number + /** Y coordinate (center) */ + y: number + /** Width of the zone */ + width: number + /** Height of the zone */ + height: number + /** Rotation in radians (for angled layouts) */ + rotation?: number +} + +/** + * Complete board layout with all zone positions. + * + * Positions are calculated based on canvas size and are updated + * when the viewport resizes. + */ +export interface BoardLayout { + // My zones (bottom of screen) + myActive: ZonePosition + myBench: ZonePosition[] // 5 slots + myHand: ZonePosition + myDeck: ZonePosition + myDiscard: ZonePosition + myPrizes: ZonePosition[] // 6 slots + myEnergyZone: ZonePosition + + // Opponent zones (top of screen, mirrored) + oppActive: ZonePosition + oppBench: ZonePosition[] // 5 slots + oppHand: ZonePosition + oppDeck: ZonePosition + oppDiscard: ZonePosition + oppPrizes: ZonePosition[] // 6 slots + oppEnergyZone: ZonePosition +} + +/** + * Card display size variants. + */ +export type CardSize = 'small' | 'medium' | 'large' + +/** + * Dimensions for each card size. + */ +export const CARD_SIZES: Record = { + small: { width: 60, height: 84 }, + medium: { width: 100, height: 140 }, + large: { width: 150, height: 210 }, +} +``` + +--- + +## Task 4: Update `src/socket/types.ts` - WebSocket Message Types + +**Goal:** Align with backend's `app/schemas/ws_messages.py`. **Full replacement** of existing file. + +**Estimated Time:** 30 min + +### Types to Define + +```typescript +/** + * WebSocket message types for Mantimon TCG real-time communication. + * + * These types align with the backend's ws_messages.py schemas. + * All messages follow a discriminated union pattern with a 'type' field. + */ + +import type { GameEndReason, TurnPhase, VisibleGameState } from '@/types' + +// ============================================================================= +// Enums +// ============================================================================= + +/** + * Error codes for WebSocket error messages. + */ +export type WSErrorCode = + // Connection errors + | 'authentication_failed' + | 'connection_closed' + | 'rate_limited' + // Game errors + | 'game_not_found' + | 'not_in_game' + | 'already_in_game' + | 'game_full' + | 'game_ended' + // Action errors + | 'invalid_action' + | 'not_your_turn' + | 'action_not_allowed' + // Protocol errors + | 'invalid_message' + | 'unknown_message_type' + // Server errors + | 'internal_error' + +/** + * Connection status for opponent status messages. + */ +export type ConnectionStatus = 'connected' | 'disconnected' | 'reconnecting' + +// ============================================================================= +// Base Message Types +// ============================================================================= + +/** + * Base fields for all server-to-client messages. + */ +export interface BaseServerMessage { + /** Server-generated UUID for tracking */ + message_id: string + /** UTC timestamp when message was created (ISO 8601) */ + timestamp: string +} + +/** + * Base fields for all client-to-server messages. + */ +export interface BaseClientMessage { + /** Client-generated UUID for idempotency */ + message_id: string +} + +// ============================================================================= +// Server -> Client Messages +// ============================================================================= + +/** + * Full game state update. + * Sent when joining a game or when full state sync is needed. + */ +export interface GameStateMessage extends BaseServerMessage { + type: 'game_state' + game_id: string + state: VisibleGameState + /** Event ID for reconnection replay */ + event_id: string + /** Number of spectators watching */ + spectator_count: number +} + +/** + * Result of a player action. + */ +export interface ActionResultMessage extends BaseServerMessage { + type: 'action_result' + game_id: string + /** The message_id of the original action request */ + request_message_id: string + success: boolean + action_type: string + /** State changes that resulted from the action */ + changes: Record + error_code?: WSErrorCode + error_message?: string +} + +/** + * Error notification for protocol or connection errors. + */ +export interface ErrorMessage extends BaseServerMessage { + type: 'error' + code: WSErrorCode + message: string + details: Record + /** ID of message that caused error, if applicable */ + request_message_id?: string +} + +/** + * Notification that a new turn has started. + */ +export interface TurnStartMessage extends BaseServerMessage { + type: 'turn_start' + game_id: string + player_id: string + turn_number: number + event_id: string +} + +/** + * Timeout warning or expiration notification. + */ +export interface TurnTimeoutMessage extends BaseServerMessage { + type: 'turn_timeout' + game_id: string + remaining_seconds: number + /** True if warning, false if timeout occurred */ + is_warning: boolean + player_id: string +} + +/** + * Notification that the game has ended. + */ +export interface GameOverMessage extends BaseServerMessage { + type: 'game_over' + game_id: string + winner_id: string | null + end_reason: GameEndReason + final_state: VisibleGameState + event_id: string +} + +/** + * Notification of opponent connection status change. + */ +export interface OpponentStatusMessage extends BaseServerMessage { + type: 'opponent_status' + game_id: string + opponent_id: string + status: ConnectionStatus +} + +/** + * Acknowledgment of a client heartbeat. + */ +export interface HeartbeatAckMessage extends BaseServerMessage { + type: 'heartbeat_ack' +} + +/** + * Union type for all server messages. + */ +export type ServerMessage = + | GameStateMessage + | ActionResultMessage + | ErrorMessage + | TurnStartMessage + | TurnTimeoutMessage + | GameOverMessage + | OpponentStatusMessage + | HeartbeatAckMessage + +// ============================================================================= +// Client -> Server Messages +// ============================================================================= + +/** + * Request to join or rejoin a game session. + */ +export interface JoinGameMessage extends BaseClientMessage { + type: 'join_game' + game_id: string + /** For reconnection - ID of last received event for replay */ + last_event_id?: string +} + +/** + * Submit a game action for execution. + */ +export interface ActionMessage extends BaseClientMessage { + type: 'action' + game_id: string + action: GameAction +} + +/** + * Request to resign from a game. + */ +export interface ResignMessage extends BaseClientMessage { + type: 'resign' + game_id: string +} + +/** + * Keep-alive message to maintain connection. + */ +export interface HeartbeatMessage extends BaseClientMessage { + type: 'heartbeat' +} + +/** + * Union type for all client messages. + */ +export type ClientMessage = + | JoinGameMessage + | ActionMessage + | ResignMessage + | HeartbeatMessage + +// ============================================================================= +// Game Action Types +// ============================================================================= + +/** + * Types of actions a player can take. + */ +export type GameActionType = + | 'play_pokemon' + | 'evolve' + | 'attach_energy' + | 'play_trainer' + | 'use_ability' + | 'attack' + | 'retreat' + | 'pass' + +/** + * A game action to be executed. + */ +export interface GameAction { + type: GameActionType + /** Card instance ID being played */ + card_id?: string + /** Target card/zone instance ID */ + target_id?: string + /** Attack index (0 or 1) for attack actions */ + attack_index?: number + /** Additional action-specific data */ + data?: Record +} + +// ============================================================================= +// Socket.IO Event Interfaces +// ============================================================================= + +/** + * Client-to-server Socket.IO events. + */ +export interface ClientToServerEvents { + /** Join or rejoin a game room */ + 'game:join': (data: JoinGameMessage) => void + /** Execute a game action */ + 'game:action': (data: ActionMessage) => void + /** Resign from the current game */ + 'game:resign': (data: ResignMessage) => void + /** Send heartbeat */ + 'game:heartbeat': (data: HeartbeatMessage) => void + /** Join as spectator */ + 'game:spectate': (data: { game_id: string }) => void + /** Leave spectator mode */ + 'game:leave_spectate': (data: { game_id: string }) => void +} + +/** + * Server-to-client Socket.IO events. + */ +export interface ServerToClientEvents { + /** Full game state update */ + 'game:state': (data: GameStateMessage) => void + /** Result of a player action */ + 'game:action_result': (data: ActionResultMessage) => void + /** Error notification */ + 'game:error': (data: ErrorMessage) => void + /** Turn has started */ + 'game:turn_start': (data: TurnStartMessage) => void + /** Turn timeout warning/expiration */ + 'game:turn_timeout': (data: TurnTimeoutMessage) => void + /** Game has ended */ + 'game:game_over': (data: GameOverMessage) => void + /** Opponent connection status changed */ + 'game:opponent_status': (data: OpponentStatusMessage) => void + /** Auto-reconnected to game */ + 'game:reconnected': (data: { game_id: string; state: VisibleGameState }) => void + /** Spectator count update */ + 'game:spectator_count': (data: { count: number }) => void +} + +// ============================================================================= +// Helper Functions +// ============================================================================= + +/** + * Generate a unique message ID. + */ +export function generateMessageId(): string { + return crypto.randomUUID() +} + +/** + * Create a join game message. + */ +export function createJoinGameMessage( + gameId: string, + lastEventId?: string +): JoinGameMessage { + return { + type: 'join_game', + message_id: generateMessageId(), + game_id: gameId, + last_event_id: lastEventId, + } +} + +/** + * Create an action message. + */ +export function createActionMessage( + gameId: string, + action: GameAction +): ActionMessage { + return { + type: 'action', + message_id: generateMessageId(), + game_id: gameId, + action, + } +} + +/** + * Create a resign message. + */ +export function createResignMessage(gameId: string): ResignMessage { + return { + type: 'resign', + message_id: generateMessageId(), + game_id: gameId, + } +} + +/** + * Create a heartbeat message. + */ +export function createHeartbeatMessage(): HeartbeatMessage { + return { + type: 'heartbeat', + message_id: generateMessageId(), + } +} +``` + +--- + +## Task 5: Update `src/types/api.ts` - Game REST API Types + +**Goal:** Add types for game creation/info REST endpoints. + +**Estimated Time:** 15 min + +### Types to Add + +```typescript +// Add to existing api.ts file + +import type { GameEndReason, TurnPhase } from './game' +import type { RulesConfig } from './rules' + +// ============================================================================= +// Game Types +// ============================================================================= + +/** + * Type of game being played. + */ +export type GameType = 'freeplay' | 'ranked' | 'campaign' | 'tutorial' + +// ============================================================================= +// Game Creation +// ============================================================================= + +/** + * Request to create a new game. + * POST /api/games + */ +export interface GameCreateRequest { + /** ID of the deck to use */ + deck_id: string + /** Opponent player ID (for PvP) */ + opponent_id: string + /** Opponent's deck ID */ + opponent_deck_id: string + /** Optional custom rules configuration */ + rules_config?: Partial + /** Type of game to create */ + game_type?: GameType +} + +/** + * Response from game creation. + */ +export interface GameCreateResponse { + /** Unique game identifier */ + game_id: string + /** WebSocket URL for gameplay */ + ws_url: string + /** Player who goes first */ + starting_player_id: string + /** Status message */ + message: string +} + +// ============================================================================= +// Game Info +// ============================================================================= + +/** + * Game information response. + * GET /api/games/{id} + */ +export interface GameInfoResponse { + game_id: string + game_type: GameType + player1_id: string + player2_id: string | null + npc_id: string | null + current_player_id: string | null + turn_number: number + phase: TurnPhase | null + is_your_turn: boolean + is_game_over: boolean + winner_id: string | null + end_reason: GameEndReason | null + started_at: string // ISO datetime + last_action_at: string // ISO datetime +} + +// ============================================================================= +// Active Games List +// ============================================================================= + +/** + * Summary of an active game. + */ +export interface ActiveGameSummary { + game_id: string + game_type: GameType + opponent_name: string + is_your_turn: boolean + turn_number: number + started_at: string + last_action_at: string +} + +/** + * List of user's active games. + * GET /api/games/me/active + */ +export interface ActiveGameListResponse { + games: ActiveGameSummary[] + total: number +} + +// ============================================================================= +// Game Actions +// ============================================================================= + +/** + * Response from game resignation. + * POST /api/games/{id}/resign + */ +export interface GameResignResponse { + success: boolean + game_id: string + message: string +} +``` + +--- + +## Task 6: Update `src/types/index.ts` - Centralize Exports + +**Goal:** Clean re-exports from new type files. + +**Estimated Time:** 10 min + +### Updated Exports + +```typescript +/** + * Central type exports for Mantimon TCG frontend. + * + * Import from '@/types' for all type needs. + */ + +// ============================================================================= +// Game State Types (from game.ts) +// ============================================================================= + +export type { + // Enums + ZoneType, + TurnPhase, + GameEndReason, + StatusCondition, + CardType, + PokemonStage, + PokemonVariant, + TrainerType, + EnergyType, + // Card types + Attack, + Ability, + WeaknessResistance, + CardDefinition, + CardInstance, + // Zone types + VisibleZone, + // Player/Game state + VisiblePlayerState, + VisibleGameState, +} from './game' + +export { + ENERGY_TYPES, + getMyPlayerState, + getOpponentState, + getCardDefinition, +} from './game' + +// ============================================================================= +// Rules Configuration (from rules.ts) +// ============================================================================= + +export type { + ModifierMode, + DeckConfig, + ActiveConfig, + BenchConfig, + EnergyConfig, + PrizeConfig, + FirstTurnConfig, + WinConditionsConfig, + StatusConfig, + TrainerConfig, + EvolutionConfig, + RetreatConfig, + CombatConfig, + RulesConfig, +} from './rules' + +export { + createDefaultRulesConfig, + createStandardPokemonTCGConfig, +} from './rules' + +// ============================================================================= +// Phaser Bridge Types (from phaser.ts) +// ============================================================================= + +export type { + CardClickEvent, + ZoneClickEvent, + CardHighlightEvent, + AnimationType, + AnimationRequestEvent, + AnimationCompleteEvent, + ResizeEvent, + GameBridgeEvents, + ZonePosition, + BoardLayout, + CardSize, +} from './phaser' + +export { CARD_SIZES } from './phaser' + +// ============================================================================= +// API Types (from api.ts) +// ============================================================================= + +export type { + // Existing deck/collection types + DeckConfig as DeckValidationConfig, // Rename to avoid conflict with rules.DeckConfig + DeckResponse, + DecksListResponse, + DeckCreateRequest, + DeckUpdateRequest, + DeckValidationRequest, + DeckValidationResponse, + StarterDeckSelectRequest, + StarterStatusResponse, + CollectionEntryResponse, + CollectionResponse, + CollectionAddRequest, + CollectionCardResponse, + EnergyConfig as EnergyValidationConfig, // Rename to avoid conflict + PartialEnergyConfig, + // New game API types + GameType, + GameCreateRequest, + GameCreateResponse, + GameInfoResponse, + ActiveGameSummary, + ActiveGameListResponse, + GameResignResponse, +} from './api' + +// ============================================================================= +// Generic API Types +// ============================================================================= + +/** + * Standard API error response. + */ +export interface ApiError { + detail: string + code?: string +} + +/** + * Generic paginated response wrapper. + */ +export interface PaginatedResponse { + items: T[] + total: number + page: number + pageSize: number + totalPages: number +} +``` + +--- + +## Task 7: Update `src/stores/game.ts` - Use New Types + +**Goal:** Update the game store to use `VisibleGameState`. + +**Estimated Time:** 15 min + +### Updated Store + +```typescript +/** + * Game store for managing active game state. + * + * Receives VisibleGameState from WebSocket and provides computed + * helpers for accessing player data, cards, and game status. + */ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' + +import type { + VisibleGameState, + VisiblePlayerState, + CardInstance, + CardDefinition, + TurnPhase, +} from '@/types' +import { getMyPlayerState, getOpponentState, getCardDefinition } from '@/types' + +export const useGameStore = defineStore('game', () => { + // --------------------------------------------------------------------------- + // State + // --------------------------------------------------------------------------- + + /** Current game state from server */ + const gameState = ref(null) + + /** Whether we're currently in a game */ + const isInGame = ref(false) + + /** Connection status */ + const isConnected = ref(false) + + /** Loading state */ + const isLoading = ref(false) + + /** Error message */ + const error = ref(null) + + // --------------------------------------------------------------------------- + // Computed - Player States + // --------------------------------------------------------------------------- + + /** My player state */ + const myPlayer = computed(() => { + if (!gameState.value) return null + return getMyPlayerState(gameState.value) + }) + + /** Opponent's player state */ + const opponent = computed(() => { + if (!gameState.value) return null + return getOpponentState(gameState.value) + }) + + // --------------------------------------------------------------------------- + // Computed - My Zones + // --------------------------------------------------------------------------- + + /** Cards in my hand */ + const myHand = computed(() => + myPlayer.value?.hand.cards ?? [] + ) + + /** My active Pokemon */ + const myActive = computed(() => + myPlayer.value?.active.cards[0] ?? null + ) + + /** My benched Pokemon */ + const myBench = computed(() => + myPlayer.value?.bench.cards ?? [] + ) + + /** My discard pile */ + const myDiscard = computed(() => + myPlayer.value?.discard.cards ?? [] + ) + + /** My available energy */ + const myEnergy = computed(() => + myPlayer.value?.energy_zone.cards ?? [] + ) + + /** My deck count */ + const myDeckCount = computed(() => + myPlayer.value?.deck_count ?? 0 + ) + + /** My prize count */ + const myPrizeCount = computed(() => + myPlayer.value?.prizes_count ?? 0 + ) + + /** My score */ + const myScore = computed(() => + myPlayer.value?.score ?? 0 + ) + + // --------------------------------------------------------------------------- + // Computed - Opponent Zones + // --------------------------------------------------------------------------- + + /** Opponent's active Pokemon */ + const oppActive = computed(() => + opponent.value?.active.cards[0] ?? null + ) + + /** Opponent's benched Pokemon */ + const oppBench = computed(() => + opponent.value?.bench.cards ?? [] + ) + + /** Opponent's hand count (not contents) */ + const oppHandCount = computed(() => + opponent.value?.hand.count ?? 0 + ) + + /** Opponent's deck count */ + const oppDeckCount = computed(() => + opponent.value?.deck_count ?? 0 + ) + + /** Opponent's prize count */ + const oppPrizeCount = computed(() => + opponent.value?.prizes_count ?? 0 + ) + + /** Opponent's score */ + const oppScore = computed(() => + opponent.value?.score ?? 0 + ) + + // --------------------------------------------------------------------------- + // Computed - Game State + // --------------------------------------------------------------------------- + + /** Current game ID */ + const gameId = computed(() => + gameState.value?.game_id ?? null + ) + + /** Whether it's my turn */ + const isMyTurn = computed(() => + gameState.value?.is_my_turn ?? false + ) + + /** Current turn phase */ + const phase = computed(() => + gameState.value?.phase ?? null + ) + + /** Current turn number */ + const turnNumber = computed(() => + gameState.value?.turn_number ?? 0 + ) + + /** Whether game is over */ + const isGameOver = computed(() => + gameState.value?.winner_id !== null + ) + + /** Winner ID if game is over */ + const winnerId = computed(() => + gameState.value?.winner_id ?? null + ) + + /** Whether I won */ + const didIWin = computed(() => + winnerId.value === gameState.value?.viewer_id + ) + + /** Forced action info */ + const forcedAction = computed(() => { + if (!gameState.value?.forced_action_type) return null + return { + player: gameState.value.forced_action_player, + type: gameState.value.forced_action_type, + reason: gameState.value.forced_action_reason, + } + }) + + /** Whether I need to respond to a forced action */ + const hasForcedAction = computed(() => + forcedAction.value?.player === gameState.value?.viewer_id + ) + + // --------------------------------------------------------------------------- + // Computed - Card Lookup + // --------------------------------------------------------------------------- + + /** Look up a card definition by ID */ + function lookupCard(definitionId: string): CardDefinition | null { + if (!gameState.value) return null + return getCardDefinition(gameState.value, definitionId) + } + + // --------------------------------------------------------------------------- + // Actions + // --------------------------------------------------------------------------- + + /** Set the game state (called from WebSocket handler) */ + function setGameState(state: VisibleGameState): void { + gameState.value = state + isInGame.value = true + error.value = null + } + + /** Clear the game state (when leaving game) */ + function clearGameState(): void { + gameState.value = null + isInGame.value = false + } + + /** Set connection status */ + function setConnected(connected: boolean): void { + isConnected.value = connected + } + + /** Set loading state */ + function setLoading(loading: boolean): void { + isLoading.value = loading + } + + /** Set error message */ + function setError(message: string | null): void { + error.value = message + } + + // --------------------------------------------------------------------------- + // Return + // --------------------------------------------------------------------------- + + return { + // State + gameState, + isInGame, + isConnected, + isLoading, + error, + + // Player states + myPlayer, + opponent, + + // My zones + myHand, + myActive, + myBench, + myDiscard, + myEnergy, + myDeckCount, + myPrizeCount, + myScore, + + // Opponent zones + oppActive, + oppBench, + oppHandCount, + oppDeckCount, + oppPrizeCount, + oppScore, + + // Game state + gameId, + isMyTurn, + phase, + turnNumber, + isGameOver, + winnerId, + didIWin, + forcedAction, + hasForcedAction, + + // Card lookup + lookupCard, + + // Actions + setGameState, + clearGameState, + setConnected, + setLoading, + setError, + } +}) +``` + +--- + +## Task 8: Write Tests + +**Goal:** Verify types compile and match expected structures. + +**Estimated Time:** 15 min + +### Test File: `src/types/game.spec.ts` + +```typescript +import { describe, it, expect } from 'vitest' + +import type { + VisibleGameState, + VisiblePlayerState, + CardInstance, + CardDefinition, +} from './game' +import { + getMyPlayerState, + getOpponentState, + getCardDefinition, + ENERGY_TYPES, +} from './game' + +describe('game types', () => { + describe('ENERGY_TYPES', () => { + it('contains all 10 energy types', () => { + /** + * Test that ENERGY_TYPES constant includes all valid energy types. + * This is important for validation and iteration. + */ + expect(ENERGY_TYPES).toHaveLength(10) + expect(ENERGY_TYPES).toContain('fire') + expect(ENERGY_TYPES).toContain('water') + expect(ENERGY_TYPES).toContain('grass') + expect(ENERGY_TYPES).toContain('lightning') + expect(ENERGY_TYPES).toContain('psychic') + expect(ENERGY_TYPES).toContain('fighting') + expect(ENERGY_TYPES).toContain('darkness') + expect(ENERGY_TYPES).toContain('metal') + expect(ENERGY_TYPES).toContain('dragon') + expect(ENERGY_TYPES).toContain('colorless') + }) + }) + + describe('helper functions', () => { + const mockState: VisibleGameState = { + game_id: 'game-123', + viewer_id: 'player-1', + players: { + 'player-1': { + player_id: 'player-1', + is_current_player: true, + deck_count: 30, + hand: { count: 5, cards: [], zone_type: 'hand' }, + active: { count: 1, cards: [], zone_type: 'active' }, + bench: { count: 2, cards: [], zone_type: 'bench' }, + discard: { count: 3, cards: [], zone_type: 'discard' }, + energy_zone: { count: 2, cards: [], zone_type: 'energy_zone' }, + prizes_count: 4, + energy_deck_count: 15, + score: 0, + gx_attack_used: false, + vstar_power_used: false, + }, + 'player-2': { + player_id: 'player-2', + is_current_player: false, + deck_count: 28, + hand: { count: 4, cards: [], zone_type: 'hand' }, + active: { count: 1, cards: [], zone_type: 'active' }, + bench: { count: 3, cards: [], zone_type: 'bench' }, + discard: { count: 5, cards: [], zone_type: 'discard' }, + energy_zone: { count: 1, cards: [], zone_type: 'energy_zone' }, + prizes_count: 4, + energy_deck_count: 12, + score: 0, + gx_attack_used: false, + vstar_power_used: false, + }, + }, + current_player_id: 'player-1', + turn_number: 3, + phase: 'main', + is_my_turn: true, + winner_id: null, + end_reason: null, + stadium_in_play: null, + stadium_owner_id: null, + forced_action_player: null, + forced_action_type: null, + forced_action_reason: null, + card_registry: { + 'pikachu-001': { + id: 'pikachu-001', + name: 'Pikachu', + card_type: 'pokemon', + hp: 60, + pokemon_type: 'lightning', + }, + }, + } + + it('getMyPlayerState returns viewer player', () => { + /** + * Test that getMyPlayerState correctly identifies the viewer's state. + */ + const myState = getMyPlayerState(mockState) + expect(myState).not.toBeNull() + expect(myState?.player_id).toBe('player-1') + expect(myState?.is_current_player).toBe(true) + }) + + it('getOpponentState returns non-viewer player', () => { + /** + * Test that getOpponentState correctly identifies the opponent's state. + */ + const oppState = getOpponentState(mockState) + expect(oppState).not.toBeNull() + expect(oppState?.player_id).toBe('player-2') + expect(oppState?.is_current_player).toBe(false) + }) + + it('getCardDefinition looks up card from registry', () => { + /** + * Test that getCardDefinition retrieves cards from the registry. + */ + const card = getCardDefinition(mockState, 'pikachu-001') + expect(card).not.toBeNull() + expect(card?.name).toBe('Pikachu') + expect(card?.hp).toBe(60) + }) + + it('getCardDefinition returns null for unknown card', () => { + /** + * Test that getCardDefinition returns null for non-existent cards. + */ + const card = getCardDefinition(mockState, 'unknown-card') + expect(card).toBeNull() + }) + }) +}) +``` + +--- + +## Summary + +| Task | Files | Estimated Time | +|------|-------|----------------| +| 1. Rules config types | `src/types/rules.ts` | 30 min | +| 2. Game state types | `src/types/game.ts` | 45 min | +| 3. Phaser bridge types | `src/types/phaser.ts` | 20 min | +| 4. WebSocket types | `src/socket/types.ts` | 30 min | +| 5. Game API types | `src/types/api.ts` | 15 min | +| 6. Export updates | `src/types/index.ts` | 10 min | +| 7. Game store update | `src/stores/game.ts` | 15 min | +| 8. Tests | `src/types/game.spec.ts` | 15 min | + +**Total: ~3 hours** + +--- + +## Notes + +- All types use snake_case to match backend JSON responses (no camelCase conversion needed) +- Helper functions are provided for common operations on VisibleGameState +- The game store is updated to use the new types with comprehensive computed properties +- Factory functions are provided for RulesConfig to easily create standard configurations diff --git a/.claude/frontend-poc/public/game/cards/a1/033-charmander.webp b/.claude/frontend-poc/public/game/cards/a1/033-charmander.webp new file mode 100644 index 0000000..00956ef Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/033-charmander.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/034-charmeleon.webp b/.claude/frontend-poc/public/game/cards/a1/034-charmeleon.webp new file mode 100644 index 0000000..f819575 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/034-charmeleon.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/035-charizard.webp b/.claude/frontend-poc/public/game/cards/a1/035-charizard.webp new file mode 100644 index 0000000..0feb63a Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/035-charizard.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/037-vulpix.webp b/.claude/frontend-poc/public/game/cards/a1/037-vulpix.webp new file mode 100644 index 0000000..5e4b646 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/037-vulpix.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/039-growlithe.webp b/.claude/frontend-poc/public/game/cards/a1/039-growlithe.webp new file mode 100644 index 0000000..7159bd8 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/039-growlithe.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/040-arcanine.webp b/.claude/frontend-poc/public/game/cards/a1/040-arcanine.webp new file mode 100644 index 0000000..2110878 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/040-arcanine.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/042-ponyta.webp b/.claude/frontend-poc/public/game/cards/a1/042-ponyta.webp new file mode 100644 index 0000000..b8be5b1 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/042-ponyta.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/043-rapidash.webp b/.claude/frontend-poc/public/game/cards/a1/043-rapidash.webp new file mode 100644 index 0000000..48082cd Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/043-rapidash.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/044-magmar.webp b/.claude/frontend-poc/public/game/cards/a1/044-magmar.webp new file mode 100644 index 0000000..76f38e8 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/044-magmar.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/094-pikachu.webp b/.claude/frontend-poc/public/game/cards/a1/094-pikachu.webp new file mode 100644 index 0000000..1aeea6d Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/094-pikachu.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/095-raichu.webp b/.claude/frontend-poc/public/game/cards/a1/095-raichu.webp new file mode 100644 index 0000000..c86aae3 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/095-raichu.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/097-magnemite.webp b/.claude/frontend-poc/public/game/cards/a1/097-magnemite.webp new file mode 100644 index 0000000..916e795 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/097-magnemite.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/098-magneton.webp b/.claude/frontend-poc/public/game/cards/a1/098-magneton.webp new file mode 100644 index 0000000..09dd11a Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/098-magneton.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/099-voltorb.webp b/.claude/frontend-poc/public/game/cards/a1/099-voltorb.webp new file mode 100644 index 0000000..3654516 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/099-voltorb.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/100-electrode.webp b/.claude/frontend-poc/public/game/cards/a1/100-electrode.webp new file mode 100644 index 0000000..5d0afb4 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/100-electrode.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/101-electabuzz.webp b/.claude/frontend-poc/public/game/cards/a1/101-electabuzz.webp new file mode 100644 index 0000000..6c98cd1 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/101-electabuzz.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/105-blitzle.webp b/.claude/frontend-poc/public/game/cards/a1/105-blitzle.webp new file mode 100644 index 0000000..04fd55e Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/105-blitzle.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/106-zebstrika.webp b/.claude/frontend-poc/public/game/cards/a1/106-zebstrika.webp new file mode 100644 index 0000000..e7230bd Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/106-zebstrika.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/216-helix-fossil.webp b/.claude/frontend-poc/public/game/cards/a1/216-helix-fossil.webp new file mode 100644 index 0000000..97652c6 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/216-helix-fossil.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/217-dome-fossil.webp b/.claude/frontend-poc/public/game/cards/a1/217-dome-fossil.webp new file mode 100644 index 0000000..0f2c3a7 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/217-dome-fossil.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/218-old-amber.webp b/.claude/frontend-poc/public/game/cards/a1/218-old-amber.webp new file mode 100644 index 0000000..1fd329b Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/218-old-amber.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/221-blaine.webp b/.claude/frontend-poc/public/game/cards/a1/221-blaine.webp new file mode 100644 index 0000000..942da74 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/221-blaine.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/224-brock.webp b/.claude/frontend-poc/public/game/cards/a1/224-brock.webp new file mode 100644 index 0000000..defae3b Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/224-brock.webp differ diff --git a/.claude/frontend-poc/public/game/cards/a1/226-lt-surge.webp b/.claude/frontend-poc/public/game/cards/a1/226-lt-surge.webp new file mode 100644 index 0000000..a844d06 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/a1/226-lt-surge.webp differ diff --git a/.claude/frontend-poc/public/game/cards/basic/fire.webp b/.claude/frontend-poc/public/game/cards/basic/fire.webp new file mode 100644 index 0000000..516d448 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/basic/fire.webp differ diff --git a/.claude/frontend-poc/public/game/cards/basic/grass.webp b/.claude/frontend-poc/public/game/cards/basic/grass.webp new file mode 100644 index 0000000..633213c Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/basic/grass.webp differ diff --git a/.claude/frontend-poc/public/game/cards/basic/lightning.webp b/.claude/frontend-poc/public/game/cards/basic/lightning.webp new file mode 100644 index 0000000..9db45df Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/basic/lightning.webp differ diff --git a/.claude/frontend-poc/public/game/cards/basic/psychic.webp b/.claude/frontend-poc/public/game/cards/basic/psychic.webp new file mode 100644 index 0000000..03e5c67 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/basic/psychic.webp differ diff --git a/.claude/frontend-poc/public/game/cards/basic/water.webp b/.claude/frontend-poc/public/game/cards/basic/water.webp new file mode 100644 index 0000000..30e79f6 Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/basic/water.webp differ diff --git a/.claude/frontend-poc/public/game/cards/card_back.webp b/.claude/frontend-poc/public/game/cards/card_back.webp new file mode 100644 index 0000000..f1684ac Binary files /dev/null and b/.claude/frontend-poc/public/game/cards/card_back.webp differ diff --git a/.claude/frontend-poc/public/vite.svg b/.claude/frontend-poc/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/.claude/frontend-poc/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.claude/frontend-poc/src/App.spec.ts b/.claude/frontend-poc/src/App.spec.ts new file mode 100644 index 0000000..1940de1 --- /dev/null +++ b/.claude/frontend-poc/src/App.spec.ts @@ -0,0 +1,234 @@ +/** + * Tests for App.vue root component. + * + * Verifies that auth initialization happens on app startup and that + * the loading state is properly displayed during initialization. + */ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import { setActivePinia, createPinia } from 'pinia' +import { defineComponent, h, nextTick } from 'vue' + +// Track initialization calls and control resolution +let initializeResolve: ((value: boolean) => void) | null = null +let initializeCalled = false + +// Mock useAuth composable +vi.mock('@/composables/useAuth', () => ({ + useAuth: () => ({ + initialize: vi.fn(() => { + initializeCalled = true + return new Promise((resolve) => { + initializeResolve = resolve + }) + }), + isInitialized: { value: false }, + isAuthenticated: { value: false }, + user: { value: null }, + isLoading: { value: false }, + error: { value: null }, + }), +})) + +// Mock vue-router +vi.mock('vue-router', () => ({ + useRoute: () => ({ + meta: { layout: 'default' }, + name: 'Dashboard', + path: '/', + }), + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + }), +})) + +import App from './App.vue' + +// Create stub components for layouts +const DefaultLayoutStub = defineComponent({ + name: 'DefaultLayout', + setup(_, { slots }) { + return () => h('div', { class: 'default-layout' }, slots.default?.()) + }, +}) + +const MinimalLayoutStub = defineComponent({ + name: 'MinimalLayout', + setup(_, { slots }) { + return () => h('div', { class: 'minimal-layout' }, slots.default?.()) + }, +}) + +const GameLayoutStub = defineComponent({ + name: 'GameLayout', + setup(_, { slots }) { + return () => h('div', { class: 'game-layout' }, slots.default?.()) + }, +}) + +const LoadingOverlayStub = defineComponent({ + name: 'LoadingOverlay', + setup() { + return () => h('div', { class: 'loading-overlay-mock' }) + }, +}) + +const ToastContainerStub = defineComponent({ + name: 'ToastContainer', + setup() { + return () => h('div', { class: 'toast-container-mock' }) + }, +}) + +describe('App.vue', () => { + beforeEach(() => { + setActivePinia(createPinia()) + initializeCalled = false + initializeResolve = null + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + const mountApp = () => { + return mount(App, { + global: { + stubs: { + RouterView: true, + DefaultLayout: DefaultLayoutStub, + MinimalLayout: MinimalLayoutStub, + GameLayout: GameLayoutStub, + LoadingOverlay: LoadingOverlayStub, + ToastContainer: ToastContainerStub, + // Also stub async component wrappers + AsyncComponentWrapper: true, + Suspense: false, + }, + }, + }) + } + + describe('auth initialization', () => { + it('shows loading spinner while auth is initializing', async () => { + /** + * Test loading state during auth initialization. + * + * When the app starts, it should show a loading spinner while + * auth tokens are being validated. This prevents navigation guards + * from running before auth state is known. + */ + const wrapper = mountApp() + + // Should show loading text initially + expect(wrapper.text()).toContain('Loading...') + + wrapper.unmount() + }) + + it('calls initialize on mount', async () => { + /** + * Test that auth initialization is triggered on mount. + * + * The initialize() function must be called to validate tokens, + * refresh if needed, and fetch the user profile. + */ + const wrapper = mountApp() + + // Wait for onMounted to run + await nextTick() + + expect(initializeCalled).toBe(true) + + wrapper.unmount() + }) + + it('hides loading spinner after initialization completes', async () => { + /** + * Test transition from loading to content. + * + * Once auth initialization completes, the loading spinner should + * be hidden and the main app content should be rendered. + */ + const wrapper = mountApp() + + // Wait for onMounted + await nextTick() + + // Should show loading initially + expect(wrapper.text()).toContain('Loading...') + + // Resolve initialization (successful) + expect(initializeResolve).not.toBeNull() + initializeResolve!(true) + + // Wait for state update + await flushPromises() + await nextTick() + + // Loading text should be gone + expect(wrapper.text()).not.toContain('Loading...') + + wrapper.unmount() + }) + }) + + describe('initialization state machine', () => { + it('starts in initializing state and transitions to initialized', async () => { + /** + * Test the auth initialization state machine. + * + * The app should start showing the loading state and transition + * to the main content after initialization completes. + */ + const wrapper = mountApp() + await nextTick() + + // Before initialization completes + expect(wrapper.find('.bg-gray-900').exists()).toBe(true) + + // Complete initialization (successful) + initializeResolve!(true) + await flushPromises() + await nextTick() + + // After initialization - the fixed loading div should be gone + // (checking that the loading screen specific class combo is gone) + const loadingScreen = wrapper.find('.fixed.inset-0.z-50.bg-gray-900') + expect(loadingScreen.exists()).toBe(false) + + wrapper.unmount() + }) + + it('renders app content even when initialization returns false (auth failed)', async () => { + /** + * Test that app renders correctly when auth initialization fails. + * + * When initialize() returns false (e.g., tokens invalid, network error + * during profile fetch), the app should still transition out of the + * loading state and render the main content. Navigation guards will + * handle redirecting unauthenticated users to login. + */ + const wrapper = mountApp() + await nextTick() + + // Should show loading initially + expect(wrapper.text()).toContain('Loading...') + + // Complete initialization with failure (auth failed) + initializeResolve!(false) + await flushPromises() + await nextTick() + + // Loading screen should be gone - app renders even on auth failure + const loadingScreen = wrapper.find('.fixed.inset-0.z-50.bg-gray-900') + expect(loadingScreen.exists()).toBe(false) + + // Loading text should be gone + expect(wrapper.text()).not.toContain('Loading...') + + wrapper.unmount() + }) + }) +}) diff --git a/.claude/frontend-poc/src/App.vue b/.claude/frontend-poc/src/App.vue new file mode 100644 index 0000000..b70d746 --- /dev/null +++ b/.claude/frontend-poc/src/App.vue @@ -0,0 +1,91 @@ + + + diff --git a/.claude/frontend-poc/src/api/client.spec.ts b/.claude/frontend-poc/src/api/client.spec.ts new file mode 100644 index 0000000..993c355 --- /dev/null +++ b/.claude/frontend-poc/src/api/client.spec.ts @@ -0,0 +1,304 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' + +import { useAuthStore } from '@/stores/auth' +import { apiClient } from './client' +import { ApiError } from './types' + +// Mock fetch globally +const mockFetch = vi.fn() +vi.stubGlobal('fetch', mockFetch) + +describe('apiClient', () => { + beforeEach(() => { + setActivePinia(createPinia()) + mockFetch.mockReset() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('get', () => { + it('makes GET request to correct URL', async () => { + /** + * Test that GET requests are sent to the correct URL. + * + * The API client must correctly build URLs from the base URL + * and path, which is fundamental to all API operations. + */ + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ id: '1', name: 'Test' }), + }) + + const result = await apiClient.get('/api/users/me') + + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining('/api/users/me'), + expect.objectContaining({ method: 'GET' }) + ) + expect(result).toEqual({ id: '1', name: 'Test' }) + }) + + it('adds query parameters to URL', async () => { + /** + * Test that query parameters are correctly appended. + * + * Many API endpoints accept query parameters for filtering, + * pagination, etc. These must be properly encoded. + */ + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ items: [] }), + }) + + await apiClient.get('/api/cards', { params: { page: 1, limit: 20 } }) + + const calledUrl = mockFetch.mock.calls[0][0] as string + expect(calledUrl).toContain('page=1') + expect(calledUrl).toContain('limit=20') + }) + + it('skips undefined query parameters', async () => { + /** + * Test that undefined parameters are not included in URL. + * + * Optional parameters should be omitted entirely rather than + * being sent as "undefined" strings. + */ + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({}), + }) + + await apiClient.get('/api/cards', { + params: { page: 1, search: undefined }, + }) + + const calledUrl = mockFetch.mock.calls[0][0] as string + expect(calledUrl).toContain('page=1') + expect(calledUrl).not.toContain('search') + }) + }) + + describe('post', () => { + it('makes POST request with JSON body', async () => { + /** + * Test that POST requests include JSON body. + * + * POST requests typically send data to create resources. + * The body must be JSON serialized. + */ + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 201, + json: async () => ({ id: '1' }), + }) + + const body = { name: 'New Deck', cards: [] } + await apiClient.post('/api/decks', body) + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(body), + }) + ) + }) + }) + + describe('authentication', () => { + it('adds Authorization header when authenticated', async () => { + /** + * Test that auth header is automatically added. + * + * Authenticated requests must include the Bearer token + * so the backend can identify the user. + */ + const auth = useAuthStore() + auth.setTokens({ + accessToken: 'test-token', + refreshToken: 'refresh-token', + expiresAt: Date.now() + 3600000, + }) + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({}), + }) + + await apiClient.get('/api/users/me') + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer test-token', + }), + }) + ) + }) + + it('skips auth header when skipAuth is true', async () => { + /** + * Test that skipAuth option prevents auth header. + * + * Some endpoints (like login/register) don't need auth + * and should not send the token. + */ + const auth = useAuthStore() + auth.setTokens({ + accessToken: 'test-token', + refreshToken: 'refresh-token', + expiresAt: Date.now() + 3600000, + }) + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({}), + }) + + await apiClient.get('/api/public/health', { skipAuth: true }) + + const calledOptions = mockFetch.mock.calls[0][1] as RequestInit + expect(calledOptions.headers).not.toHaveProperty('Authorization') + }) + + it('does not add auth header when not authenticated', async () => { + /** + * Test that unauthenticated requests have no auth header. + * + * Without a token, there's nothing to send. + */ + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({}), + }) + + await apiClient.get('/api/public/status') + + const calledOptions = mockFetch.mock.calls[0][1] as RequestInit + const headers = calledOptions.headers as Record + expect(headers['Authorization']).toBeUndefined() + }) + }) + + describe('error handling', () => { + it('throws ApiError on non-2xx response', async () => { + /** + * Test that API errors are properly thrown. + * + * Non-successful responses should throw ApiError with + * status code and message for proper error handling. + */ + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + json: async () => ({ detail: 'Deck not found' }), + }) + + try { + await apiClient.get('/api/decks/999') + expect.fail('Should have thrown') + } catch (e) { + expect(e).toBeInstanceOf(ApiError) + const error = e as ApiError + expect(error.status).toBe(404) + expect(error.detail).toBe('Deck not found') + expect(error.isNotFound).toBe(true) + } + }) + + it('handles non-JSON error responses', async () => { + /** + * Test that non-JSON errors are handled gracefully. + * + * Some server errors may return HTML or plain text. + * The client should still create a proper ApiError. + */ + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + json: async () => { + throw new Error('Not JSON') + }, + }) + + await expect(apiClient.get('/api/crash')).rejects.toThrow(ApiError) + }) + + it('handles 204 No Content responses', async () => { + /** + * Test that 204 responses return undefined. + * + * DELETE and some PUT/PATCH endpoints return 204 with no body. + * The client should handle this without trying to parse JSON. + */ + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 204, + }) + + const result = await apiClient.delete('/api/decks/1') + + expect(result).toBeUndefined() + }) + }) + + describe('token refresh', () => { + it('retries request after successful token refresh on 401', async () => { + /** + * Test that 401 triggers token refresh and retry. + * + * When the access token expires, the client should automatically + * refresh it and retry the failed request transparently. + */ + const auth = useAuthStore() + auth.setTokens({ + accessToken: 'expired-token', + refreshToken: 'valid-refresh-token', + expiresAt: Date.now() + 3600000, + }) + + // First call returns 401 + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 401, + statusText: 'Unauthorized', + json: async () => ({ detail: 'Token expired' }), + }) + + // Refresh token call succeeds + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + access_token: 'new-token', + expires_in: 3600, + }), + }) + + // Retry succeeds + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ id: '1', name: 'Test User' }), + }) + + const result = await apiClient.get('/api/users/me') + + expect(mockFetch).toHaveBeenCalledTimes(3) + expect(result).toEqual({ id: '1', name: 'Test User' }) + }) + }) +}) diff --git a/.claude/frontend-poc/src/api/client.ts b/.claude/frontend-poc/src/api/client.ts new file mode 100644 index 0000000..a219526 --- /dev/null +++ b/.claude/frontend-poc/src/api/client.ts @@ -0,0 +1,180 @@ +/** + * HTTP API client with authentication and error handling. + * + * Provides typed fetch wrapper that automatically: + * - Injects Authorization header from auth store + * - Refreshes tokens on 401 responses + * - Parses JSON responses + * - Throws typed ApiError on failures + */ +import { config } from '@/config' +import { useAuthStore } from '@/stores/auth' +import { ApiError } from './types' +import type { RequestOptions, ErrorResponse } from './types' + +// Re-export ApiError for convenience +export { ApiError } + +/** + * Build URL with query parameters. + */ +function buildUrl(path: string, params?: Record): string { + const base = config.apiBaseUrl.replace(/\/$/, '') + const cleanPath = path.startsWith('/') ? path : `/${path}` + const url = new URL(`${base}${cleanPath}`) + + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) { + url.searchParams.set(key, String(value)) + } + }) + } + + return url.toString() +} + +/** + * Parse error response from backend. + */ +async function parseErrorResponse(response: Response): Promise { + let detail: string | undefined + let code: string | undefined + + try { + const data: ErrorResponse = await response.json() + detail = data.detail || data.message + code = data.code + } catch { + // Response body is not JSON or empty + } + + return new ApiError(response.status, response.statusText, detail, code) +} + +/** + * Make an authenticated API request. + * + * @param method - HTTP method + * @param path - API path (e.g., '/api/users/me') + * @param options - Request options + * @returns Parsed JSON response + * @throws ApiError on non-2xx responses + */ +async function request( + method: string, + path: string, + options: RequestOptions = {} +): Promise { + const { skipAuth = false, headers = {}, body, params, signal } = options + const auth = useAuthStore() + + // Build headers + const requestHeaders: Record = { + 'Content-Type': 'application/json', + ...headers, + } + + // Add auth header if authenticated and not skipped + if (!skipAuth && auth.isAuthenticated) { + const token = await auth.getValidToken() + if (token) { + requestHeaders['Authorization'] = `Bearer ${token}` + } + } + + // Make request + const url = buildUrl(path, params) + const response = await fetch(url, { + method, + headers: requestHeaders, + body: body !== undefined ? JSON.stringify(body) : undefined, + signal, + }) + + // Handle 401 - try to refresh and retry once + if (response.status === 401 && !skipAuth && auth.refreshToken) { + const refreshed = await auth.refreshAccessToken() + if (refreshed) { + // Retry with new token + const newToken = await auth.getValidToken() + if (newToken) { + requestHeaders['Authorization'] = `Bearer ${newToken}` + const retryResponse = await fetch(url, { + method, + headers: requestHeaders, + body: body !== undefined ? JSON.stringify(body) : undefined, + signal, + }) + + if (!retryResponse.ok) { + throw await parseErrorResponse(retryResponse) + } + + // Handle 204 No Content + if (retryResponse.status === 204) { + return undefined as T + } + + return retryResponse.json() + } + } + + // Refresh failed, throw the original 401 + throw await parseErrorResponse(response) + } + + // Handle other errors + if (!response.ok) { + throw await parseErrorResponse(response) + } + + // Handle 204 No Content + if (response.status === 204) { + return undefined as T + } + + return response.json() +} + +/** + * API client with typed methods for each HTTP verb. + */ +export const apiClient = { + /** + * Make a GET request. + */ + get(path: string, options?: RequestOptions): Promise { + return request('GET', path, options) + }, + + /** + * Make a POST request. + */ + post(path: string, body?: unknown, options?: RequestOptions): Promise { + return request('POST', path, { ...options, body }) + }, + + /** + * Make a PUT request. + */ + put(path: string, body?: unknown, options?: RequestOptions): Promise { + return request('PUT', path, { ...options, body }) + }, + + /** + * Make a PATCH request. + */ + patch(path: string, body?: unknown, options?: RequestOptions): Promise { + return request('PATCH', path, { ...options, body }) + }, + + /** + * Make a DELETE request. + */ + delete(path: string, options?: RequestOptions): Promise { + return request('DELETE', path, options) + }, +} + +export default apiClient diff --git a/.claude/frontend-poc/src/api/index.ts b/.claude/frontend-poc/src/api/index.ts new file mode 100644 index 0000000..6475b55 --- /dev/null +++ b/.claude/frontend-poc/src/api/index.ts @@ -0,0 +1,11 @@ +/** + * API module exports. + */ +export { apiClient, default } from './client' +export { ApiError } from './types' +export type { + RequestOptions, + ApiResponse, + PaginatedResponse, + ErrorResponse, +} from './types' diff --git a/.claude/frontend-poc/src/api/types.ts b/.claude/frontend-poc/src/api/types.ts new file mode 100644 index 0000000..607d98e --- /dev/null +++ b/.claude/frontend-poc/src/api/types.ts @@ -0,0 +1,100 @@ +/** + * API client types and error classes. + */ + +/** + * Custom error class for API errors. + * + * Provides structured error information including HTTP status code, + * error message, and optional detail from the backend. + */ +export class ApiError extends Error { + constructor( + public readonly status: number, + public readonly statusText: string, + public readonly detail?: string, + public readonly code?: string + ) { + super(detail || statusText) + this.name = 'ApiError' + } + + /** + * Check if this is an authentication error (401). + */ + get isUnauthorized(): boolean { + return this.status === 401 + } + + /** + * Check if this is a forbidden error (403). + */ + get isForbidden(): boolean { + return this.status === 403 + } + + /** + * Check if this is a not found error (404). + */ + get isNotFound(): boolean { + return this.status === 404 + } + + /** + * Check if this is a validation error (422). + */ + get isValidationError(): boolean { + return this.status === 422 + } + + /** + * Check if this is a server error (5xx). + */ + get isServerError(): boolean { + return this.status >= 500 + } +} + +/** + * Options for API requests. + */ +export interface RequestOptions { + /** Skip automatic auth header injection */ + skipAuth?: boolean + /** Custom headers to merge with defaults */ + headers?: Record + /** Request body (will be JSON serialized) */ + body?: unknown + /** Query parameters */ + params?: Record + /** AbortSignal for request cancellation */ + signal?: AbortSignal +} + +/** + * Standard API response wrapper from backend. + */ +export interface ApiResponse { + data: T + message?: string +} + +/** + * Paginated response from backend. + */ +export interface PaginatedResponse { + items: T[] + total: number + page: number + pageSize: number + totalPages: number +} + +/** + * Backend error response shape. + */ +export interface ErrorResponse { + detail?: string + message?: string + code?: string +} diff --git a/.claude/frontend-poc/src/assets/main.css b/.claude/frontend-poc/src/assets/main.css new file mode 100644 index 0000000..684fb68 --- /dev/null +++ b/.claude/frontend-poc/src/assets/main.css @@ -0,0 +1,74 @@ +@import "tailwindcss"; + +/* Import pixel font for retro GBC aesthetic */ +@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + +/* Custom theme */ +@theme { + /* Pokemon type colors */ + --color-type-grass: #78C850; + --color-type-fire: #F08030; + --color-type-water: #6890F0; + --color-type-lightning: #F8D030; + --color-type-psychic: #F85888; + --color-type-fighting: #C03028; + --color-type-darkness: #705848; + --color-type-metal: #B8B8D0; + --color-type-fairy: #EE99AC; + --color-type-dragon: #7038F8; + --color-type-colorless: #A8A878; + + /* UI colors */ + --color-primary: #3B82F6; + --color-primary-dark: #2563EB; + --color-primary-light: #60A5FA; + --color-secondary: #6366F1; + --color-secondary-dark: #4F46E5; + --color-secondary-light: #818CF8; + --color-success: #22C55E; + --color-warning: #F59E0B; + --color-error: #EF4444; + + /* Dark theme surfaces */ + --color-surface: #374151; + --color-surface-dark: #1F2937; + --color-surface-light: #4B5563; + + /* Font family */ + --font-pixel: "Press Start 2P", cursive; +} + +@layer base { + html { + @apply bg-gray-900 text-gray-100; + } + + body { + @apply min-h-screen; + } +} + +@layer components { + /* Card styling */ + .card { + @apply bg-surface rounded-lg shadow-lg overflow-hidden; + } + + /* Button variants */ + .btn { + @apply px-4 py-2 rounded font-medium transition-colors duration-200; + } + + .btn-primary { + @apply bg-primary hover:bg-primary-dark text-white; + } + + .btn-secondary { + @apply bg-secondary hover:bg-secondary-dark text-white; + } + + /* Type badges */ + .type-badge { + @apply px-2 py-1 rounded text-xs font-bold uppercase; + } +} diff --git a/.claude/frontend-poc/src/components/NavBottomTabs.spec.ts b/.claude/frontend-poc/src/components/NavBottomTabs.spec.ts new file mode 100644 index 0000000..80e5a48 --- /dev/null +++ b/.claude/frontend-poc/src/components/NavBottomTabs.spec.ts @@ -0,0 +1,307 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { mount } from '@vue/test-utils' +import { setActivePinia, createPinia } from 'pinia' +import { ref } from 'vue' + +// Mock vue-router +const mockRoute = ref({ path: '/', name: 'Dashboard' } as { path: string; name: string }) +vi.mock('vue-router', () => ({ + useRoute: () => mockRoute.value, + RouterLink: { + name: 'RouterLink', + props: ['to'], + template: '', + }, +})) + +// Shared state for mocks - plain refs +const mockAvatarUrl = ref(null) +const mockDisplayName = ref('Test User') + +// Mock user store - use getters to return unwrapped values like real Pinia store +vi.mock('@/stores/user', () => ({ + useUserStore: () => ({ + get avatarUrl() { return mockAvatarUrl.value }, + get displayName() { return mockDisplayName.value }, + }), +})) + +import NavBottomTabs from './NavBottomTabs.vue' + +describe('NavBottomTabs', () => { + beforeEach(() => { + setActivePinia(createPinia()) + mockAvatarUrl.value = null + mockDisplayName.value = 'Test User' + mockRoute.value = { path: '/', name: 'Dashboard' } + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + describe('navigation', () => { + it('renders all navigation tabs', () => { + /** + * Test navigation tab rendering. + * + * The bottom tabs should display all main navigation items so users + * can easily navigate to different sections on mobile. + */ + const wrapper = mount(NavBottomTabs) + + expect(wrapper.text()).toContain('Home') + expect(wrapper.text()).toContain('Play') + expect(wrapper.text()).toContain('Decks') + expect(wrapper.text()).toContain('Cards') + expect(wrapper.text()).toContain('Profile') + }) + + it('includes correct navigation paths', () => { + /** + * Test navigation link paths. + * + * Each tab should link to the correct route so navigation + * works as expected. + */ + const wrapper = mount(NavBottomTabs) + + const links = wrapper.findAll('a') + const hrefs = links.map(l => l.attributes('href')) + + expect(hrefs).toContain('/') + expect(hrefs).toContain('/play') + expect(hrefs).toContain('/decks') + expect(hrefs).toContain('/collection') + expect(hrefs).toContain('/profile') + }) + + it('highlights active tab for home route', () => { + /** + * Test home tab highlighting. + * + * When on the home page, the Home tab should be highlighted + * to show the current location. + */ + mockRoute.value = { path: '/', name: 'Dashboard' } + + const wrapper = mount(NavBottomTabs) + + const links = wrapper.findAll('a') + const homeLink = links.find(l => l.attributes('href') === '/') + expect(homeLink?.classes()).toContain('text-primary-light') + }) + + it('highlights active tab for nested routes', () => { + /** + * Test nested route highlighting. + * + * When on a nested route like /decks/123, the parent tab (Decks) + * should be highlighted to show the user's location. + */ + mockRoute.value = { path: '/decks/123', name: 'EditDeck' } + + const wrapper = mount(NavBottomTabs) + + const links = wrapper.findAll('a') + const decksLink = links.find(l => l.attributes('href') === '/decks') + expect(decksLink?.classes()).toContain('text-primary-light') + }) + }) + + describe('profile tab with avatar', () => { + it('displays avatar image when avatarUrl is provided', () => { + /** + * Test avatar display in profile tab. + * + * When the user has an avatar, it should be displayed in the + * profile tab instead of the generic icon. + */ + mockDisplayName.value = 'Cal' + mockAvatarUrl.value = 'https://example.com/avatar.jpg' + + const wrapper = mount(NavBottomTabs) + + const img = wrapper.find('img') + expect(img.exists()).toBe(true) + expect(img.attributes('src')).toBe('https://example.com/avatar.jpg') + expect(img.attributes('alt')).toBe('Cal') + }) + + it('displays initial fallback when no avatar is provided', () => { + /** + * Test avatar fallback in profile tab. + * + * When users don't have an avatar, we show their first initial + * in the profile tab as a placeholder. + */ + mockDisplayName.value = 'Player' + mockAvatarUrl.value = null + + const wrapper = mount(NavBottomTabs) + + // Profile tab should show the initial + const profileTab = wrapper.findAll('a').find(a => a.attributes('href') === '/profile') + expect(profileTab?.text()).toContain('P') + }) + + it('capitalizes initial in fallback avatar', () => { + /** + * Test initial capitalization in profile tab. + * + * The avatar initial should be uppercase for visual consistency, + * regardless of the display name casing. + */ + mockDisplayName.value = 'testUser' + mockAvatarUrl.value = null + + const wrapper = mount(NavBottomTabs) + + const profileTab = wrapper.findAll('a').find(a => a.attributes('href') === '/profile') + expect(profileTab?.text()).toContain('T') + }) + + it('adds ring highlight to avatar when profile is active', () => { + /** + * Test active state styling for avatar. + * + * When on the profile page, the avatar should have a ring border + * to indicate the tab is active. + */ + mockRoute.value = { path: '/profile', name: 'Profile' } + mockAvatarUrl.value = 'https://example.com/avatar.jpg' + + const wrapper = mount(NavBottomTabs) + + const img = wrapper.find('img') + expect(img.classes()).toContain('ring-2') + expect(img.classes()).toContain('ring-primary-light') + }) + + it('styles initial fallback differently when active', () => { + /** + * Test active state styling for initial fallback. + * + * When on the profile page without an avatar, the initial circle + * should be styled differently to indicate the active state. + */ + mockRoute.value = { path: '/profile', name: 'Profile' } + mockAvatarUrl.value = null + + const wrapper = mount(NavBottomTabs) + + const profileTab = wrapper.findAll('a').find(a => a.attributes('href') === '/profile') + const initialSpan = profileTab?.find('.rounded-full') + expect(initialSpan?.classes()).toContain('bg-primary/30') + expect(initialSpan?.classes()).toContain('text-primary-light') + }) + + it('uses gray styling for initial when not active', () => { + /** + * Test inactive state styling for initial fallback. + * + * When not on the profile page, the initial circle should + * use muted gray colors. + */ + mockRoute.value = { path: '/', name: 'Dashboard' } + mockAvatarUrl.value = null + + const wrapper = mount(NavBottomTabs) + + const profileTab = wrapper.findAll('a').find(a => a.attributes('href') === '/profile') + const initialSpan = profileTab?.find('.rounded-full') + expect(initialSpan?.classes()).toContain('bg-gray-600') + expect(initialSpan?.classes()).toContain('text-gray-300') + }) + }) + + describe('other tabs', () => { + it('displays emoji icons for non-profile tabs', () => { + /** + * Test icon display for regular tabs. + * + * Non-profile tabs should display their emoji icons rather + * than avatars or initials. + */ + const wrapper = mount(NavBottomTabs) + + // Check that emoji icons are present (not rendered as images) + const links = wrapper.findAll('a') + + // Home tab should have home emoji + const homeLink = links.find(l => l.attributes('href') === '/') + expect(homeLink?.html()).not.toContain(' { + it('is hidden on desktop (md and above)', () => { + /** + * Test desktop visibility. + * + * The bottom tabs should be hidden on desktop where the sidebar + * navigation is used instead. + */ + const wrapper = mount(NavBottomTabs) + + const nav = wrapper.find('nav') + expect(nav.classes()).toContain('md:hidden') + }) + + it('is fixed at the bottom of the screen', () => { + /** + * Test fixed positioning. + * + * The bottom tabs should be fixed at the bottom of the viewport + * so they're always accessible on mobile. + */ + const wrapper = mount(NavBottomTabs) + + const nav = wrapper.find('nav') + expect(nav.classes()).toContain('fixed') + expect(nav.classes()).toContain('bottom-0') + }) + + it('has safe area padding for iOS devices', () => { + /** + * Test iOS safe area support. + * + * On iOS devices with home indicator bars, the navigation + * should have extra padding to avoid overlap. + */ + const wrapper = mount(NavBottomTabs) + + const nav = wrapper.find('nav') + expect(nav.classes()).toContain('safe-area-inset-bottom') + }) + }) + + describe('accessibility', () => { + it('uses semantic nav element', () => { + /** + * Test semantic HTML usage. + * + * Using a