diff --git a/archive/legacy-backup-20251209/README.md b/archive/legacy-backup-20251209/README.md new file mode 100644 index 0000000..dbf8a63 --- /dev/null +++ b/archive/legacy-backup-20251209/README.md @@ -0,0 +1,39 @@ +# Legacy Backup - 2025-12-09 + +## Archived Files + +These files were archived during the repository cleanup on 2025-12-09. + +### Deprecated AI Server Actions + +| File | Reason | +| --------------------- | ------------------------------------------------------- | +| `generateQuestion.ts` | Replaced by `src/lib/questionService.ts` and API routes | +| `evaluateCode.ts` | Replaced by `/api/ai/evaluate` route | +| `getHints.ts` | Replaced by `/api/ai/hints` route | +| `revealSolution.ts` | Replaced by `/api/ai/solution` route | +| `optimizeSolution.ts` | Replaced by `/api/ai/optimize` route | + +## Restoration Instructions + +To restore any file: + +```bash +# Copy back from archive +cp archive/legacy-backup-20251209/ src/lib/ai/ + +# Or use git to restore from before cleanup +git checkout pre-cleanup-20251209 -- src/lib/ai/ +``` + +## Why Archived + +These files were marked as DEPRECATED in their headers. They were server actions +that have been replaced by the new API route-based architecture for Gemini calls. + +The new architecture uses: + +- `/api/ai/*` routes for all AI operations +- `src/lib/ai/geminiClient.ts` for client initialization +- `src/lib/ai/modelRouter.ts` for model selection +- `src/lib/questionService.ts` for question generation diff --git a/src/lib/ai/evaluateCode.ts b/archive/legacy-backup-20251209/evaluateCode.ts similarity index 100% rename from src/lib/ai/evaluateCode.ts rename to archive/legacy-backup-20251209/evaluateCode.ts diff --git a/src/lib/ai/generateQuestion.ts b/archive/legacy-backup-20251209/generateQuestion.ts similarity index 100% rename from src/lib/ai/generateQuestion.ts rename to archive/legacy-backup-20251209/generateQuestion.ts diff --git a/src/lib/ai/getHints.ts b/archive/legacy-backup-20251209/getHints.ts similarity index 100% rename from src/lib/ai/getHints.ts rename to archive/legacy-backup-20251209/getHints.ts diff --git a/src/lib/ai/optimizeSolution.ts b/archive/legacy-backup-20251209/optimizeSolution.ts similarity index 100% rename from src/lib/ai/optimizeSolution.ts rename to archive/legacy-backup-20251209/optimizeSolution.ts diff --git a/src/lib/ai/revealSolution.ts b/archive/legacy-backup-20251209/revealSolution.ts similarity index 100% rename from src/lib/ai/revealSolution.ts rename to archive/legacy-backup-20251209/revealSolution.ts diff --git a/cleanup-report.json b/cleanup-report.json new file mode 100644 index 0000000..e5c47fa --- /dev/null +++ b/cleanup-report.json @@ -0,0 +1,90 @@ +{ + "auditDate": "2025-12-09T13:44:43+05:30", + "branch": "chore/cleanup-legacy-20251209", + "summary": { + "unusedDependencies": [], + "lucideReactUsage": false, + "deprecatedFiles": [ + { + "path": "src/lib/ai/generateQuestion.ts", + "reason": "Marked DEPRECATED - replaced by questionService", + "priority": "high", + "action": "archive" + }, + { + "path": "src/lib/ai/evaluateCode.ts", + "reason": "Marked DEPRECATED - replaced by API routes", + "priority": "high", + "action": "archive" + }, + { + "path": "src/lib/ai/getHints.ts", + "reason": "Marked DEPRECATED - replaced by API routes", + "priority": "high", + "action": "archive" + }, + { + "path": "src/lib/ai/revealSolution.ts", + "reason": "Marked DEPRECATED - replaced by API routes", + "priority": "high", + "action": "archive" + }, + { + "path": "src/lib/ai/optimizeSolution.ts", + "reason": "Marked DEPRECATED - replaced by API routes", + "priority": "high", + "action": "archive" + } + ], + "mockDataFiles": [ + { + "path": "src/lib/mockQuestions.ts", + "reason": "Mock data - still referenced by PracticeContext and fallback", + "priority": "low", + "action": "keep-for-now", + "usedBy": [ + "src/app/PracticeContext.tsx", + "src/lib/ai/generateQuestion.ts", + "src/app/api/ai/generate-question/route.ts" + ] + } + ], + "lintIssues": [ + { + "file": "src/components/automode/AutoModeStatsBarV2.tsx", + "line": 96, + "issue": "setState in useEffect", + "action": "fix" + } + ], + "manualReview": [ + { + "file": "src/lib/statsStore.ts", + "note": "Contains legacy compatibility wrappers - keep for backward compat" + } + ] + }, + "removalSequence": [ + { + "commit": "A", + "description": "Archive deprecated AI server actions", + "files": [ + "src/lib/ai/generateQuestion.ts", + "src/lib/ai/evaluateCode.ts", + "src/lib/ai/getHints.ts", + "src/lib/ai/revealSolution.ts", + "src/lib/ai/optimizeSolution.ts" + ] + }, + { + "commit": "B", + "description": "Fix setState in effect issue", + "files": ["src/components/automode/AutoModeStatsBarV2.tsx"] + }, + { + "commit": "C", + "description": "Remove unused STATS_VERSION constant", + "files": ["src/lib/statsStore.ts"] + } + ] +} diff --git a/docs/AUTO_MODE_V2.md b/docs/AUTO_MODE_V2.md new file mode 100644 index 0000000..da69473 --- /dev/null +++ b/docs/AUTO_MODE_V2.md @@ -0,0 +1,100 @@ +# Auto Mode v2: Curriculum-Aware Adaptive Pacing + +## Overview + +Auto Mode v2 provides intelligent, curriculum-aware practice with adaptive difficulty based on user performance. + +## How It Works + +### 1. Mini-Curriculum + +New runs start with a focused 12-question curriculum on **String Manipulation**: + +- Basic string operations (indexing, slicing) +- Two-pointer techniques +- Sliding window patterns +- Pattern matching + +After completing the mini-curriculum (or demonstrating mastery), the system broadens to weakness-based topic selection. + +### 2. Streak-Based Difficulty Progression + +| Event | Action | +| ------------------ | --------------------------------------------------- | +| Correct answer | `streak++` | +| 3 correct in a row | Promote difficulty (beginner→intermediate→advanced) | +| Incorrect answer | `streak = 0`, demote difficulty, inject remediation | + +**Aggressive mode** (opt-in): Promote after 2 correct instead of 3. + +### 3. Difficulty Per Subtopic + +Each subtopic tracks its own difficulty level: + +``` +difficultyPointer: { + "basic-string-operations": "intermediate", + "sliding-window-patterns": "beginner", + "pattern-matching": "advanced" +} +``` + +### 4. Remediation + +When a user answers incorrectly: + +1. Difficulty is demoted for that subtopic +2. 2 extra beginner questions from that subtopic are injected into the queue +3. "Slow Down" button available to manually trigger this + +### 5. Decay Timer + +If inactive for >24 hours, streak is reduced by 50% to prevent stale mastery carryover. + +--- + +## Tuning Parameters + +| Parameter | Default | Description | +| --------------------------- | ------- | ------------------------------------- | +| `streakToPromote` | 3 | Correct answers to promote difficulty | +| `aggressiveStreakToPromote` | 2 | Same, when Fast Mode enabled | +| `extraRemediationCount` | 2 | Questions injected on failure | +| `miniCurriculumSize` | 12 | Initial focused curriculum size | +| `decayHours` | 24 | Hours before streak decays | +| `prefetchBufferSize` | 2 | Questions to prefetch | + +--- + +## User Controls + +### Stats Bar + +- **Breadcrumb**: Current module → subtopic +- **Streak**: 🔥 indicator with animation +- **Difficulty badge**: Current level for subtopic +- **Progress**: Mini-curriculum or ongoing count + +### Quick Settings + +- **Fast Progression**: Toggle for 2-correct promotion +- **Remediation Mode**: Auto-inject questions on mistakes +- **Slow Down**: Reset streak + add easier questions + +--- + +## Storage + +| Key | Content | +| ---------------------------- | --------------------------- | +| `pytrix_auto_run_v2_{runId}` | Individual run state | +| `pytrix_auto_analytics` | Promotion/demotion counters | + +--- + +## Files + +- [`src/lib/autoRunTypes.ts`](./src/lib/autoRunTypes.ts) - Type definitions +- [`src/lib/autoModeServiceV2.ts`](./src/lib/autoModeServiceV2.ts) - Core service +- [`src/components/automode/AutoModeStatsBarV2.tsx`](./src/components/automode/AutoModeStatsBarV2.tsx) - Stats bar +- [`src/components/automode/AutoModeControls.tsx`](./src/components/automode/AutoModeControls.tsx) - Settings popover diff --git a/docs/PAGE-ROLES.md b/docs/PAGE-ROLES.md new file mode 100644 index 0000000..77e4dd2 --- /dev/null +++ b/docs/PAGE-ROLES.md @@ -0,0 +1,80 @@ +# Page Roles & Responsibilities + +This document defines the distinct identities, interaction models, and content hierarchies for the core pages of the PyPractice MVP. + +## 1. Dashboard (`/dashboard`) + +**"Overview & Progress Hub"** + +### Purpose + +Answers: _"How am I doing overall, and where should I go next?"_ + +### Key Responsibilities + +- **High-Level Status**: Global stats (mastery, streaks, problems solved). +- **Navigation Hub**: Suggestions on what to do next (e.g., "Resume Auto Run", "Practice Weakest Topic"). +- **Activity Feed**: Quick history of recent sessions. + +### Anti-Patterns (What NOT to do) + +- Do not show the full curriculum tree (that belongs in Modules). +- Do not include detailed configuration controls for new questions (that belongs in Manual Practice). + +--- + +## 2. Modules (`/modules`) + +**"Curriculum Browser"** + +### Purpose + +Answers: _"What content exists and how is it structured?"_ + +### Key Responsibilities + +- **Content Discovery**: Full browseable tree of Modules -> Subtopics -> Problem Types. +- **Learning Roadmap**: Linear or exploratory view of the curriculum. +- **Deep Linking**: Entry points to start practice on specific topics. + +### Anti-Patterns + +- Do not clutter with global user stats or streaks. +- Do not autoplay practice sessions directly without context. + +--- + +## 3. Manual Practice (`/practice/manual`) + +**"Focused Practice Configurator"** + +### Purpose + +Answers: _"Exactly what type of question do you want to solve right now?"_ + +### Key Responsibilities + +- **Precise Setup**: Form-like controls to select Module, Subtopic, Difficulty, and Constraints. +- **Quick Launch**: Strong "Generate Question" action. +- **Preview**: Brief summary of what will be generated. + +### Anti-Patterns + +- Do not show the full module browsing grid. +- Do not show unrelated global stats. + +--- + +## 4. Practice Workspace (`/practice`) + +**"The IDE"** + +### Purpose + +Answers: _"How do I solve this specific problem?"_ + +### Key Responsibilities + +- **Execution Environment**: Code editor, question panel, output terminal. +- **Feedback Loop**: Run, check, hints, solution reveal. +- **Focus**: Minimal distractions, centered on the current problem. diff --git a/package-lock.json b/package-lock.json index 8f6fee5..b1bb731 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,13 @@ "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", @@ -33,6 +35,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "depcheck": "^1.4.7", "mini-svg-data-uri": "^1.4.4", "monaco-editor": "^0.55.1", "motion": "^12.23.25", @@ -49,17 +52,29 @@ "zustand": "^5.0.9" }, "devDependencies": { + "@playwright/test": "^1.57.0", "@tailwindcss/postcss": "^4", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.0.7", + "jsdom": "^27.3.0", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.0.15" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.28", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.28.tgz", + "integrity": "sha512-LuS6IVEivI75vKN8S04qRD+YySP0RmU/cV8UNukhQZvprxF+76Z43TNo/a08eCodaGhT1Us8etqS1ZRY9/Or0A==", + "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", @@ -73,11 +88,65 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.0.tgz", + "integrity": "sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==", + "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.2" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "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.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "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/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -133,7 +202,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.5", @@ -167,7 +235,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -184,166 +251,750 @@ "@babel/types": "^7.27.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "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.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", + "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "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/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@emnapi/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", - "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1059,7 +1710,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1081,7 +1731,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1091,14 +1740,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1346,6 +1993,22 @@ "react-dom": ">= 16.8" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -1900,16 +2563,71 @@ }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, @@ -2577,88 +3295,396 @@ } } }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.1.tgz", + "integrity": "sha512-HjhlEREguAyBTGNzRlGNiDHGQ2EjLSPWwdhhpoEqHYy8hWak3Dp6/fU72OfqVsiMb8S6rbfPsWUF24fxpilrVA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz", + "integrity": "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@reduxjs/toolkit": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.1.tgz", - "integrity": "sha512-HjhlEREguAyBTGNzRlGNiDHGQ2EjLSPWwdhhpoEqHYy8hWak3Dp6/fU72OfqVsiMb8S6rbfPsWUF24fxpilrVA==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz", - "integrity": "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -3018,6 +4044,64 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3029,6 +4113,24 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -3092,6 +4194,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "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/emscripten": { "version": "1.41.5", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", @@ -3119,6 +4228,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.25", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", @@ -3129,6 +4244,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", @@ -3668,38 +4789,178 @@ ], "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitest/expect": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.15.tgz", + "integrity": "sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.15", + "@vitest/utils": "4.0.15", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz", + "integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.15.tgz", + "integrity": "sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.15", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.15.tgz", + "integrity": "sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.15", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.15.tgz", + "integrity": "sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.15.tgz", + "integrity": "sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz", + "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", + "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", + "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", + "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz", + "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", + "license": "MIT" }, "node_modules/acorn": { "version": "8.15.0", @@ -3724,6 +4985,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3741,11 +5012,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "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" @@ -3803,6 +5082,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -3826,6 +5114,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -3946,6 +5243,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -4003,7 +5319,6 @@ "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": { @@ -4016,11 +5331,20 @@ "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/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", @@ -4031,7 +5355,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -4124,16 +5447,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", + "engines": { + "node": "*" + } + }, "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/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001759", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", @@ -4154,6 +5496,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4189,6 +5541,17 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4218,7 +5581,6 @@ "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" @@ -4231,14 +5593,12 @@ "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/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/convert-source-map": { @@ -4248,6 +5608,22 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4263,6 +5639,35 @@ "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/cssstyle": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.4.tgz", + "integrity": "sha512-KyOS/kJMEq5O9GdPnaf82noigg5X5DYn0kZPJTaAsCUaBizp6Xa1y9D4Qoqf/JazEXWuruErHgVXwjN5391ZJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.0", + "@csstools/css-syntax-patches-for-csstree": "1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -4398,6 +5803,20 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -4456,67 +5875,202 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=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/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depcheck": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz", + "integrity": "sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@vue/compiler-sfc": "^3.3.4", + "callsite": "^1.0.0", + "camelcase": "^6.3.0", + "cosmiconfig": "^7.1.0", + "debug": "^4.3.4", + "deps-regex": "^0.2.0", + "findup-sync": "^5.0.0", + "ignore": "^5.2.4", + "is-core-module": "^2.12.0", + "js-yaml": "^3.14.1", + "json5": "^2.2.3", + "lodash": "^4.17.21", + "minimatch": "^7.4.6", + "multimatch": "^5.0.0", + "please-upgrade-node": "^3.2.0", + "readdirp": "^3.6.0", + "require-package-name": "^2.0.1", + "resolve": "^1.22.3", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "yargs": "^16.2.0" + }, + "bin": { + "depcheck": "bin/depcheck.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/depcheck/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==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/depcheck/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/depcheck/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/depcheck/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/depcheck/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, + "node_modules/deps-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/deps-regex/-/deps-regex-0.2.0.tgz", + "integrity": "sha512-PwuBojGMQAYbWkMXOY9Pd/NWCDNHVH12pnS7WHqZkTSeMESe4hwnKKRp0yR87g37113x4JPbo/oIvXY+s/f56Q==", "license": "MIT" }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, "node_modules/detect-libc": { @@ -4548,6 +6102,13 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dompurify": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", @@ -4600,6 +6161,27 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -4717,6 +6299,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4787,11 +6376,52 @@ "benchmarks" ] }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/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" @@ -5188,6 +6818,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -5224,6 +6867,12 @@ "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", @@ -5240,6 +6889,28 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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", @@ -5318,7 +6989,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -5344,6 +7014,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -5408,11 +7093,25 @@ } } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5469,6 +7168,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5561,6 +7269,48 @@ "node": ">=10.13.0" } }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -5703,7 +7453,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5729,11 +7478,76 @@ "hermes-estree": "0.25.1" } }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/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/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/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" @@ -5753,7 +7567,6 @@ "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", @@ -5776,6 +7589,12 @@ "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==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5818,6 +7637,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -5911,7 +7736,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -5962,7 +7786,6 @@ "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" @@ -5984,6 +7807,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -6008,7 +7840,6 @@ "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" @@ -6047,7 +7878,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -6070,6 +7900,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -6215,6 +8052,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -6226,7 +8072,6 @@ "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/iterator.prototype": { @@ -6261,7 +8106,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -6277,11 +8121,50 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.3.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.3.0.tgz", + "integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -6297,6 +8180,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "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", @@ -6315,7 +8204,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -6645,6 +8533,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6661,6 +8555,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6691,11 +8591,20 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -6723,6 +8632,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6737,7 +8653,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -6760,7 +8675,6 @@ "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" @@ -6834,9 +8748,27 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -7098,6 +9030,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7170,7 +9113,6 @@ "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" @@ -7179,6 +9121,59 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7203,6 +9198,21 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -7216,7 +9226,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -7225,6 +9234,47 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -7239,7 +9289,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7274,6 +9323,41 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7475,6 +9559,18 @@ } } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/recharts": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.5.1.tgz", @@ -7564,6 +9660,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "license": "MIT" + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -7574,7 +9695,6 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -7591,11 +9711,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "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" @@ -7622,6 +9754,48 @@ "node": ">=0.10.0" } }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "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.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7701,6 +9875,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -7717,6 +9911,12 @@ "semver": "bin/semver.js" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -7923,6 +10123,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -7942,6 +10149,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -7949,12 +10162,26 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/state-local": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", "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/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -7969,6 +10196,26 @@ "node": ">= 0.4" } }, + "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==", + "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/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==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -8082,6 +10329,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -8145,7 +10404,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8154,6 +10412,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", @@ -8191,6 +10456,23 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -8231,25 +10513,80 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.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/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=16" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/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": { - "is-number": "^7.0.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=8.0" + "node": ">=20" } }, "node_modules/ts-api-utils": { @@ -8612,6 +10949,320 @@ "d3-timer": "^3.0.1" } }, + "node_modules/vitest": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz", + "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.15", + "@vitest/mocker": "4.0.15", + "@vitest/pretty-format": "4.0.15", + "@vitest/runner": "4.0.15", + "@vitest/snapshot": "4.0.15", + "@vitest/spy": "4.0.15", + "@vitest/utils": "4.0.15", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.15", + "@vitest/browser-preview": "4.0.15", + "@vitest/browser-webdriverio": "4.0.15", + "@vitest/ui": "4.0.15", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.15.tgz", + "integrity": "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.15", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vitest/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/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", + "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^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/vitest/node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "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", @@ -8717,6 +11368,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -8727,6 +11395,23 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -8748,6 +11433,32 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -8755,6 +11466,42 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 8cfd205..6ee8c03 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,13 @@ "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", @@ -34,6 +36,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "depcheck": "^1.4.7", "mini-svg-data-uri": "^1.4.4", "monaco-editor": "^0.55.1", "motion": "^12.23.25", @@ -50,14 +53,19 @@ "zustand": "^5.0.9" }, "devDependencies": { + "@playwright/test": "^1.57.0", "@tailwindcss/postcss": "^4", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.0.7", + "jsdom": "^27.3.0", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.0.15" } } diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 0000000..0ea8c08 --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,85 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..6c90e4f --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests/e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + use: { + baseURL: "http://localhost:3000", + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: { + command: "npm run dev", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/scripts/convert-md-to-json.ts b/scripts/convert-md-to-json.ts new file mode 100644 index 0000000..9982ada --- /dev/null +++ b/scripts/convert-md-to-json.ts @@ -0,0 +1,354 @@ +/** + * Markdown to JSON Converter for Topics + * + * Parses src/data/topics.md and generates src/data/topics.json + * following the Module → Subtopic → ProblemType hierarchy. + * + * Usage: npx tsx scripts/convert-md-to-json.ts + */ + +import * as fs from "fs"; +import * as path from "path"; + +// Types (inline to avoid import issues during script execution) +interface ProblemType { + id: string; + name: string; + description?: string; +} + +interface Subtopic { + id: string; + name: string; + sectionNumber?: string; + concepts?: string[]; + problemTypes: ProblemType[]; +} + +interface Module { + id: string; + name: string; + order: number; + overview?: string; + subtopics: Subtopic[]; + problemArchetypes: string[]; + pythonConsiderations?: string[]; +} + +interface TopicsData { + version: string; + generatedAt: string; + modules: Module[]; +} + +/** + * Converts a display name to kebab-case ID + */ +function toKebabCase(name: string): string { + return name + .toLowerCase() + .replace(/[&]/g, "and") + .replace(/[()\\*]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +/** + * Extracts text content from markdown formatting + */ +function cleanMarkdown(text: string): string { + return text + .replace(/\*\*/g, "") // Remove bold + .replace(/`[^`]+`/g, (match) => match.slice(1, -1)) // Remove inline code but keep content + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // Extract link text + .trim(); +} + +/** + * Parses a bullet point line and extracts the main item and description + */ +function parseBulletItem(line: string): { name: string; description?: string } { + const content = line.replace(/^[-*]\s*/, "").trim(); + const colonIndex = content.indexOf(":"); + if (colonIndex > 0 && colonIndex < 60) { + const name = cleanMarkdown(content.slice(0, colonIndex)); + const description = cleanMarkdown(content.slice(colonIndex + 1)); + return { name, description: description || undefined }; + } + return { name: cleanMarkdown(content) }; +} + +/** + * Main parser for topics.md + */ +function parseTopicsMarkdown(content: string): TopicsData { + const lines = content.split("\n"); + const modules: Module[] = []; + + let currentModule: Module | null = null; + let currentSubtopic: Subtopic | null = null; + let inProblemArchetypes = false; + let inPythonConsiderations = false; + let inOverview = false; + let overviewLines: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + + // Module header: ## 1. String Manipulation + const moduleMatch = trimmed.match(/^##\s+(\d+)\.\s+(.+)$/); + if (moduleMatch) { + // Save previous module + if (currentModule) { + if (currentSubtopic) { + currentModule.subtopics.push(currentSubtopic); + currentSubtopic = null; + } + modules.push(currentModule); + } + + const order = parseInt(moduleMatch[1], 10); + const name = cleanMarkdown(moduleMatch[2]); + + currentModule = { + id: toKebabCase(name), + name, + order, + subtopics: [], + problemArchetypes: [], + }; + inProblemArchetypes = false; + inPythonConsiderations = false; + inOverview = false; + overviewLines = []; + continue; + } + + // Skip if no current module + if (!currentModule) continue; + + // Section headers + if (trimmed.startsWith("### ")) { + // Save current subtopic to module + if (currentSubtopic) { + currentModule.subtopics.push(currentSubtopic); + currentSubtopic = null; + } + + const sectionName = trimmed.slice(4).trim(); + + if (sectionName === "Overview") { + inOverview = true; + inProblemArchetypes = false; + inPythonConsiderations = false; + } else if (sectionName === "Problem Archetypes") { + inProblemArchetypes = true; + inPythonConsiderations = false; + inOverview = false; + if (overviewLines.length > 0) { + currentModule.overview = overviewLines.join(" ").trim(); + overviewLines = []; + } + } else if ( + sectionName === "Python-Specific Considerations" || + sectionName.includes("Python") + ) { + inPythonConsiderations = true; + inProblemArchetypes = false; + inOverview = false; + currentModule.pythonConsiderations = []; + } else if (sectionName === "Sub-topics") { + inOverview = false; + inProblemArchetypes = false; + inPythonConsiderations = false; + if (overviewLines.length > 0) { + currentModule.overview = overviewLines.join(" ").trim(); + overviewLines = []; + } + } + continue; + } + + // Overview content (paragraph after ### Overview) + if (inOverview && trimmed && !trimmed.startsWith("#")) { + overviewLines.push(trimmed); + continue; + } + + // Subtopic header: #### 1.1 Basic String Operations + const subtopicMatch = trimmed.match(/^####\s+(\d+\.\d+)\s+(.+)$/); + if (subtopicMatch) { + // Save previous subtopic + if (currentSubtopic) { + currentModule.subtopics.push(currentSubtopic); + } + + const sectionNumber = subtopicMatch[1]; + const name = cleanMarkdown(subtopicMatch[2]); + + currentSubtopic = { + id: toKebabCase(name), + name, + sectionNumber, + problemTypes: [], + }; + inProblemArchetypes = false; + inPythonConsiderations = false; + inOverview = false; + continue; + } + + // Problem archetypes list + if (inProblemArchetypes && trimmed.startsWith("-")) { + const items = trimmed + .slice(1) + .split(",") + .map((s) => cleanMarkdown(s.trim())) + .filter((s) => s.length > 0); + currentModule.problemArchetypes.push(...items); + continue; + } + + // Python considerations list + if (inPythonConsiderations && trimmed.startsWith("-")) { + const consideration = cleanMarkdown(trimmed.slice(1).trim()); + if (consideration && currentModule.pythonConsiderations) { + currentModule.pythonConsiderations.push(consideration); + } + continue; + } + + // Problem types within subtopic (bullet points) + if ( + currentSubtopic && + !inProblemArchetypes && + !inPythonConsiderations && + trimmed.startsWith("-") + ) { + const { name, description } = parseBulletItem(trimmed); + if (name) { + currentSubtopic.problemTypes.push({ + id: toKebabCase(name), + name, + description, + }); + } + continue; + } + } + + // Save last module and subtopic + if (currentModule) { + if (currentSubtopic) { + currentModule.subtopics.push(currentSubtopic); + } + modules.push(currentModule); + } + + return { + version: "1.0.0", + generatedAt: new Date().toISOString(), + modules, + }; +} + +/** + * Validates the generated data + */ +function validateTopicsData(data: TopicsData): string[] { + const errors: string[] = []; + + if (data.modules.length === 0) { + errors.push("No modules found"); + } + + const kebabCaseRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/; + + for (const module of data.modules) { + if (!kebabCaseRegex.test(module.id)) { + errors.push(`Module ID not kebab-case: "${module.id}"`); + } + if (!module.name) { + errors.push(`Module ${module.id} has no name`); + } + + for (const subtopic of module.subtopics) { + if (!kebabCaseRegex.test(subtopic.id)) { + errors.push( + `Subtopic ID not kebab-case: "${subtopic.id}" in ${module.id}` + ); + } + + for (const pt of subtopic.problemTypes) { + if (!kebabCaseRegex.test(pt.id)) { + errors.push( + `ProblemType ID not kebab-case: "${pt.id}" in ${subtopic.id}` + ); + } + } + } + } + + return errors; +} + +/** + * Main execution + */ +function main() { + const projectRoot = path.resolve(__dirname, ".."); + const inputPath = path.join(projectRoot, "src/data/topics.md"); + const outputPath = path.join(projectRoot, "src/data/topics.json"); + + console.log("🔄 Converting topics.md to topics.json...\n"); + + // Read input + if (!fs.existsSync(inputPath)) { + console.error(`❌ Input file not found: ${inputPath}`); + process.exit(1); + } + + const markdown = fs.readFileSync(inputPath, "utf-8"); + console.log(`📖 Read ${markdown.length} bytes from topics.md`); + + // Parse + const data = parseTopicsMarkdown(markdown); + + // Validate + const errors = validateTopicsData(data); + if (errors.length > 0) { + console.warn("\n⚠️ Validation warnings:"); + errors.forEach((e) => console.warn(` - ${e}`)); + } + + // Stats + const totalSubtopics = data.modules.reduce( + (acc, m) => acc + m.subtopics.length, + 0 + ); + const totalProblemTypes = data.modules.reduce( + (acc, m) => + acc + m.subtopics.reduce((a, s) => a + s.problemTypes.length, 0), + 0 + ); + const totalArchetypes = data.modules.reduce( + (acc, m) => acc + m.problemArchetypes.length, + 0 + ); + + console.log(`\n📊 Statistics:`); + console.log(` Modules: ${data.modules.length}`); + console.log(` Subtopics: ${totalSubtopics}`); + console.log(` Problem Types: ${totalProblemTypes}`); + console.log(` Archetypes: ${totalArchetypes}`); + + // Write output + const jsonOutput = JSON.stringify(data, null, 2); + fs.writeFileSync(outputPath, jsonOutput, "utf-8"); + + console.log(`\n✅ Written ${jsonOutput.length} bytes to topics.json`); + console.log(` Path: ${outputPath}`); +} + +main(); diff --git a/scripts/migrate-stats.ts b/scripts/migrate-stats.ts new file mode 100644 index 0000000..77f9d90 --- /dev/null +++ b/scripts/migrate-stats.ts @@ -0,0 +1,368 @@ +/** + * Stats Migration Script + * + * Migrates old flat pypractice-stats to new hierarchical pytrix_stats_v2. + * + * Run with: npx tsx scripts/migrate-stats.ts + */ + +// This script is designed to be run in a browser context via the console +// or as a Node.js script after mocking localStorage + +const STORAGE_KEY_OLD = "pypractice-stats"; +const STORAGE_KEY_V2 = "pytrix_stats_v2"; + +interface OldDifficultyStats { + attempts: number; + solved: number; +} + +interface OldTopicStats { + topic: string; + beginner?: OldDifficultyStats; + intermediate?: OldDifficultyStats; + advanced?: OldDifficultyStats; + attempts: number; + solved: number; +} + +interface OldGlobalStats { + version?: number; + totalAttempts: number; + totalSolved: number; + topicsTouched: number; + masteryPercent: number; + perTopic: OldTopicStats[]; +} + +interface NewDifficultyStats { + attempts: number; + solved: number; + avgTimeTakenMs: number; + lastAttemptAt: number; +} + +interface NewProblemTypeStats { + problemTypeId: string; + problemTypeName: string; + beginner: NewDifficultyStats; + intermediate: NewDifficultyStats; + advanced: NewDifficultyStats; + attempts: number; + solved: number; +} + +interface NewSubtopicStats { + subtopicId: string; + subtopicName: string; + problemTypes: NewProblemTypeStats[]; + attempts: number; + solved: number; + masteryPercent: number; +} + +interface NewModuleStats { + moduleId: string; + moduleName: string; + subtopics: NewSubtopicStats[]; + attempts: number; + solved: number; + masteryPercent: number; +} + +interface NewGlobalStats { + version: 3; + totalAttempts: number; + totalSolved: number; + totalTimeTakenMs: number; + modulesTouched: number; + subtopicsTouched: number; + masteryPercent: number; + modules: NewModuleStats[]; + lastUpdatedAt: number; +} + +// Topic name to module ID mapping (best effort) +const TOPIC_TO_MODULE_MAP: Record = { + strings: "string-manipulation", + string: "string-manipulation", + "string manipulation": "string-manipulation", + lists: "lists-and-arrays", + list: "lists-and-arrays", + arrays: "lists-and-arrays", + array: "lists-and-arrays", + dictionaries: "dictionaries-and-hashmaps", + dictionary: "dictionaries-and-hashmaps", + dict: "dictionaries-and-hashmaps", + hashmaps: "dictionaries-and-hashmaps", + hashmap: "dictionaries-and-hashmaps", + loops: "loops-and-iteration", + loop: "loops-and-iteration", + iteration: "loops-and-iteration", + recursion: "recursion", + "sorting and searching": "sorting-and-searching", + sorting: "sorting-and-searching", + searching: "sorting-and-searching", + "linked lists": "linked-lists", + "linked list": "linked-lists", + stacks: "stacks-and-queues", + queues: "stacks-and-queues", + stack: "stacks-and-queues", + queue: "stacks-and-queues", + trees: "trees-and-graphs", + tree: "trees-and-graphs", + graphs: "trees-and-graphs", + graph: "trees-and-graphs", + "dynamic programming": "dynamic-programming", + dp: "dynamic-programming", +}; + +function createEmptyDiffStats(): NewDifficultyStats { + return { + attempts: 0, + solved: 0, + avgTimeTakenMs: 0, + lastAttemptAt: 0, + }; +} + +function calculateMastery(attempts: number, solved: number): number { + if (attempts === 0) return 0; + return Math.min(100, Math.round((solved / attempts) * 100)); +} + +function toKebabCase(str: string): string { + return str + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .trim(); +} + +function migrateStats(oldStats: OldGlobalStats): NewGlobalStats { + const newStats: NewGlobalStats = { + version: 3, + totalAttempts: oldStats.totalAttempts || 0, + totalSolved: oldStats.totalSolved || 0, + totalTimeTakenMs: 0, // No time data in old format + modulesTouched: 0, + subtopicsTouched: 0, + masteryPercent: oldStats.masteryPercent || 0, + modules: [], + lastUpdatedAt: Date.now(), + }; + + // Group old topics by inferred module + const moduleMap = new Map(); + + for (const oldTopic of oldStats.perTopic || []) { + const topicLower = oldTopic.topic.toLowerCase(); + const moduleId = + TOPIC_TO_MODULE_MAP[topicLower] || toKebabCase(oldTopic.topic); + + if (!moduleMap.has(moduleId)) { + moduleMap.set(moduleId, []); + } + moduleMap.get(moduleId)!.push(oldTopic); + } + + // Convert to new structure + for (const [moduleId, topics] of moduleMap.entries()) { + // Use first topic name as module name (best effort) + const moduleName = topics[0]?.topic || moduleId; + + const moduleStats: NewModuleStats = { + moduleId, + moduleName, + subtopics: [], + attempts: 0, + solved: 0, + masteryPercent: 0, + }; + + for (const oldTopic of topics) { + const subtopicId = toKebabCase(oldTopic.topic); + const subtopicStats: NewSubtopicStats = { + subtopicId, + subtopicName: oldTopic.topic, + problemTypes: [], + attempts: oldTopic.attempts || 0, + solved: oldTopic.solved || 0, + masteryPercent: calculateMastery( + oldTopic.attempts || 0, + oldTopic.solved || 0 + ), + }; + + // Create a synthetic problem type with the old difficulty stats + const problemTypeStats: NewProblemTypeStats = { + problemTypeId: `${subtopicId}-general`, + problemTypeName: `${oldTopic.topic} (migrated)`, + beginner: oldTopic.beginner + ? { + ...createEmptyDiffStats(), + attempts: oldTopic.beginner.attempts, + solved: oldTopic.beginner.solved, + } + : createEmptyDiffStats(), + intermediate: oldTopic.intermediate + ? { + ...createEmptyDiffStats(), + attempts: oldTopic.intermediate.attempts, + solved: oldTopic.intermediate.solved, + } + : createEmptyDiffStats(), + advanced: oldTopic.advanced + ? { + ...createEmptyDiffStats(), + attempts: oldTopic.advanced.attempts, + solved: oldTopic.advanced.solved, + } + : createEmptyDiffStats(), + attempts: oldTopic.attempts || 0, + solved: oldTopic.solved || 0, + }; + + subtopicStats.problemTypes.push(problemTypeStats); + moduleStats.subtopics.push(subtopicStats); + moduleStats.attempts += subtopicStats.attempts; + moduleStats.solved += subtopicStats.solved; + } + + moduleStats.masteryPercent = calculateMastery( + moduleStats.attempts, + moduleStats.solved + ); + newStats.modules.push(moduleStats); + } + + // Calculate global touched counts + newStats.modulesTouched = newStats.modules.filter( + (m) => m.attempts > 0 + ).length; + newStats.subtopicsTouched = newStats.modules.reduce( + (sum, m) => sum + m.subtopics.filter((s) => s.attempts > 0).length, + 0 + ); + + return newStats; +} + +// ============================================ +// BROWSER MIGRATION FUNCTION +// ============================================ + +/** + * Run this function in the browser console to migrate stats. + */ +export function runMigration(): { success: boolean; message: string } { + if (typeof window === "undefined" || typeof localStorage === "undefined") { + return { + success: false, + message: "This script must be run in a browser environment.", + }; + } + + // Check for existing v2 stats + const existingV2 = localStorage.getItem(STORAGE_KEY_V2); + if (existingV2) { + try { + const parsed = JSON.parse(existingV2); + if (parsed.version === 3 && parsed.modules?.length > 0) { + return { + success: true, + message: `Stats v2 already exists with ${parsed.modules.length} modules. Skipping migration.`, + }; + } + } catch { + // Corrupted, will overwrite + } + } + + // Load old stats + const oldStatsRaw = localStorage.getItem(STORAGE_KEY_OLD); + if (!oldStatsRaw) { + return { + success: true, + message: "No old stats found. Nothing to migrate.", + }; + } + + try { + const oldStats = JSON.parse(oldStatsRaw) as OldGlobalStats; + const newStats = migrateStats(oldStats); + + // Save new stats + localStorage.setItem(STORAGE_KEY_V2, JSON.stringify(newStats)); + + // Backup old stats (don't delete) + localStorage.setItem(`${STORAGE_KEY_OLD}_backup`, oldStatsRaw); + + return { + success: true, + message: `Migration complete! Migrated ${newStats.modules.length} modules, ${newStats.subtopicsTouched} subtopics. Total: ${newStats.totalAttempts} attempts.`, + }; + } catch (error) { + return { + success: false, + message: `Migration failed: ${ + error instanceof Error ? error.message : String(error) + }`, + }; + } +} + +/** + * Rollback to old stats format. + */ +export function rollbackMigration(): { success: boolean; message: string } { + if (typeof window === "undefined" || typeof localStorage === "undefined") { + return { + success: false, + message: "This script must be run in a browser environment.", + }; + } + + const backup = localStorage.getItem(`${STORAGE_KEY_OLD}_backup`); + if (!backup) { + return { + success: false, + message: "No backup found. Cannot rollback.", + }; + } + + localStorage.setItem(STORAGE_KEY_OLD, backup); + localStorage.removeItem(STORAGE_KEY_V2); + + return { + success: true, + message: "Rollback complete. Old stats restored.", + }; +} + +// ============================================ +// CLI ENTRY (for Node.js with mocked localStorage) +// ============================================ + +// If running directly (not imported) +if (typeof require !== "undefined" && require.main === module) { + console.log("=== Stats Migration Script ==="); + console.log(""); + console.log("This script migrates pypractice-stats to pytrix_stats_v2."); + console.log(""); + console.log("To run in the browser:"); + console.log("1. Open your app in the browser"); + console.log("2. Open DevTools (F12)"); + console.log("3. Go to Console tab"); + console.log("4. Paste and run:"); + console.log(""); + console.log(" const result = (() => {"); + console.log(" // Copy the runMigration function here"); + console.log(" return runMigration();"); + console.log(" })();"); + console.log(" console.log(result);"); + console.log(""); +} + +export { migrateStats }; diff --git a/scripts/run-internal-llm-check.ts b/scripts/run-internal-llm-check.ts new file mode 100644 index 0000000..cd3d842 --- /dev/null +++ b/scripts/run-internal-llm-check.ts @@ -0,0 +1,42 @@ +import { getInternalClient } from "../src/lib/internalTestClient"; + +async function runInternalCheck() { + console.log("Starting internal LLM check..."); + + if (process.env.NODE_ENV !== "development") { + console.error("This script must be run in development mode."); + process.exit(1); + } + + const client = await getInternalClient(); + if (!client) { + console.log("No internal client available (missing key?). Skipping check."); + process.exit(0); + } + + try { + console.log("Generating test question (Strings)..."); + const result = await client.generateContent( + "question-generation", + "Generate a simple Python question about string reversal.", + (text) => text // raw text + ); + + if (result.success) { + console.log("✅ Question generation successful!"); + console.log("Preview:", String(result.data).substring(0, 100) + "..."); + } else { + console.error("❌ Question generation failed:", result.message); + process.exit(1); + } + + // Verify blocking in production (simulated) + // We cannot simulate process.env change easily here without reload, + // but the logic in modelRouter is present. + } catch (error) { + console.error("❌ Unexpected error:", error); + process.exit(1); + } +} + +runInternalCheck(); diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..3747359 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { DashboardLayout } from "@/components/layout/DashboardLayout"; +import { StatsRow } from "@/components/dashboard/StatsRow"; +import { NextStepsPanel } from "@/components/dashboard/NextStepsPanel"; +import { RecentActivityRow } from "@/components/dashboard/RecentActivityRow"; +import { Separator } from "@/components/ui/separator"; + +export default function DashboardPage() { + return ( + + {/* 1. Global Stats Row */} +
+

Overview

+ +
+ + {/* 2. Next Actions */} +
+

+ What to do next +

+ +
+ + + + {/* 3. Recent Activity */} +
+
+

+ Recent Activity +

+ {/* Link to full history could go here */} +
+ +
+
+ ); +} diff --git a/src/app/modules/page.tsx b/src/app/modules/page.tsx new file mode 100644 index 0000000..f65c21f --- /dev/null +++ b/src/app/modules/page.tsx @@ -0,0 +1,32 @@ +import { Suspense } from "react"; +import { Metadata } from "next"; +import { + ModulesGrid, + ModulesGridSkeleton, +} from "@/components/modules/ModulesGrid"; + +export const metadata: Metadata = { + title: "Browse Modules | PyPractice", + description: + "Explore all DSA and Python modules with subtopics and problem types", +}; + +export default function ModulesPage() { + return ( +
+ {/* Page Header */} +
+

Browse Modules

+

+ Explore all DSA topics organized by module, subtopic, and problem + type. +

+
+ + {/* Modules Grid */} + }> + + +
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 0bb3419..67ba516 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,6 +2,7 @@ import { StatsRow } from "@/components/dashboard/StatsRow"; import { TopicGrid } from "@/components/dashboard/TopicGrid"; +import { ModuleStatsGrid } from "@/components/dashboard/ModuleStatsGrid"; import { Separator } from "@/components/ui/separator"; import { Button } from "@/components/ui/button"; import { @@ -88,6 +89,16 @@ export default function Home() { + {/* Module Progress Section (Hierarchical Stats) */} +
+

+ Module Progress +

+ +
+ + + {/* Topics Section */}

diff --git a/src/app/practice/auto/page.tsx b/src/app/practice/auto/page.tsx index ae1a6c1..24e1402 100644 --- a/src/app/practice/auto/page.tsx +++ b/src/app/practice/auto/page.tsx @@ -1,11 +1,18 @@ "use client"; /** - * Auto Mode Page - * Launches or continues Auto Mode runs. + * Auto Mode Landing Page v2 + * + * Dashboard-style control center for adaptive learning: + * - Curriculum path preview + * - Adaptive learning cards + * - Past insights + * - Action zone with New/Continue buttons + * - Settings panel */ -import { useState } from "react"; +import { useState, useMemo } from "react"; +import { useRouter } from "next/navigation"; import { SaveFileDialog } from "@/components/automode/SaveFileDialog"; import { Button } from "@/components/ui/button"; import { @@ -15,76 +22,522 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Lightning, Sparkle, Target, TrendUp } from "@phosphor-icons/react"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + Lightning, + Sparkle, + Target, + TrendUp, + CaretRight, + Play, + ArrowRight, + Fire, + Hourglass, + Stairs, + GearSix, + CaretDown, + Brain, + Rocket, + Info, +} from "@phosphor-icons/react"; import { useRequireApiKey } from "@/hooks/useRequireApiKey"; +import { getStats } from "@/lib/statsStore"; +import { + getAllAutoRunsV2, + createAutoRunV2, + getCurrentQueueEntry, + getSubtopicDifficulty, + generateMiniCurriculum, + DEFAULT_AUTO_RUN_CONFIG, +} from "@/lib/autoModeServiceV2"; +import type { AutoRunV2, DifficultyLevel } from "@/lib/autoRunTypes"; +import { cn } from "@/lib/utils"; -export default function AutoModePage() { - const [dialogOpen, setDialogOpen] = useState(false); - const { isLoading } = useRequireApiKey(); +// ============================================ +// DIFFICULTY ICON COMPONENT +// ============================================ - if (isLoading) return null; +function DifficultyIcon({ + difficulty, + className, +}: { + difficulty: DifficultyLevel; + className?: string; +}) { + switch (difficulty) { + case "beginner": + return ( + + ); + case "intermediate": + return ; + case "advanced": + return ; + } +} + +// ============================================ +// CURRICULUM PATH PREVIEW +// ============================================ + +function CurriculumPathPreview({ run }: { run: AutoRunV2 | null }) { + const curriculum = useMemo(() => generateMiniCurriculum(6), []); + const currentIndex = run?.currentIndex ?? 0; return ( -
-
-

- Auto Mode -

-

- AI-powered adaptive practice that focuses on your weakest areas. -

-
+ + + + + Curriculum Path + + + New runs start with String Manipulation and expand based on mastery + + + +
+ {curriculum.slice(0, 5).map((entry, idx) => { + const isActive = run && idx === currentIndex; + const isCompleted = run && idx < currentIndex; - {/* Feature Cards */} -
- - - - Adaptive Learning - - - - Questions adjust to your skill level, focusing on areas that need - improvement. - - - + return ( +
+ + {entry.subtopicName} + + {idx < 4 && ( + + )} +
+ ); + })} + + + more + +
+ + + ); +} - - - - Track Progress - - - - Monitor your improvement across topics and difficulty levels over - time. - - - +// ============================================ +// ADAPTIVE LEARNING CARDS +// ============================================ + +function AdaptiveLearningCards({ run }: { run: AutoRunV2 | null }) { + const entry = run ? getCurrentQueueEntry(run) : null; + const difficulty = + run && entry ? getSubtopicDifficulty(run, entry.subtopicId) : "beginner"; + + return ( +
+ {/* Card A: Streak-Based Progression */} + +
+ +
+ + Dynamic Difficulty +
+
+ + + Difficulty increases after 2–3 consecutive correct answers. Mistakes + trigger targeted remediation. + + {run && ( +
+
+ + {run.streak} +
+ + + {difficulty} + +
+ )} +
+ + + {/* Card B: Weakness Detection */} + +
+ +
+ + Focus Where You Struggle +
+
+ + + Prioritizes weaker subtopics and inserts extra beginner-level + questions when needed. + + {run && Object.keys(run.perSubtopicStats).length > 0 && ( +
+

+ {Object.keys(run.perSubtopicStats).length} subtopics tracked +

+
+ )} +
+ + + {/* Card C: Question Buffering */} + +
+ +
+ + Low-Latency Questions +
+
+ + + 2–3 upcoming questions prefetched using light models for + near-instant transitions. + +
+ + Buffer:{" "} + {run?.prefetchSize ?? DEFAULT_AUTO_RUN_CONFIG.prefetchBufferSize} + + + flash-lite + +
+
+ + + {/* Card D: Personalized Trajectory */} + +
+ +
+ + Skill Growth Path +
+
+ + + Curriculum expands: Strings → Lists → Dictionaries → Loops → + Patterns based on evolving mastery. + + + +
+ ); +} + +// ============================================ +// PAST INSIGHTS +// ============================================ +function PastInsights({ run }: { run: AutoRunV2 | null }) { + const stats = getStats(); + + if (!run && stats.totalAttempts === 0) { + return ( + + + +

No practice history yet

+

+ Start your first adaptive run to see insights here +

+
+
+ ); + } + + const accuracy = + stats.totalAttempts > 0 + ? Math.round((stats.totalSolved / stats.totalAttempts) * 100) + : 0; + + return ( + + + + + Your Progress + + + +
+
+

{stats.totalAttempts}

+

Total Attempts

+
+
+

{accuracy}%

+

Accuracy

+
+
+

{run?.streak ?? 0}

+

Current Streak

+
+
+

{run?.completedQuestions ?? 0}

+

This Run

+
+
+ + {run && ( +
+
+ Run Progress + + {run.currentIndex + 1} / {run.topicQueue.length} + +
+ +
+ )} +
+
+ ); +} + +// ============================================ +// SETTINGS PANEL +// ============================================ + +function SettingsPanel({ + aggressiveProgression, + remediationMode, + onAggressiveChange, + onRemediationChange, +}: { + aggressiveProgression: boolean; + remediationMode: boolean; + onAggressiveChange: (v: boolean) => void; + onRemediationChange: (v: boolean) => void; +}) { + const [open, setOpen] = useState(false); + + return ( + + + + + - - - Smart Prioritization - - - - Automatically prioritizes weakest topics first for efficient - learning. - + +
+
+ +

+ Promote difficulty after 2 correct (vs 3) +

+
+ +
+ +
+
+ +

+ Add extra questions when struggling +

+
+ +
+
+
+ ); +} + +// ============================================ +// MAIN PAGE +// ============================================ + +export default function AutoModePage() { + const router = useRouter(); + const [dialogOpen, setDialogOpen] = useState(false); + + const { isLoading: apiKeyLoading } = useRequireApiKey(); + + // Load runs lazily to avoid setState in effect + const [initialData] = useState(() => { + if (typeof window === "undefined") { + return { runs: [], latestRun: null }; + } + const runs = getAllAutoRunsV2(); + return { + runs, + latestRun: runs.length > 0 ? runs[0] : null, + }; + }); + + const [latestRun, setLatestRun] = useState( + initialData.latestRun + ); + const [aggressiveProgression, setAggressiveProgression] = useState( + initialData.latestRun?.aggressiveProgression ?? false + ); + const [remediationMode, setRemediationMode] = useState( + initialData.latestRun?.remediationMode ?? true + ); + + const handleStartNewRun = () => { + const newRun = createAutoRunV2(undefined, { + aggressiveProgression, + remediationMode, + }); + router.push(`/practice?mode=auto&saveId=${newRun.id}`); + }; + + const handleContinueRun = () => { + if (latestRun) { + router.push(`/practice?mode=auto&saveId=${latestRun.id}`); + } + }; + + if (apiKeyLoading) { + return ( +
+ + +
+ + + + +
+ ); + } - {/* Start Button */} -
- + return ( +
+ {/* Header */} +
+
+

+ Adaptive Auto Mode +

+ {latestRun && ( + + Run Active + + )} +
+

+ Personalized Python practice driven by real-time performance and an + evolving curriculum. +

+
+ +

+ New runs begin with Basic String Manipulation and + adapt difficulty based on your streak and mastery. Prepare for a + personalized learning journey. +

+
+ {/* Curriculum Path Preview */} + + + {/* Adaptive Learning Cards */} + + + {/* Past Insights */} + + + {/* Action Zone */} + + +
+ + {latestRun && ( + + )} +
+

+ New runs always begin at basic strings and scale up quickly if you + demonstrate mastery. +

+
+
+ + {/* Settings Panel */} + + + {/* Legacy Dialog (for backwards compatibility) */}
); diff --git a/src/app/practice/manual/page.tsx b/src/app/practice/manual/page.tsx index 64107e7..b506fe4 100644 --- a/src/app/practice/manual/page.tsx +++ b/src/app/practice/manual/page.tsx @@ -2,29 +2,172 @@ /** * Manual Practice Landing Page - * Allows users to select a topic and difficulty before starting practice. + * + * Modules-first layout with toggle for "By Problem Type" view. + * Persists view preference in localStorage. */ -import { TopicGrid } from "@/components/dashboard/TopicGrid"; +import { useState } from "react"; +import { PracticeModulesView } from "@/components/practice/PracticeModulesView"; +import { PracticeByProblemTypeView } from "@/components/practice/PracticeByProblemTypeView"; import { useRequireApiKey } from "@/hooks/useRequireApiKey"; +import type { Difficulty } from "@/lib/types"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + SquaresFour, + ListBullets, + Lightning, + Brain, + Rocket, +} from "@phosphor-icons/react"; +import { cn } from "@/lib/utils"; + +// Storage key for view preference +const VIEW_PREF_KEY = "pypractice-manual-view"; +type ViewMode = "modules" | "problem-type"; + +// Get initial view mode from localStorage +function getInitialViewMode(): ViewMode { + if (typeof window === "undefined") return "modules"; + const stored = localStorage.getItem(VIEW_PREF_KEY); + if (stored === "modules" || stored === "problem-type") return stored; + return "modules"; +} export default function ManualPracticePage() { - const { isLoading } = useRequireApiKey(); + const { isLoading: apiKeyLoading } = useRequireApiKey(); + + // View mode (modules or problem-type) + const [viewMode, setViewMode] = useState(getInitialViewMode); + const [difficulty, setDifficulty] = useState("beginner"); - if (isLoading) return null; + // Persist preference + const handleViewChange = (mode: ViewMode) => { + setViewMode(mode); + localStorage.setItem(VIEW_PREF_KEY, mode); + }; + + if (apiKeyLoading) return null; return (
-
-

- Manual Practice -

-

- Choose a topic and difficulty to start practicing. -

+ {/* Header */} +
+
+

+ Manual Practice +

+

+ Choose a topic and start practicing at your own pace. +

+
+ + {/* View Toggle + Difficulty */} +
+ {/* Difficulty Toggle (for modules view) */} + {viewMode === "modules" && ( +
+ + + +
+ )} + + {/* View Mode Toggle */} +
+ + +
+
- + {/* Mobile Difficulty Selector (shown in modules view) */} + {viewMode === "modules" && ( +
+ Difficulty: + setDifficulty("beginner")} + > + Beginner + + setDifficulty("intermediate")} + > + Intermediate + + setDifficulty("advanced")} + > + Advanced + +
+ )} + + {/* Content */} + {viewMode === "modules" ? ( + + ) : ( + + )}
); } diff --git a/src/app/practice/page.tsx b/src/app/practice/page.tsx index 4662fc7..ac3731b 100644 --- a/src/app/practice/page.tsx +++ b/src/app/practice/page.tsx @@ -57,6 +57,9 @@ import { evaluateCode, } from "@/lib/aiClient"; +// Question Service for topic-select mode +import { getTemplateQuestion } from "@/lib/questionService"; + // Question Buffer Service import { initBuffer, @@ -87,6 +90,9 @@ function PracticeWorkspace() { const topicId = searchParams.get("topic") || "Strings"; const saveId = searchParams.get("saveId"); const historyId = searchParams.get("historyId"); // For review mode + const moduleParam = searchParams.get("module"); // For manual mode + const subtopicParam = searchParams.get("subtopic"); // For manual mode + const problemTypeParam = searchParams.get("problemType"); // For topic-select/manual mode const difficultyParam = searchParams.get( "difficulty" ) as DifficultyLevel | null; @@ -195,6 +201,50 @@ function PracticeWorkspace() { } }, [mode, historyId, router]); + // Topic-select mode: Load question from sessionStorage or generate new + useEffect(() => { + if (mode !== "topic-select") return; + if (!problemTypeParam) { + toast.error("No problem type specified"); + router.push("/practice/manual"); + return; + } + + // Try to load from sessionStorage first + const pending = sessionStorage.getItem("pendingQuestion"); + if (pending) { + try { + const { question: pendingQ } = JSON.parse(pending); + setQuestion(pendingQ); + setCode( + pendingQ.starterCode || + `def solve(input_data):\n # Write your solution here\n pass` + ); + sessionStorage.removeItem("pendingQuestion"); + setIsLoading(false); + toast.info(`Practice: ${pendingQ.title}`); + return; + } catch { + // Fall through to generate + } + } + + // Generate from problemTypeParam + const newQ = getTemplateQuestion(problemTypeParam, currentDifficulty); + if (newQ) { + setQuestion(newQ); + setCode( + newQ.starterCode || + `def solve(input_data):\n # Write your solution here\n pass` + ); + setIsLoading(false); + toast.info(`Practice: ${newQ.title}`); + } else { + toast.error("Failed to generate question"); + router.push("/practice/manual"); + } + }, [mode, problemTypeParam, currentDifficulty, router]); + // Load Question via Buffer Service useEffect(() => { let isMounted = true; @@ -209,7 +259,8 @@ function PracticeWorkspace() { // For Auto Mode, get topic from save file if (mode === "auto" && saveFile) { - targetTopic = getCurrentTopic(saveFile); + const entry = getCurrentTopic(saveFile); + targetTopic = entry.problemTypeName; } // Use buffer service - gets first question immediately, prefetches in background @@ -245,9 +296,9 @@ function PracticeWorkspace() { } } - // Only load if we have what we need (skip if review mode) - if (mode === "review") { - // Review mode handled by separate effect + // Only load if we have what we need (skip if review or topic-select mode) + if (mode === "review" || mode === "topic-select") { + // These modes handled by separate effects return; } @@ -453,7 +504,8 @@ function PracticeWorkspace() { const rotated = advanceTopic(saveFile); setSaveFile(rotated); } - targetTopic = getCurrentTopic(saveFile); + const entry = getCurrentTopic(saveFile); + targetTopic = entry.problemTypeName; } // Use buffered question - instant if available @@ -482,6 +534,31 @@ function PracticeWorkspace() { }; const handleRegenerate = async () => { + // Topic-select mode: use questionService template + if (mode === "topic-select" && problemTypeParam) { + setIsLoading(true); + try { + const newQ = getTemplateQuestion(problemTypeParam, currentDifficulty); + if (newQ) { + setQuestion(newQ); + setCode( + newQ.starterCode || + `def solve(input_data):\n # Write your solution here\n pass` + ); + setRunResult({ status: "not_run", stdout: "", stderr: "" }); + setFailedAttempts(0); + setIsSolutionRevealed(false); + setHintsUsed(0); + toast.info(`Regenerated: ${newQ.title}`); + } else { + toast.error("Failed to regenerate question."); + } + } finally { + setIsLoading(false); + } + return; + } + if (!saveFile && mode !== "auto") { // Manual mode regenerate setIsLoading(true); @@ -509,8 +586,11 @@ function PracticeWorkspace() { if (mode === "auto" && saveFile) { setIsLoading(true); try { - const currentTopic = getCurrentTopic(saveFile); - const newQ = await generateQuestion(currentTopic, currentDifficulty); + const entry = getCurrentTopic(saveFile); + const newQ = await generateQuestion( + entry.problemTypeName, + currentDifficulty + ); setQuestion(newQ); setCode( diff --git a/src/app/support/help/content.tsx b/src/app/support/help/content.tsx index 14ff00e..b3013f9 100644 --- a/src/app/support/help/content.tsx +++ b/src/app/support/help/content.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/no-unescaped-entities */ import { BookOpen, RocketLaunch, @@ -10,6 +11,19 @@ import { ChartBar, Note, IconProps, + Command, + Gear, + ListBullets, + Target, + TerminalWindow, + Database, + GitBranch, + CheckCircle, + Info, + ArrowRight, + Bug, + Sun, + Moon, } from "@phosphor-icons/react"; import { ReactNode, ForwardRefExoticComponent, RefAttributes } from "react"; import Link from "next/link"; @@ -30,17 +44,6 @@ import { AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; -import { - CheckCircle, - Info, - ArrowRight, - Bug, - TerminalWindow, - Database, - Target, - GitBranch, - SpinnerGap, -} from "@phosphor-icons/react"; // ============================================ // TYPE DEFINITIONS @@ -49,12 +52,15 @@ import { export type HelpSectionId = | "about" | "getting-started" - | "core-features" + | "curriculum" + | "practice-modes" + | "command-center" + | "stats" | "api-llm" | "limits" - | "stats" - | "shortcuts" + | "settings" | "privacy" + | "shortcuts" | "known-issues" | "faq"; @@ -93,63 +99,84 @@ export const HELP_SECTIONS: HelpSection[] = [ title: "Getting Started", icon: RocketLaunch, description: - "Step-by-step setup: configure your API key, understand the dashboard, and solve your first problem.", + "Step-by-step setup: get your API key, configure the app, and start your first practice session.", + }, + { + id: "curriculum", + title: "Curriculum & Modules", + icon: ListBullets, + description: + "Understanding the Module → Subtopic → Archetype hierarchy and how to navigate the content library.", }, { - id: "core-features", - title: "Core Features", + id: "practice-modes", + title: "Practice Modes", icon: Lightning, description: - "Deep dive into Manual Practice, Auto Mode, difficulty levels, optimal AI solutions, and the Python runtime.", + "Deep dive into Manual Practice for precision and Auto Mode for adaptive, flow-state learning.", + }, + { + id: "command-center", + title: "Command Center", + icon: Command, + description: + "Master the global command palette (⌘K) for fast navigation, search, and power actions.", + }, + { + id: "stats", + title: "Stats & Progress", + icon: ChartBar, + description: + "How we measure your mastery, track your history, and help you visualize your improvement over time.", }, { id: "api-llm", title: "API & LLM Usage", icon: Cpu, description: - "How Pytrix talks to the Gemini API, what is sent, what is received, and how your key is used at runtime.", + "How the BYOK (Bring Your Own Key) model works, data privacy, and understanding token usage.", }, { id: "limits", title: "Free Tier & Limits", icon: Warning, description: - "How free tiers, quotas, and rate limits work, and how Pytrix helps you stay within your usage budget.", - }, - { - id: "stats", - title: "Stats & History", - icon: ChartBar, - description: - "What each metric means (attempts, mastery, API usage) and how to review your past questions and runs.", + "Managing quotas, avoiding rate limits, and dealing with 'Resource Exhausted' errors.", }, { - id: "shortcuts", - title: "Keyboard Shortcuts", - icon: Keyboard, + id: "settings", + title: "Settings & Customization", + icon: Gear, description: - "All the hotkeys that make navigation, running code, and switching views faster and smoother.", + "Configuring themes, editor fonts, difficulty defaults, and managing your API key.", }, { id: "privacy", title: "Privacy & Security", icon: ShieldCheck, description: - "Where your data lives, how your API key is stored, and what Pytrix does—and does not—send to external APIs.", + "Where your data lives (spoiler: on your device) and what is sent to Google.", + }, + { + id: "shortcuts", + title: "Keyboard Shortcuts", + icon: Keyboard, + description: + "All the hotkeys to navigate Pytrix like a pro without lifting your hands from the keyboard.", }, { id: "known-issues", title: "Known Limitations", icon: Note, description: - "Current browser-based constraints, occasional AI quirks, and areas for future improvement.", + "Current constraints of browser-based Python and occasional AI quirks.", }, { id: "faq", title: "FAQ & Troubleshooting", icon: Question, description: - "Common questions, error explanations, and quick fixes for problems with keys, limits, or question generation.", + "Quick answers to common questions about keys, code execution, and bug reporting.", }, ]; @@ -160,9 +187,9 @@ export const HELP_SECTIONS: HelpSection[] = [ export const SHORTCUTS_DATA: Record = { general: [ { - action: "Open Command Palette", + action: "Open Command Center", keys: ["⌘", "K"], - description: "Quick access to all commands and navigation", + description: "Search modules, navigate, or run commands", }, { action: "Toggle Sidebar", @@ -174,41 +201,24 @@ export const SHORTCUTS_DATA: Record = { { action: "Run & Check Code", keys: ["⌘", "Enter"], - description: "Execute code with Pyodide and AI validation", + description: "Execute code and validate with AI", }, { action: "Save Code", keys: ["⌘", "S"], - description: "Save current code to history", + description: "Save snapshot to history (auto-saved on run)", }, ], navigation: [ { action: "Go to Dashboard", keys: ["⌘", "K", "→", "Home"], - description: "Navigate to main dashboard", - }, - { - action: "Go to Manual Practice", - keys: ["⌘", "K", "→", "Manual"], - description: "Start topic-specific practice", + description: "Via Command Center", }, { - action: "Go to Auto Mode", - keys: ["⌘", "K", "→", "Auto"], - description: "Start adaptive practice session", - }, - { - action: "Go to History", - keys: ["⌘", "K", "→", "History"], - description: "View past questions and attempts", - }, - ], - practice: [ - { - action: "Reset Topic Stats", - keys: ["Right-click", "Topic Card"], - description: "Clear progress for a specific topic", + action: "Quick Practice", + keys: ["⌘", "K", "→", "Auto/Manual"], + description: "Start a session instantly", }, ], }; @@ -220,47 +230,27 @@ export const SHORTCUTS_DATA: Record = { export const FAQ_DATA: FAQItem[] = [ { q: "Why do I need my own API key?", - a: "Pytrix runs entirely in your browser and connects directly to Google's Gemini API. To keep your data private and your usage under your control, requests are made with your own API key instead of a shared server key. This also means the app can remain free and serverless.", - }, - { - q: "How is my API key stored?", - a: "Your key is stored locally in your browser's localStorage. It is never sent to a Pytrix backend server or any third party except Google Gemini. You can remove it at any time from Settings → API & Keys.", + a: "Pytrix is a client-side application with no backend server costs. By bringing your own (free) Google Gemini key, you get unlimited learning without a subscription, and you maintain full control over your data.", }, { - q: "What happens if I hit rate limits or free-tier quotas?", - a: "The Gemini API may return errors like 'Resource Exhausted' or HTTP 429. This means you've made too many requests or used up your free quota for a short period. Wait a few minutes, reduce rapid question regeneration, or avoid repeatedly calling 'Check with AI'.", + q: "Is my API key safe?", + a: "Yes. It is stored only in your browser's Local Storage. It is never sent to any Pytrix server. It is only sent directly to Google's API for the sole purpose of generating questions and feedback.", }, { - q: "Can I use Pytrix offline?", - a: "Once loaded, the Python runtime (Pyodide) can execute your code offline. However, generating new questions, hints, or AI-optimized solutions always requires an internet connection and available API quota.", + q: "What happens if I hit the rate limit?", + a: "You might see a '429 Resource Exhausted' error. This is normal on the free tier if you generate many questions quickly. Just wait a minute and try again. Pytrix's Auto Mode buffers questions to help prevent this.", }, { - q: "How does Auto Mode pick topics and difficulties?", - a: "Auto Mode looks at your per-topic stats—attempts, correctness, and inferred mastery. It prefers topics where you've practiced less or solved fewer problems, and can adjust difficulty based on your recent performance to keep you in a productive challenge zone.", + q: "Does Pytrix work offline?", + a: "Partially. The Python runtime (Pyodide) works offline once loaded, so you can run your code. However, generating *new* questions or getting AI feedback requires an active internet connection to reach Google Gemini.", }, { - q: "What exactly is tracked in Stats & History?", - a: "For each attempt, Pytrix tracks the topic, difficulty, mode (Manual or Auto), whether you passed, your code snapshot, and timestamp. Aggregated stats like 'Problems Solved', mastery percentage, and per-difficulty counts are computed from this history in your browser.", + q: "Can I customize the Auto Mode difficulty?", + a: "Auto Mode manages difficulty automatically, but you can set a 'Preferred Starting Difficulty' in Settings to bias the initial questions towards Beginner, Intermediate, or Advanced.", }, { - q: "Are the AI questions and solutions always correct?", - a: "LLMs are powerful but not perfect. Most of the time questions and solutions are solid, but occasionally you may see edge cases or sub-optimal code. Treat them like a smart assistant, not a ground-truth judge—if something looks suspicious, investigate and learn from it.", - }, - { - q: "How do I reset my progress?", - a: "From the Dashboard, use the 'Reset Stats' button to clear your stored stats and mastery. You can also clear individual topics via their context menu (right-click), or wipe everything (including your key) by clearing Pytrix data from your browser's storage.", - }, - { - q: "Why does the first question take a while to load?", - a: "Pyodide (the in-browser Python runtime) loads on first use, which can take 5-10 seconds depending on your connection. After that, all Python execution is instant. The initial Gemini API call also needs to connect, but subsequent questions are typically faster due to buffering.", - }, - { - q: "What Python libraries are available in Pyodide?", - a: "Pyodide includes the Python standard library plus many popular packages like numpy, pandas (basic), and more. However, some packages with C extensions or system dependencies may not work. Most DSA and beginner Python code will run fine.", - }, - { - q: "Can I contribute questions or improve the AI prompts?", - a: "Yes! Pytrix is open source. You can suggest improvements, report bugs, or contribute code via GitHub. Check the Bug Report page for links to the repository.", + q: "How do I clear my data?", + a: "Go to Settings -> Danger Zone to clear specific data (like history or stats) or perform a full reset to wipe everything including your API key.", }, ]; @@ -330,14 +320,7 @@ function FeatureCard({ title, description, icon, details }: FeatureCardProps) { } // ============================================ -// SECTION COMPONENT MAPPING (export placeholder) -// ============================================ - -export const HELP_SECTION_COMPONENTS: Record = - {} as Record; - -// ============================================ -// SECTION CONTENT COMPONENTS +// SECTION COMPONENTS // ============================================ // --- About Section --- @@ -355,9 +338,9 @@ export function AboutSection() { Master Python with AI-Powered Practice - A privacy-first, verified practice environment designed for students - and developers who want to sharpen their Python skills with - intelligent, adaptive feedback—all running in your browser. + Pytrix is a browser-based, privacy-first practice platform that uses + AI to generate infinite coding problems tailored to your skill + level. It runs Python locally and adapts to your progress. @@ -367,35 +350,35 @@ export function AboutSection() { className="w-6 h-6 text-yellow-500 shrink-0" />
-

Adaptive Learning

+

Adaptive Auto Mode

- Auto Mode adapts to your skill level in real-time, targeting - your weakest topics. + Automatically identifies your weak spots and builds a + personalized practice queue.

-
-

Privacy First

+

Local Python Runtime

- Your code runs in your browser. All stats stay local. You own - your data. + Executes code instantly in your browser using Pyodide + (WebAssembly).

-
-

Real Python Runtime

+

Privacy by Design

- Powered by Pyodide (WebAssembly), not fake simulations. + Your API key and history never leave your device.

@@ -405,97 +388,30 @@ export function AboutSection() { className="w-6 h-6 text-purple-500 shrink-0" />
-

Infinite Questions

+

Uncapped Potential

- Google Gemini generates fresh problems tailored to your needs. + No fixed curriculum. Infinite variations of any problem type.

-
-
-

Core Philosophy

-

- Pytrix is built on the idea that doing is better - than reading. Instead of static tutorials, we give you a live Python - environment and an "Infinite Problem Generator" powered by - Google Gemini. You bring the API key, you own the practice, and you - control the pace. -

-
- -
-

Who is this for?

-
    -
  • - Students preparing for exams or building - foundational skills -
  • -
  • - Job seekers practicing DSA and Python interview - questions -
  • -
  • - Developers sharpening their problem-solving speed - and syntax fluency -
  • -
  • - Educators looking for a self-guided practice tool - for students -
  • -
-
- -
-

- What makes it different? -

-
- - -

- No Server, No Tracking -

-

- All data lives in your browser. Your API key never leaves your - device. -

-
-
- - -

- Real Python Execution -

-

- Pyodide runs actual Python 3.11+ in WebAssembly, not a sandbox - simulation. -

-
-
- - -

- AI That Learns You -

-

- Auto Mode adapts based on your weak spots, not random - shuffling. -

-
-
- - -

Free & Open

-

- Open source. Free tier friendly. No subscription required. -

-
-
-
-
+
+

What Pytrix is NOT

+
    +
  • + It is not a hosted competitive programming platform + (like LeetCode's server). +
  • +
  • + It is not a static tutorial site; you learn by + doing. +
  • +
  • + It is not storing your data on a cloud database. +
  • +
); @@ -507,10 +423,10 @@ export function GettingStartedSection() {
- First Time Here? + Initial Setup - Follow these steps to set up Pytrix and start solving Python problems - in minutes. + Pytrix requires a valid Google Gemini API key to function. The + dashboard will remain blurred/locked until a key is configured. @@ -518,7 +434,7 @@ export function GettingStartedSection() { - Get API Key + Get Key from Google @@ -535,12 +451,12 @@ export function GettingStartedSection() { - - Open API Settings + + Enter API Key @@ -549,152 +465,139 @@ export function GettingStartedSection() { +
+
+ ); +} - - - Start Manual Practice - - - - } - /> +// --- Curriculum Section (NEW) --- +export function CurriculumSection() { + return ( +
+

+ Pytrix organizes Python concepts into a structured but flexible + hierarchy. +

- - - Launch Auto Mode - - - - } - /> +
+ + + + + 1. Modules + + + +

+ High-level categories like "Strings", "Lists", or "OOP". Pytrix + includes a core set of modules covering standard Python topics. +

+
+
- - - View Stats - - - - } - /> + + + + + 2. Subtopics + + + +

+ Specific concepts within a module. E.g., inside "Strings", you + find "Slicing", "Formatting", and "Methods". +

+
+
+ + + + + + 3. Archetypes + + + +

+ Concrete problem patterns. E.g., "Reverse a string" or "Find max + in list". You can practice a specific pattern endlessly. +

+
+
+ + + + Smart Search + + Use the Command Center (⌘K) to instantly find any + Module, Subtopic, or Archetype. Pytrix's search is granular—you can + jump straight to "List Comprehensions" without navigating menus. + +
); } -// --- Core Features Section --- -export function CoreFeaturesSection() { +// --- Practice Modes Section (Refactored) --- +export function PracticeModesSection() { return (
} - description="Select from 12 Python topics (Strings, Lists, Tuples, Sets, Dictionaries, Functions, Errors, OOP, Classes, Modules, Files, Pandas) and 3 difficulty levels. Perfect for focused, topic-specific drilling." + description="Complete control. Select exactly what you want to practice." details={[ - "Choose topic + difficulty combination", - "Generate unlimited questions with 'Get New Question'", - "Run code locally with Pyodide", - "Check correctness with AI validation", - "Request hints or reveal optimal solutions", + "Drill specific Modules, Subtopics, or Archetypes", + "Force a Difficulty (Beginner/Intermediate/Advanced)", + "Deep linking support—share a specific problem setup", + "Great for: Homework, targeted revision, debugging specific skills", ]} /> } - description="An adaptive practice session that keeps you in the flow zone. Auto Mode analyzes your stats and queues up questions from your weakest topics, automatically." + description="Flow state. Let Pytrix decide what you need." details={[ - "Creates topic queue based on your weak spots", - "Buffers next question while you solve current one", - "Automatically advances after successful submission", - "Saves progress in named sessions", - "Export/import runs for backup", - ]} - /> - - } - description="Powered by Pyodide, a full Python interpreter compiled to WebAssembly. Your code runs locally, instantly, with no server roundtrip." - details={[ - "Python 3.11+ with standard library", - "Common packages: numpy, pandas basics", - "Stdout/stderr capture for debugging", - "Timeout protection against infinite loops", - "No network latency for execution", - ]} - /> - - } - description="When you're stuck or get an error, Pytrix's AI analyzes your specific code and provides contextual hints, error explanations, or optimal solutions." - details={[ - "Run & Check: validates against AI expectations", - "Get Hint: progressive nudges without spoilers", - "Reveal Solution: see optimal, commented code", - "Optimize Solution: AI refactors your working code", - "Model fallback: flash-lite → flash → pro", + "Analyzes which topics you are weakest in", + "Builds a dynamic queue of mixed topics", + "Increases difficulty after streaks of correct answers", + "Smart Buffering: Loads the next question while you code", + "Great for: Daily practice, general fluency, warming up", ]} />
-

- Question Generation & Buffering -

+

Adaptive Intelligence

- -
-
- -
-

- Smart Buffering -

-

- In Auto Mode, Pytrix prefetches the next question while - you're working on the current one. This eliminates - waiting time and keeps you in flow state. -

-
+ +
+ +
+

+ Streak-Based Progression +

+

+ In Auto Mode, answering 2-3 questions correctly in a row + triggers a difficulty increase. Struggling with a concept will + lower the difficulty or offer remediation. +

-
- -
-

- Adaptive Difficulty -

-

- Questions are generated with AI-driven constraints matching - your selected difficulty. Beginner = simple syntax, Advanced - = multi-step algorithms. -

-
+
+
+ +
+

Session Resume

+

+ Auto Mode sessions are saved automatically. You can close the + tab and resume your "Run" later from where you left off. +

@@ -704,223 +607,52 @@ export function CoreFeaturesSection() { ); } -// --- API & LLM Section --- -export function ApiLlmSection() { +// --- Command Center Section (NEW) --- +export function CommandCenterSection() { return (
-

- Pytrix uses a Bring Your Own Key (BYOK) architecture. - The application code runs entirely in your browser and communicates - directly with Google's Gemini API using your personal API key. +

+ The Command Center is the power-user's heart of Pytrix. + Access it anywhere with + K (Mac) or Ctrl + K (Windows).

- - - Data Flow - - -
- You (Browser) -
- -
- Next.js API Route (Proxy) -
- -
- Google Gemini API -
- -
- Pytrix Display -
-
-
- -
- - - What is sent? - - -
    -
  • Topic selection & difficulty constraints
  • -
  • Your code (only when you request checking/help)
  • -
  • System prompts defining AI behavior
  • -
  • Your API key via X-API-Key header (to proxy only)
  • -
-
-
+
- - What is NOT sent? + + Features - -
    -
  • Your personal stats or history database
  • -
  • Your browser data or cookies
  • -
  • Other users' data (app is fully client-side)
  • -
  • API key is never stored server-side
  • -
+ +

+ • Navigation: Jump to Dashboard, Stats, or + Settings. +

+

+ • Deep Search: Type "List" to see the Module + "Lists", Subtopics like "List Slicing", and Archetypes like + "Filter a List". +

+

+ • Quick Actions: Toggle Theme, clear history, or + reset state. +

+

+ • Smart Suggestions: Shows "Practice Weakest + Subtopic" based on your actual stats. +

-
- -
-

Model Router & Fallback

-

- Pytrix uses a cost-aware model router to balance quality and quota - usage: -

-
-
- - 1st - -
-

gemini-2.5-flash-lite

-

- Fast, lightweight, perfect for question generation and hints -

+ +
+
+ + K
+

Try it now!

-
- - 2nd - -
-

gemini-2.5-flash

-

- More capable, used for complex evaluations if lite fails -

-
-
-
- - 3rd - -
-

gemini-2.5-pro

-

- Highest quality, fallback for edge cases or premium operations -

-
-
-
-
-
- ); -} - -// --- Limits Section --- -export function LimitsSection() { - return ( -
- - - Using the Free Tier - - Google AI Studio provides a generous free tier, but it has rate limits - (Requests Per Minute) and daily quotas. Pytrix helps you stay within - them. - - - -
-

Free Tier Quotas

-
- - -
-
-

gemini-2.5-flash-lite

-

- ~1500 calls/day, ~1M tokens/day -

-
- Most Used -
-
-
- - -
-
-

gemini-2.5-flash

-

- ~500 calls/day, ~500K tokens/day -

-
-
-
-
- - -
-
-

gemini-2.5-pro

-

- ~50 calls/day, ~100K tokens/day -

-
-
-
-
-
-
- -
-

Tips to Avoid Rate Limits

-
-
- -
- Buffer questions: In - Auto Mode, Pytrix prefetches the next question while you're - solving the current one. This smooths out request spikes. -
-
-
- -
- - Use "Run" first: - {" "} - The internal "Run Code" button uses the local browser - Python runtime. It's free and instant. Only use "Check - with AI" if you need validation. -
-
-
- -
- - Don't spam Regenerate: - {" "} - Hitting the regenerate button repeatedly will quickly exhaust your - RPM quota. -
-
-
- -
- Monitor usage: Check - the API Usage page to see your daily consumption and model - distribution. -
-
-
+
- - - - Common Error: HTTP 429 - - If you see "Too Many Requests" or "Resource - Exhausted," you've hit Google's rate limit. Wait 60 - seconds and try again. Consider slowing down question generation. - -
); } @@ -930,151 +662,191 @@ export function StatsSection() { return (

- Pytrix tracks every attempt locally in your browser. These stats power - the adaptive Auto Mode and help you visualize your progress. + Pytrix tracks metrics locally to visualize your growth.

-
+
Mastery %

- A weighted score of how many topics you've attempted and your - verified success rate. Formula: (solved / attempts) × 100. + A composite score derived from your success rate and difficulty + level across all topics.

- Topics Touched + Weakest Topics

- The number of unique Python concepts (out of 12) you've - practiced at least once. Higher = broader coverage. + Identifies modules where you have high failure rates or low + attempt counts. Auto Mode targets these.

- Problems Solved + History Log

- Total count of questions marked correct by AI validation. Includes - Manual and Auto Mode. + Every code execution is logged. You can review past solutions and + AI feedback from the History page.

+
+ + + + Aggregation + + Stats are aggregated from the bottom up. Solving "List Slicing" + improves your stats for the "Lists" module and your overall global + mastery. + + +
+ ); +} + +// --- API & LLM Section --- +export function ApiLlmSection() { + return ( +
+
+

+ Pytrix acts as a Local Client for the Gemini API. +

+
    +
  • + You provide the key. +
  • +
  • + Google provides the intelligence. +
  • +
  • + Pytrix provides the interface and runtime. +
  • +
+
+ +
- Total Attempts + What is sent? -

- Every time you click "Run & Check" counts as an - attempt, whether correct or not. -

+
    +
  • Topic constraints (e.g., "Create a list problem")
  • +
  • User Code (only when you click "Check with AI")
  • +
  • Your API Key (in transit only, for authentication)
  • +
- Per-Difficulty Stats + What is NOT sent? -

- Separate tracking for Beginner, Intermediate, and Advanced - problems. Helps identify where you excel. -

+
    +
  • Your dashboard stats or history
  • +
  • Personal identity information
  • +
  • Browser cookies or local files
  • +
+
+
+ ); +} + +// --- Limits Section --- +export function LimitsSection() { + return ( +
+ + + Rate Limits + + Google's free tier has a Request Per Minute (RPM) limit. If you click + "Get Question" too fast, you may see a 429 error. Pytrix tries to + handle this gracefully, but if it happens, just wait 60 seconds. + + + +

+ Tips for smooth practice: +

+
    +
  • + Use Run Code (⌘+Enter) frequently. It uses the local + runtime and costs 0 API tokens. +
  • +
  • + Only use Check with AI when you are ready to submit + or stuck. +
  • +
  • + Auto Mode creates a buffer to mask network latency and API limits. +
  • +
+
+ ); +} + +// --- Settings Section (NEW) --- +export function SettingsSection() { + return ( +
+

+ Customize your experience in{" "} + + Settings + + . +

+ +
- - History + + Appearance - -

- A chronological log of every question, your code snapshot, and the - result. Stored in your browser, revisit anytime. -

+ +
+ / + Switch between Light and Dark mode. +
-
- -
-

How Auto Mode Uses Stats

- -
-
- -
-

Weakest Topics

-

- Auto Mode calls{" "} - getWeakestTopics() to - identify topics with fewer attempts or lower success rates, - prioritizing them in the queue. -

-
-
-
- -
-

- Adaptive Rotation -

-

- After 3 questions from a topic, Auto Mode rotates to the - next in queue. This prevents grinding and ensures balanced - exposure. -

-
-
-
+ + Editor + + + Change font size, enabling ligatures, or editor wrapping + preferences.
-
- -
-

API Usage Tracking

-

- Every AI call is logged with model name, input/output tokens, and - timestamp. View detailed breakdowns in the{" "} - - API Usage - {" "} - page. -

- -
- Total Calls: - Tracked per model -
-
- Input Tokens: - Summed daily -
-
- Output Tokens: - Summed daily -
-
- Rate Limit Hits: - Auto-tracked for alerts -
+ + API Key + + + Update or remove your stored Google Gemini API key. + +
+ + + Danger Zone + + + Reset all stats, clear history, or wipe application state + completely.
@@ -1082,6 +854,34 @@ export function StatsSection() { ); } +// --- Privacy Section --- +export function PrivacySection() { + return ( +
+ + + Local Storage Only + + Pytrix has no backend database. If you clear your browser + cookies/data, your Pytrix progress is wiped. + + + +
+

+ We designed Pytrix to be privacy-preserving by default. We do not + track you. We do not use analytics cookies. +

+

+ The only external connection is to{" "} + generativelanguage.googleapis.com (Google Gemini) to + generate questions. +

+
+
+ ); +} + // --- Shortcuts Section --- export function ShortcutsSection() { return ( @@ -1118,214 +918,37 @@ export function ShortcutsSection() { ); } -// --- Privacy Section --- -export function PrivacySection() { - return ( -
- - - Local First - - We do not control where your data lives. It lives on your device, in - your browser, under your control. - - - -
-
-

Where is my data?

-

- Everything—your API key, your stats, your history logs—is stored in - your browser's{" "} - - localStorage - - . If you clear your browser data, you lose your Pytrix data. No - remote server has a copy. -

-
-
-

- How is my API key handled? -

-

- Your key is stored in{" "} - - apiKeyStore.ts - {" "} - and passed to Next.js API routes via the{" "} - - X-API-Key - {" "} - header. The API route uses it to call Gemini, then discards it. The - key is never logged, never stored server-side, never sent to anyone - but Google. -

-
-
-

- Why do I need to bring my own key? -

-

- This architecture ensures we don't need a backend database to - store keys or user accounts. It keeps the app free, private, and - fast. You control your quota, you pay nothing (on free tier), and - you can revoke access anytime. -

-
-
- -
-

What data does Google see?

- - -
    -
  • - - - Google sees your API requests (prompts, code submissions) to - generate responses. - -
  • -
  • - - - Google logs usage for billing/quotas but does NOT use free - tier data to train models (per their terms). - -
  • -
  • - - - Google does NOT see your stats, history, or other Pytrix - metadata. - -
  • -
-
-
-
- - - - Browser Storage Limits - - localStorage typically has a 5-10MB limit per domain. If you solve - hundreds of problems, history may grow large. Consider exporting your - data periodically. - - -
- ); -} - // --- Known Issues Section --- export function KnownIssuesSection() { return (
-

- Pytrix is a browser-based app with some inherent limitations. Here are - known issues and areas for future improvement: -

-
- - - AI-Generated Questions May Be Imperfect - - - -

- LLMs are powerful but not infallible. Occasionally, a generated - question may have unclear wording, incorrect test cases, or - suboptimal solutions. Use the "Get New Question" button - to regenerate if needed. -

-
-
- - - - - Pyodide Takes Time to Load - - - -

- The first Python execution in a session can take 5-10 seconds as - Pyodide downloads (~30MB). After that, all execution is instant. - This is a one-time cost per page load. -

-
-
- - - - - Browser Storage is Ephemeral - + + Pyodide Load Time

- If you clear your browser cache or use Incognito mode, all stats - and history are lost. Consider exporting your data regularly or - using a persistent browser profile. + The first time you run code, the Python runtime must download + (~10-20MB). This can take a few seconds. Subsequent runs are + instant.

- - - Limited Python Package Support - + + AI Hallucinations

- Pyodide supports many packages, but not all. Packages with C - extensions or system dependencies may fail. Most standard library - and pure-Python packages work fine. -

-
-
- - - - No Multi-Device Sync - - -

- Because data is stored locally, your progress doesn't sync - across devices. Future versions may add optional cloud backup. -

-
-
- - - - - Rate Limits Can Be Hit Quickly - - - -

- Free tier rate limits are generous but finite. Rapid question - generation or repeated AI checks can exhaust your quota. Be - mindful of usage. + Rarely, the AI might generate a question with a subtly incorrect + solution. If a question seems impossible or wrong, you can skip it + or regenerate.

- - - - Help Us Improve - - Found a bug or have a feature request? Open an issue on GitHub or use - the Bug Report form. - -
); } @@ -1350,9 +973,7 @@ export function FaqSection() {
-

- Still have questions? Need help with an error? -

+

Still have questions?

@@ -1372,14 +993,22 @@ export function FaqSection() { ); } -// Update HELP_SECTION_COMPONENTS mapping -HELP_SECTION_COMPONENTS.about = AboutSection; -HELP_SECTION_COMPONENTS["getting-started"] = GettingStartedSection; -HELP_SECTION_COMPONENTS["core-features"] = CoreFeaturesSection; -HELP_SECTION_COMPONENTS["api-llm"] = ApiLlmSection; -HELP_SECTION_COMPONENTS.limits = LimitsSection; -HELP_SECTION_COMPONENTS.stats = StatsSection; -HELP_SECTION_COMPONENTS.shortcuts = ShortcutsSection; -HELP_SECTION_COMPONENTS.privacy = PrivacySection; -HELP_SECTION_COMPONENTS["known-issues"] = KnownIssuesSection; -HELP_SECTION_COMPONENTS.faq = FaqSection; +// ============================================ +// COMPONENT MAPPING +// ============================================ + +export const HELP_SECTION_COMPONENTS: Record = { + about: AboutSection, + "getting-started": GettingStartedSection, + curriculum: CurriculumSection, + "practice-modes": PracticeModesSection, + "command-center": CommandCenterSection, + stats: StatsSection, + "api-llm": ApiLlmSection, + limits: LimitsSection, + settings: SettingsSection, + privacy: PrivacySection, + shortcuts: ShortcutsSection, + "known-issues": KnownIssuesSection, + faq: FaqSection, +}; diff --git a/src/components/CommandCenter.tsx b/src/components/CommandCenter.tsx new file mode 100644 index 0000000..2d09b5b --- /dev/null +++ b/src/components/CommandCenter.tsx @@ -0,0 +1,369 @@ +"use client"; + +import React, { useEffect, useState, useMemo } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import { useTheme } from "next-themes"; +import { + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandSeparator, +} from "@/components/ui/command"; +import { + House, + Books, + Code, + Lightning, + Clock, + ChartLine, + Cpu, + Gear, + Question, + BookOpen, + Hash, + GitBranch, + Sun, + Moon, + Key, +} from "@phosphor-icons/react"; +import { + getStaticPages, + getModuleItems, + getSubtopicItems, + getArchetypeItems, + searchItems, + SearchResult, +} from "@/lib/searchIndex"; + +export function CommandCenter() { + const [open, setOpen] = useState(false); + const router = useRouter(); + const pathname = usePathname(); + const [query, setQuery] = useState(""); + const { theme, setTheme } = useTheme(); + + // Memoize static lists + const staticPages = useMemo(() => getStaticPages(), []); + const moduleItems = useMemo(() => getModuleItems(), []); + // Subtopics and Archetypes are loaded but filtering happens on render + const subtopicItems = useMemo(() => getSubtopicItems(), []); + const archetypeItems = useMemo(() => getArchetypeItems(), []); + + // Filter Logic + const isSearching = query.trim().length > 0; + + const displayedPages = useMemo( + () => (isSearching ? searchItems(staticPages, query) : staticPages), + [isSearching, query, staticPages] + ); + + const displayedModules = useMemo( + () => (isSearching ? searchItems(moduleItems, query) : moduleItems), + [isSearching, query, moduleItems] + ); + + const displayedSubtopics = useMemo( + () => (isSearching ? searchItems(subtopicItems, query) : []), + [isSearching, query, subtopicItems] + ); + + const displayedArchetypes = useMemo( + () => (isSearching ? searchItems(archetypeItems, query) : []), + [isSearching, query, archetypeItems] + ); + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); + + const handleSelect = (item: SearchResult) => { + setOpen(false); + if (item.onSelect) { + item.onSelect(); + } else if (item.href) { + router.push(item.href); + } + }; + + // Icon mapping + const IconMap: Record = { + House, + Books, + Code, + Lightning, + Clock, + ChartLine, + Cpu, + Gear, + Question, + BookOpen, + Hash, + GitBranch, + }; + + // Stats integration + const [weakestSubtopic, setWeakestSubtopic] = useState<{ + id: string; + name: string; + moduleId: string; + } | null>(null); + + useEffect(() => { + if (open && !isSearching) { + // Dynamic import to avoid SSR issues with localStorage + import("@/lib/statsStore").then(({ getWeakestSubtopics }) => { + const weakest = getWeakestSubtopics(1); + if (weakest.length > 0) { + setWeakestSubtopic({ + id: weakest[0].subtopic.subtopicId, + name: weakest[0].subtopic.subtopicName, + moduleId: weakest[0].moduleId, + }); + } + }); + } + }, [open, isSearching]); + + return ( + <> + {/* Removed trigger button since it's global shortcut, but could add a hidden one or use context */} + + + + No results found. + + {/* Initial State Groups */} + {!isSearching && ( + <> + + { + router.push("/practice/auto"); + setOpen(false); + }} + > + + Start Auto Mode + + + {weakestSubtopic && ( + { + router.push( + `/practice?mode=manual&module=${ + weakestSubtopic.moduleId + }&subtopic=${encodeURIComponent( + weakestSubtopic.name + )}&difficulty=beginner` + ); + setOpen(false); + }} + > + + Practice Weakest: {weakestSubtopic.name} + + )} + + { + router.push("/practice/manual"); + setOpen(false); + }} + > + + Open Manual Practice + + + + + + {/* Suggestions / Context (Merged into specific actions or hidden if redundant) */} + {/* Keeping context-aware items if they add value beyond the standard groups */} + {pathname === "/practice/auto" && ( + + { + router.push("/"); + setOpen(false); + }} + > + + Go Home + + + )} + + )} + + {/* Core Navigation */} + {displayedPages.length > 0 && ( + + {displayedPages.map((page) => { + const Icon = page.icon ? IconMap[page.icon] : House; + return ( + handleSelect(page)} + > + + {page.title} + + ); + })} + + )} + + + + {/* Modules */} + {displayedModules.length > 0 && ( + + {displayedModules.map((item) => ( + handleSelect(item)} + > + + {item.title} + + ))} + + )} + + {/* Subtopics - Search Only */} + {isSearching && displayedSubtopics.length > 0 && ( + <> + + + {displayedSubtopics.map((item) => ( + handleSelect(item)} + > + +
+ {item.title} + {item.subtitle && ( + + {item.subtitle} + + )} +
+
+ ))} +
+ + )} + + {/* Archetypes - Search Only */} + {isSearching && displayedArchetypes.length > 0 && ( + <> + + + {displayedArchetypes.map((item) => ( + handleSelect(item)} + > + +
+ {item.title} + {item.subtitle && ( + + {item.subtitle} + + )} +
+
+ ))} +
+ + )} + + {/* Settings & Toggles - Initial Only */} + {!isSearching && ( + <> + + + { + setTheme(theme === "dark" ? "light" : "dark"); + }} + > + {theme === "dark" ? ( + + ) : ( + + )} + Toggle Theme + + { + router.push("/support/settings?tab=api"); + setOpen(false); + }} + > + + API Key Settings + + { + router.push("/support/settings?tab=general"); + setOpen(false); + }} + > + + General Settings + + + + )} + + {/* Help - Initial Only */} + {!isSearching && ( + <> + + + { + router.push("/support/help"); + setOpen(false); + }} + > + + Search Documentation + + + + )} +
+
+ + ); +} diff --git a/src/components/automode/AutoModeControls.tsx b/src/components/automode/AutoModeControls.tsx new file mode 100644 index 0000000..ddd9b66 --- /dev/null +++ b/src/components/automode/AutoModeControls.tsx @@ -0,0 +1,123 @@ +"use client"; + +/** + * Auto Mode Controls + * + * In-run quick settings popover for Auto Mode: + * - Aggressive progression toggle + * - Remediation mode toggle + */ + +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Switch } from "@/components/ui/switch"; +import { Separator } from "@/components/ui/separator"; +import { type AutoRunV2 } from "@/lib/autoRunTypes"; +import { saveRun } from "@/lib/autoModeServiceV2"; +import { GearSix, Lightning, FirstAid } from "@phosphor-icons/react"; + +interface AutoModeControlsProps { + run: AutoRunV2; + onRunUpdate: (run: AutoRunV2) => void; +} + +export function AutoModeControls({ run, onRunUpdate }: AutoModeControlsProps) { + const handleAggressiveChange = (checked: boolean) => { + const updated: AutoRunV2 = { + ...run, + aggressiveProgression: checked, + lastUpdatedAt: Date.now(), + }; + saveRun(updated); + onRunUpdate(updated); + }; + + const handleRemediationChange = (checked: boolean) => { + const updated: AutoRunV2 = { + ...run, + remediationMode: checked, + lastUpdatedAt: Date.now(), + }; + saveRun(updated); + onRunUpdate(updated); + }; + + return ( + + + + + +
+
+

Auto Mode Settings

+

+ Adjust pacing for this run +

+
+ + + + {/* Aggressive Progression */} +
+
+ +
+ +

+ Promote after 2 correct (vs 3) +

+
+
+ +
+ + {/* Remediation Mode */} +
+
+ +
+ +

+ Add extra questions on mistakes +

+
+
+ +
+ + + +
+

+ Streak: {run.streak} correct in a row +

+

+ Completed: {run.completedQuestions} questions +

+
+
+
+
+ ); +} diff --git a/src/components/automode/AutoModeStatsBar.tsx b/src/components/automode/AutoModeStatsBar.tsx index 4d3223d..418590a 100644 --- a/src/components/automode/AutoModeStatsBar.tsx +++ b/src/components/automode/AutoModeStatsBar.tsx @@ -27,7 +27,7 @@ export function AutoModeStatsBar({ saveFile }: AutoModeStatsBarProps) { Topic: - {currentTopic} + {currentTopic.problemTypeName}
@@ -49,7 +49,7 @@ export function AutoModeStatsBar({ saveFile }: AutoModeStatsBarProps) {
Next: - {nextTopic} + {nextTopic.problemTypeName}
{/* Total Completed */} diff --git a/src/components/automode/AutoModeStatsBarV2.tsx b/src/components/automode/AutoModeStatsBarV2.tsx new file mode 100644 index 0000000..d64ec71 --- /dev/null +++ b/src/components/automode/AutoModeStatsBarV2.tsx @@ -0,0 +1,244 @@ +"use client"; + +/** + * Auto Mode Stats Bar v2 + * + * Enhanced stats bar showing: + * - Module → Subtopic breadcrumb + * - Streak indicator with animation + * - Difficulty badge per subtopic + * - Progress in run + * - Quick controls (Slow Down, Settings) + */ + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Progress } from "@/components/ui/progress"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { type AutoRunV2, type DifficultyLevel } from "@/lib/autoRunTypes"; +import { + getCurrentQueueEntry, + getSubtopicDifficulty, + slowDown, +} from "@/lib/autoModeServiceV2"; +import { + CaretRight, + Fire, + Lightning, + Brain, + Rocket, + Pause, + GearSix, +} from "@phosphor-icons/react"; +import { cn } from "@/lib/utils"; +import { useState, useEffect } from "react"; + +interface AutoModeStatsBarV2Props { + run: AutoRunV2; + onRunUpdate: (run: AutoRunV2) => void; + onOpenSettings?: () => void; +} + +/** + * Get icon for difficulty level. + */ +function DifficultyIcon({ + difficulty, + className, +}: { + difficulty: DifficultyLevel; + className?: string; +}) { + switch (difficulty) { + case "beginner": + return ( + + ); + case "intermediate": + return ; + case "advanced": + return ; + } +} + +/** + * Get difficulty badge variant. + */ +function getDifficultyVariant( + difficulty: DifficultyLevel +): "default" | "secondary" | "destructive" { + switch (difficulty) { + case "beginner": + return "secondary"; + case "intermediate": + return "default"; + case "advanced": + return "destructive"; + } +} + +export function AutoModeStatsBarV2({ + run, + onRunUpdate, + onOpenSettings, +}: AutoModeStatsBarV2Props) { + const entry = getCurrentQueueEntry(run); + const [isAnimatingStreak, setIsAnimatingStreak] = useState(false); + const [prevStreak, setPrevStreak] = useState(run.streak); + + // Animate streak changes + useEffect(() => { + if (run.streak !== prevStreak) { + setIsAnimatingStreak(true); + setPrevStreak(run.streak); + const timer = setTimeout(() => setIsAnimatingStreak(false), 500); + return () => clearTimeout(timer); + } + }, [run.streak, prevStreak]); + + if (!entry) return null; + + const difficulty = getSubtopicDifficulty(run, entry.subtopicId); + const progress = run.miniCurriculumComplete + ? { + current: run.completedQuestions, + total: run.completedQuestions + 10, + label: "Ongoing", + } + : { + current: run.currentIndex + 1, + total: run.topicQueue.length, + label: "Mini-Curriculum", + }; + + const handleSlowDown = () => { + const updated = slowDown(run); + onRunUpdate(updated); + }; + + return ( +
+
+ {/* Left: Breadcrumb */} +
+ + {entry.moduleName} + + + + {entry.subtopicName} + +
+ + {/* Center: Streak + Difficulty + Progress */} +
+ {/* Streak */} + + +
0 + ? "bg-orange-500/10 text-orange-500" + : "bg-muted text-muted-foreground", + isAnimatingStreak && "animate-pulse" + )} + > + 0 ? "fill" : "regular"} + className={cn("h-4 w-4", run.streak >= 3 && "animate-bounce")} + /> + {run.streak} +
+
+ +

+ {run.streak > 0 + ? `${run.streak} correct in a row!` + : "Get questions right to build a streak"} +

+
+
+ + {/* Difficulty Badge */} + + + + + {difficulty} + + + +

Current difficulty for {entry.subtopicName}

+
+
+ + {/* Progress */} +
+ + {progress.label}: + +
+ + + {progress.current}/{progress.total} + +
+
+
+ + {/* Right: Controls */} +
+ {/* Aggressive Mode Indicator */} + {run.aggressiveProgression && ( + + Fast Mode + + )} + + {/* Slow Down Button */} + + + + + +

Reset streak and add easier questions

+
+
+ + {/* Settings */} + {onOpenSettings && ( + + )} +
+
+ + {/* Promotion/Demotion Toast Area (handled by parent) */} +
+ ); +} diff --git a/src/components/dashboard/DashboardModuleSheet.tsx b/src/components/dashboard/DashboardModuleSheet.tsx new file mode 100644 index 0000000..625ad11 --- /dev/null +++ b/src/components/dashboard/DashboardModuleSheet.tsx @@ -0,0 +1,266 @@ +"use client"; + +import { useMemo } from "react"; +import { useRouter } from "next/navigation"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription, +} from "@/components/ui/sheet"; +import { Progress } from "@/components/ui/progress"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { + Play, + CheckCircle, + Lightning, + Brain, + Rocket, + CaretRight, +} from "@phosphor-icons/react"; +import { cn } from "@/lib/utils"; +import type { Module, Subtopic, ProblemType } from "@/lib/topicsStore"; +import type { ModuleStats, SubtopicStats } from "@/lib/statsStore"; +import { pickWeakestSubtopic } from "@/lib/statsStore"; + +interface DashboardModuleSheetProps { + module: Module | null; + moduleStats: ModuleStats | null; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +function getMasteryColor(percent: number) { + if (percent >= 80) return "bg-green-500"; + if (percent >= 40) return "bg-yellow-500"; + return "bg-red-500"; // Changed to red for low mastery to matching existing logic +} + +export function DashboardModuleSheet({ + module, + moduleStats, + open, + onOpenChange, +}: DashboardModuleSheetProps) { + const router = useRouter(); + + const handlePracticeSubtopic = (subtopicId: string) => { + if (!module) return; + const subtopicDef = module.subtopics.find((s) => s.id === subtopicId); + if (!subtopicDef) return; + + router.push( + `/practice?mode=manual&topic=${encodeURIComponent( + subtopicDef.name + )}&difficulty=beginner` + ); + onOpenChange(false); + }; + + const handlePracticeModule = () => { + if (!module) return; + const weakest = pickWeakestSubtopic(module.id); + const targetSubtopic = weakest?.subtopicId || module.subtopics[0]?.id; + + if (targetSubtopic) { + handlePracticeSubtopic(targetSubtopic); + } + }; + + if (!module) return null; + + // Merge definition with stats + const subtopicRows = useMemo(() => { + return module.subtopics.map((sub) => { + const stats = moduleStats?.subtopics.find((s) => s.subtopicId === sub.id); + return { + def: sub, + stats, + }; + }); + }, [module, moduleStats]); + + return ( + + + {/* Header */} + +
+ + Module {module.order} + + {moduleStats && ( + = 80 + ? "bg-green-100 text-green-800" + : moduleStats.masteryPercent >= 40 + ? "bg-yellow-100 text-yellow-800" + : "bg-red-100 text-red-800" + )} + > + {moduleStats.masteryPercent}% Mastery + + )} +
+ {module.name} + {module.overview} + + {moduleStats && ( +
+
+ Progress + + {moduleStats.solved} / {moduleStats.attempts} Solved + +
+ +
+ )} +
+ + {/* Content */} + +
+
+

+ Subtopics +

+ + + {subtopicRows.map(({ def, stats }) => ( + + +
+
+
{def.name}
+
+ {def.problemTypes.length} Types + {stats?.attempts ? ( + • {stats.attempts} Attempts + ) : null} +
+
+ {stats && ( +
+
= 80 + ? "text-green-600" + : stats.masteryPercent >= 40 + ? "text-yellow-600" + : "text-muted-foreground" + )} + > + {stats.masteryPercent}% +
+
+ )} +
+
+ +
+
+ + +
+ + {/* Breakdown of problem types */} +
+ {def.problemTypes.map((pt) => { + const ptStats = stats?.problemTypes.find( + (p) => p.problemTypeId === pt.id + ); + return ( +
+ + {pt.name} + + {ptStats ? ( +
+ + {" "} + {ptStats.beginner.attempts} + + + {" "} + {ptStats.intermediate.attempts} + + + {" "} + {ptStats.advanced.attempts} + +
+ ) : ( + + Untouched + + )} +
+ ); + })} +
+
+
+
+ ))} +
+
+
+
+ + {/* Footer */} +
+ +
+
+
+ ); +} diff --git a/src/components/dashboard/ModuleStatCard.tsx b/src/components/dashboard/ModuleStatCard.tsx new file mode 100644 index 0000000..b85ba66 --- /dev/null +++ b/src/components/dashboard/ModuleStatCard.tsx @@ -0,0 +1,229 @@ +"use client"; + +/** + * Module Stat Card + * + * Displays a module with its mastery percentage and expandable subtopic stats. + */ + +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { Button } from "@/components/ui/button"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + CaretDown, + Lightning, + Brain, + Rocket, + Target, + CheckCircle, + XCircle, +} from "@phosphor-icons/react"; +import type { ModuleStats } from "@/lib/statsStore"; +import { cn } from "@/lib/utils"; +import { SubtopicTile } from "./SubtopicTile"; + +interface ModuleStatCardProps { + moduleStats: ModuleStats; + moduleName?: string; + moduleIcon?: React.ReactNode; + onPractice?: (moduleId: string) => void; +} + +/** + * Get mastery badge variant. + */ +function getMasteryBadgeVariant( + mastery: number +): "default" | "secondary" | "destructive" | "outline" { + if (mastery >= 80) return "default"; + if (mastery >= 40) return "secondary"; + if (mastery > 0) return "destructive"; + return "outline"; +} + +export function ModuleStatCard({ + moduleStats, + moduleName, + moduleIcon, + onPractice, +}: ModuleStatCardProps) { + const [isOpen, setIsOpen] = useState(false); + const displayName = moduleName || moduleStats.moduleName; + const hasStats = moduleStats.attempts > 0; + + // Difficulty distribution + const diffCounts = { beginner: 0, intermediate: 0, advanced: 0 }; + for (const subtopic of moduleStats.subtopics) { + for (const pt of subtopic.problemTypes) { + diffCounts.beginner += pt.beginner.attempts; + diffCounts.intermediate += pt.intermediate.attempts; + diffCounts.advanced += pt.advanced.attempts; + } + } + const totalDiffAttempts = + diffCounts.beginner + diffCounts.intermediate + diffCounts.advanced; + + return ( + = 80 && "border-l-green-500", + hasStats && + moduleStats.masteryPercent >= 40 && + moduleStats.masteryPercent < 80 && + "border-l-yellow-500", + hasStats && moduleStats.masteryPercent < 40 && "border-l-red-500" + )} + > + + +
+
+ {moduleIcon && ( +
{moduleIcon}
+ )} +
+ {displayName} +

+ {moduleStats.subtopics.length} subtopics +

+
+
+
+ {/* Mastery Badge */} + {hasStats ? ( + + {moduleStats.masteryPercent}% Mastery + + ) : ( + + Not started + + )} + + {/* Expand Button */} + + + +
+
+
+ + + {/* Stats Row */} +
+
+ + {moduleStats.attempts} attempts +
+
+ + {moduleStats.solved} solved +
+ {moduleStats.attempts > 0 && + moduleStats.attempts !== moduleStats.solved && ( +
+ + + {moduleStats.attempts - moduleStats.solved} missed + +
+ )} +
+ + {/* Progress Bar */} + {hasStats && ( +
+ +
+ )} + + {/* Difficulty Distribution */} + {totalDiffAttempts > 0 && ( +
+
+ + {diffCounts.beginner} +
+
+ + {diffCounts.intermediate} +
+
+ + {diffCounts.advanced} +
+
+ )} +
+ + {/* Expanded Subtopics */} + + +
+

Subtopics

+ {moduleStats.subtopics.length > 0 ? ( +
+ {moduleStats.subtopics.map((subtopic) => ( + + ))} +
+ ) : ( +

+ No subtopic activity yet +

+ )} +
+ + {/* Practice Button */} + {onPractice && ( +
+ +
+ )} +
+
+
+
+ ); +} diff --git a/src/components/dashboard/ModuleStatsGrid.tsx b/src/components/dashboard/ModuleStatsGrid.tsx new file mode 100644 index 0000000..5f428a0 --- /dev/null +++ b/src/components/dashboard/ModuleStatsGrid.tsx @@ -0,0 +1,225 @@ +"use client"; + +/** + * Module Stats Grid + * + * Displays all modules with their hierarchical stats. + * Uses the new statsStore v2 structure. + */ + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { ModuleStatCard } from "./ModuleStatCard"; +import { Card, CardContent } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Badge } from "@/components/ui/badge"; +import { + Code, + ListBullets, + TreeStructure, + ArrowsClockwise, + MagnifyingGlass, + Function as FunctionIcon, + Stack, + Tree, + Sparkle, +} from "@phosphor-icons/react"; +import { + getStatsV2, + type ModuleStats, + type GlobalStatsV2, +} from "@/lib/statsStore"; +import { getAllModules } from "@/lib/topicsStore"; + +// Module ID to icon mapping +const moduleIcons: Record = { + "string-manipulation": , + "lists-and-arrays": , + "dictionaries-and-hashmaps": ( + + ), + "loops-and-iteration": ( + + ), + "searching-and-sorting": ( + + ), + recursion: , + "stacks-and-queues": , + "trees-and-graphs": , +}; + +function getModuleIcon(moduleId: string): React.ReactNode { + return ( + moduleIcons[moduleId] || + ); +} + +interface ModuleStatsGridProps { + showEmptyModules?: boolean; +} + +export function ModuleStatsGrid({ + showEmptyModules = true, +}: ModuleStatsGridProps) { + const router = useRouter(); + const [stats, setStats] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const loadStats = () => { + const loaded = getStatsV2(); + setStats(loaded); + setIsLoading(false); + }; + + loadStats(); + }, []); + + const handlePractice = (moduleId: string) => { + router.push(`/practice/manual?module=${moduleId}`); + }; + + if (isLoading) { + return ( +
+ + + +
+ ); + } + + if (!stats) { + return ( + + + +

No stats available

+
+
+ ); + } + + // Get all modules from topicsStore for complete list + const allModules = getAllModules(); + + // Create a map of existing stats + const statsMap = new Map(stats.modules.map((m) => [m.moduleId, m])); + + // Build display list: either only touched modules or all + const displayModules: Array<{ + moduleId: string; + moduleName: string; + stats: ModuleStats | null; + }> = []; + + if (showEmptyModules) { + // Show all modules, with stats if available + for (const mod of allModules) { + displayModules.push({ + moduleId: mod.id, + moduleName: mod.name, + stats: statsMap.get(mod.id) || null, + }); + } + } else { + // Only show modules with stats + for (const modStats of stats.modules) { + if (modStats.attempts > 0) { + displayModules.push({ + moduleId: modStats.moduleId, + moduleName: modStats.moduleName, + stats: modStats, + }); + } + } + } + + // Sort: modules with stats first, then by mastery + displayModules.sort((a, b) => { + if (a.stats && !b.stats) return -1; + if (!a.stats && b.stats) return 1; + if (a.stats && b.stats) { + return b.stats.masteryPercent - a.stats.masteryPercent; + } + return 0; + }); + + if (displayModules.length === 0) { + return ( + + + +

+ Start practicing to see your progress +

+
+
+ ); + } + + return ( +
+ {/* Summary Header */} +
+
+ Modules: + {stats.modulesTouched} touched +
+
+ Subtopics: + {stats.subtopicsTouched} practiced +
+
+ Overall: + = 60 ? "default" : "secondary"}> + {stats.masteryPercent}% mastery + +
+
+ + {/* Module Cards */} +
+ {displayModules.map(({ moduleId, moduleName, stats: modStats }) => + modStats ? ( + + ) : ( + handlePractice(moduleId)} + > + +
+ {getModuleIcon(moduleId)} +
+
+

{moduleName}

+

+ Not started yet +

+
+ + Start + +
+
+ ) + )} +
+
+ ); +} diff --git a/src/components/dashboard/NextStepsPanel.tsx b/src/components/dashboard/NextStepsPanel.tsx new file mode 100644 index 0000000..8150f07 --- /dev/null +++ b/src/components/dashboard/NextStepsPanel.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Lightning, TrendUp, ArrowRight, Robot } from "@phosphor-icons/react"; +import Link from "next/link"; +import { getWeakestSubtopics, type SubtopicStats } from "@/lib/statsStore"; + +export function NextStepsPanel() { + const [weakest, setWeakest] = useState<{ + moduleId: string; + subtopic: SubtopicStats; + } | null>(null); + + useEffect(() => { + // Load weakest subtopic + const weak = getWeakestSubtopics(1); + if (weak.length > 0) { + setTimeout(() => setWeakest(weak[0]), 0); + } + }, []); + + return ( +
+ {/* 1. Auto Mode */} + + + + + Auto Practice + + + +

+ Rapid-fire questions with instant feedback. Great for warmup. +

+ +
+
+ + {/* 2. Weakest Link */} + + + + + Improve Weaker Areas + + + + {weakest ? ( + <> +

+ Your mastery in{" "} + + {weakest.subtopic.subtopicName} + {" "} + is low. +

+ + + ) : ( + <> +

+ No weak areas detected yet! Try exploring new topics. +

+ + + )} +
+
+ + {/* 3. Manual Config */} + + + + + Custom Session + + + +

+ Configure difficulty, topic, and constraints manually. +

+ +
+
+
+ ); +} diff --git a/src/components/dashboard/RecentActivityRow.tsx b/src/components/dashboard/RecentActivityRow.tsx new file mode 100644 index 0000000..65b3f8f --- /dev/null +++ b/src/components/dashboard/RecentActivityRow.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { getHistory, type QuestionHistoryEntry } from "@/lib/historyStore"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { CheckCircle, XCircle } from "@phosphor-icons/react"; +import Link from "next/link"; +import { formatDistanceToNow } from "date-fns"; + +export function RecentActivityRow() { + const [history, setHistory] = useState([]); + + useEffect(() => { + setTimeout(() => setHistory(getHistory().slice(0, 10)), 0); // Top 10 recent + }, []); + + if (history.length === 0) { + return ( +
+ No recent activity. Start practicing to see your history here! +
+ ); + } + + return ( + +
+ {history.map((entry) => ( + + + +
+ + {entry.topic} + + {entry.wasCorrect ? ( + + ) : ( + + )} +
+

+ {entry.questionTitle} +

+

+ {formatDistanceToNow(entry.executedAt, { addSuffix: true })} +

+
+
+ + ))} +
+ +
+ ); +} diff --git a/src/components/dashboard/SubtopicTile.tsx b/src/components/dashboard/SubtopicTile.tsx new file mode 100644 index 0000000..e30bfe8 --- /dev/null +++ b/src/components/dashboard/SubtopicTile.tsx @@ -0,0 +1,152 @@ +"use client"; + +/** + * Subtopic Tile + * + * Compact tile showing subtopic stats with mastery and difficulty breakdown. + */ + +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Lightning, Brain, Rocket, CheckCircle } from "@phosphor-icons/react"; +import type { SubtopicStats } from "@/lib/statsStore"; +import { cn } from "@/lib/utils"; + +interface SubtopicTileProps { + subtopicStats: SubtopicStats; + onClick?: () => void; +} + +/** + * Get background based on mastery. + */ +function getMasteryBg(mastery: number): string { + if (mastery >= 80) return "bg-green-500/10 border-green-500/20"; + if (mastery >= 60) return "bg-yellow-500/10 border-yellow-500/20"; + if (mastery >= 40) return "bg-orange-500/10 border-orange-500/20"; + if (mastery > 0) return "bg-red-500/10 border-red-500/20"; + return "bg-muted/50 border-muted"; +} + +export function SubtopicTile({ subtopicStats, onClick }: SubtopicTileProps) { + const hasStats = subtopicStats.attempts > 0; + + // Aggregate difficulty counts from problem types + const diffCounts = { beginner: 0, intermediate: 0, advanced: 0 }; + for (const pt of subtopicStats.problemTypes) { + diffCounts.beginner += pt.beginner.attempts; + diffCounts.intermediate += pt.intermediate.attempts; + diffCounts.advanced += pt.advanced.attempts; + } + const totalDiff = + diffCounts.beginner + diffCounts.intermediate + diffCounts.advanced; + + return ( +
+
+
+

+ {subtopicStats.subtopicName} +

+
+ {subtopicStats.attempts} attempts + {hasStats && ( + + + {subtopicStats.solved} + + )} +
+
+ + {/* Mastery Badge */} + {hasStats ? ( + + + = 60 ? "default" : "secondary" + } + className="text-xs shrink-0" + > + {subtopicStats.masteryPercent}% + + + +

Mastery: {subtopicStats.masteryPercent}%

+
+
+ ) : ( + + New + + )} +
+ + {/* Progress Bar */} + {hasStats && ( + + )} + + {/* Difficulty Distribution (mini) */} + {totalDiff > 0 && ( +
+ {diffCounts.beginner > 0 && ( + + +
+ + {diffCounts.beginner} +
+
+ +

{diffCounts.beginner} beginner attempts

+
+
+ )} + {diffCounts.intermediate > 0 && ( + + +
+ + {diffCounts.intermediate} +
+
+ +

{diffCounts.intermediate} intermediate attempts

+
+
+ )} + {diffCounts.advanced > 0 && ( + + +
+ + {diffCounts.advanced} +
+
+ +

{diffCounts.advanced} advanced attempts

+
+
+ )} +
+ )} +
+ ); +} diff --git a/src/components/layout/DashboardLayout.tsx b/src/components/layout/DashboardLayout.tsx new file mode 100644 index 0000000..3ee85e5 --- /dev/null +++ b/src/components/layout/DashboardLayout.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +interface DashboardLayoutProps { + children: React.ReactNode; +} + +export function DashboardLayout({ children }: DashboardLayoutProps) { + return ( +
+
+
Dashboard
+
+
+ {children} +
+
+ ); +} diff --git a/src/components/layout/ModulesLayout.tsx b/src/components/layout/ModulesLayout.tsx new file mode 100644 index 0000000..2498447 --- /dev/null +++ b/src/components/layout/ModulesLayout.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +interface ModulesLayoutProps { + children: React.ReactNode; +} + +export function ModulesLayout({ children }: ModulesLayoutProps) { + return ( +
+
+
Curriculum
+
+
{children}
+
+ ); +} diff --git a/src/components/layout/PracticeLayout.tsx b/src/components/layout/PracticeLayout.tsx new file mode 100644 index 0000000..8ff0924 --- /dev/null +++ b/src/components/layout/PracticeLayout.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +interface PracticeLayoutProps { + children: React.ReactNode; +} + +export function PracticeLayout({ children }: PracticeLayoutProps) { + return ( +
+
+
Practice Configurator
+
+
+ {children} +
+
+ ); +} diff --git a/src/components/modules/CurriculumExplorer.tsx b/src/components/modules/CurriculumExplorer.tsx new file mode 100644 index 0000000..df70043 --- /dev/null +++ b/src/components/modules/CurriculumExplorer.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useState } from "react"; +import { getAllModules } from "@/lib/topicsStore"; +import { ModuleSidebar } from "./ModuleSidebar"; +import { ModuleDetailView } from "./ModuleDetailView"; + +export function CurriculumExplorer() { + const modules = getAllModules(); + // Default to first module + const [selectedModuleId, setSelectedModuleId] = useState( + modules[0]?.id || "" + ); + const [searchQuery, setSearchQuery] = useState(""); + + const filteredModules = modules.filter((m) => { + const query = searchQuery.toLowerCase(); + return ( + m.name.toLowerCase().includes(query) || + m.subtopics.some( + (s) => + s.name.toLowerCase().includes(query) || + s.problemTypes.some((pt) => pt.name.toLowerCase().includes(query)) + ) + ); + }); + + const selectedModule = modules.find((m) => m.id === selectedModuleId); + + return ( +
+ {/* Sidebar - Settings-style List */} + + + {/* Mobile Drawer */} +
+ +
+ + {/* Main Content Area */} +
+ {selectedModule ? ( + + ) : ( +
+
+

+ No Module Selected +

+

+ Select a module from the sidebar to view its curriculum and + start practicing. +

+
+
+ )} +
+
+ ); +} diff --git a/src/components/modules/ModuleCard.tsx b/src/components/modules/ModuleCard.tsx new file mode 100644 index 0000000..4cd1180 --- /dev/null +++ b/src/components/modules/ModuleCard.tsx @@ -0,0 +1,237 @@ +"use client"; + +import { useState } from "react"; +import { Module } from "@/lib/topicsStore"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { SubtopicAccordion } from "./SubtopicAccordion"; +import { + CaretDown, + CaretUp, + Stack, + TreeStructure, + Code, + ListBullets, + Database, + Tree, + Graph, + ArrowsCounterClockwise, + Calculator, + Binary, + SortAscending, + Sliders, + FileText, + Gear, + Rocket, + CirclesThree, + StackSimple, + Queue, + GitBranch, +} from "@phosphor-icons/react"; + +// Map module IDs to appropriate icons +const moduleIcons: Record = { + "string-manipulation": , + "arrays-and-lists-python-lists": ( + + ), + "hash-maps-dictionaries": , + sets: , + "linked-lists": , + "stacks-and-queues": , + "heaps-priority-queues": , + trees: , + graphs: , + "recursion-and-backtracking": ( + + ), + "dynamic-programming-dp": ( + + ), + "greedy-algorithms": , + "binary-search": , + "math-and-number-theory": , + "bit-manipulation": , + "sorting-and-searching-algorithms": ( + + ), + "two-pointers-and-sliding-window-advanced": ( + + ), + "file-handling-python-specific": ( + + ), + "system-design-patterns-coding-interview-context": ( + + ), + "advanced-topics-and-specialized-algorithms": ( + + ), +}; + +interface ModuleCardProps { + module: Module; +} + +/** + * Card component for a single module with expandable subtopics. + */ +export function ModuleCard({ module }: ModuleCardProps) { + const [isExpanded, setIsExpanded] = useState(false); + + const icon = moduleIcons[module.id] || ( + + ); + + const totalProblemTypes = module.subtopics.reduce( + (acc, st) => acc + st.problemTypes.length, + 0 + ); + + return ( + + +
+
+
{icon}
+
+ + {module.name} + + #{module.order} + + + {module.overview && ( + + {module.overview} + + )} +
+
+
+
+ + + {/* Stats Row */} +
+
+ + + {module.subtopics.length} subtopic + {module.subtopics.length !== 1 ? "s" : ""} + +
+
+ + + {totalProblemTypes} problem type + {totalProblemTypes !== 1 ? "s" : ""} + +
+
+ + {/* Problem Archetypes Preview */} + {!isExpanded && module.problemArchetypes.length > 0 && ( +
+ {module.problemArchetypes.slice(0, 3).map((archetype, i) => ( + + {archetype} + + ))} + {module.problemArchetypes.length > 3 && ( + + +{module.problemArchetypes.length - 3} more + + )} +
+ )} + + {/* Expand/Collapse Button */} + + + {/* Expanded Content */} + {isExpanded && ( +
+ + + {/* Python Considerations */} + {module.pythonConsiderations && + module.pythonConsiderations.length > 0 && ( +
+

+ + Python Tips +

+
    + {module.pythonConsiderations + .filter((tip) => tip !== "--") + .slice(0, 5) + .map((tip, i) => ( +
  • + {tip} +
  • + ))} +
+
+ )} + + {/* All Archetypes when expanded */} + {module.problemArchetypes.length > 0 && ( +
+

Problem Archetypes

+
+ {module.problemArchetypes.map((archetype, i) => ( + + {archetype} + + ))} +
+
+ )} +
+ )} +
+
+ ); +} diff --git a/src/components/modules/ModuleDetailView.tsx b/src/components/modules/ModuleDetailView.tsx new file mode 100644 index 0000000..a7e315c --- /dev/null +++ b/src/components/modules/ModuleDetailView.tsx @@ -0,0 +1,272 @@ +"use client"; + +import Link from "next/link"; +import { + BookOpen, + Tag as TagIcon, + Play, + CaretDown, + Lightning, +} from "@phosphor-icons/react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Progress } from "@/components/ui/progress"; +import { Accordion, AccordionContent } from "@/components/ui/accordion"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import type { Module, Subtopic } from "@/types/topics"; +import { getModuleStats, getSubtopicStats } from "@/lib/statsStore"; + +interface ModuleDetailViewProps { + module: Module; + searchQuery?: string; +} + +export function ModuleDetailView({ + module, + searchQuery = "", +}: ModuleDetailViewProps) { + const moduleStats = getModuleStats(module.id); + const query = searchQuery.toLowerCase(); + + // Filter subtopics based on search query + const filteredSubtopics = module.subtopics.filter((subtopic) => { + if (!query) return true; + + // If module name matches, show all + if (module.name.toLowerCase().includes(query)) return true; + + // If subtopic name matches, show + if (subtopic.name.toLowerCase().includes(query)) return true; + + // If any problem type matches, show + return subtopic.problemTypes.some((pt) => + pt.name.toLowerCase().includes(query) + ); + }); + + // Calculate default expanded items based on search + const defaultValue = + query && !module.name.toLowerCase().includes(query) + ? filteredSubtopics.map((s) => s.id) + : []; + + return ( +
+ {/* Module Header Area */} +
+
+
+
+
+
+ + Module {module.order} + + + {module.subtopics.length}{" "} + subtopics + +
+

+ {module.name} +

+ {module.overview && ( +

+ {module.overview} +

+ )} +
+ + {/* Stats Bar */} + {moduleStats && moduleStats.attempts > 0 && ( +
+
+
+ Progress + {moduleStats.masteryPercent}% +
+ +
+
+ {moduleStats.solved} / {moduleStats.attempts} Solved +
+
+ )} +
+ + {/* Quick Actions */} +
+ +
+
+
+
+ + {/* Main Content: Hierarchy */} + +
+
+

+ + Curriculum & Topics +

+ + {filteredSubtopics.length > 0 ? ( + + {filteredSubtopics.map((subtopic) => ( + + ))} + + ) : ( +
+ No topics found matching “{searchQuery}” in this + module. +
+ )} +
+
+
+
+ ); +} +function SubtopicItem({ + module, + subtopic, + searchQuery = "", +}: { + module: Module; + subtopic: Subtopic; + searchQuery?: string; +}) { + const stats = getSubtopicStats(module.id, subtopic.id); + const mastery = stats?.masteryPercent || 0; + const query = searchQuery.toLowerCase(); + + // Filter types logic... + const showAllTypes = + !query || + module.name.toLowerCase().includes(query) || + subtopic.name.toLowerCase().includes(query); + + const displayedTypes = showAllTypes + ? subtopic.problemTypes + : subtopic.problemTypes.filter((pt) => + pt.name.toLowerCase().includes(query) + ); + + return ( + +
+ {/* Main Trigger Area - Click anywhere to expand */} + + +
+
+

+ {subtopic.name} +

+ {mastery > 0 && ( + 70 ? "default" : "secondary"} + className="text-[10px] h-5 px-1.5" + > + {mastery}% + + )} +
+
+ {subtopic.problemTypes.length} Archetypes +
+
+ {/* Chevron is typically here, let's keep it or move it? + Standard Accordion puts it at right. We can put it here. + */} +
+
+ + {/* Separate Actions - Not part of trigger */} +
+ +
+
+ + +
+ {displayedTypes.map((pt) => ( + +
+
+ + + {pt.name} + +
+ {pt.description && ( +

+ {pt.description} +

+ )} +
+ + + ))} +
+
+
+ ); +} diff --git a/src/components/modules/ModuleSidebar.tsx b/src/components/modules/ModuleSidebar.tsx new file mode 100644 index 0000000..741fbe1 --- /dev/null +++ b/src/components/modules/ModuleSidebar.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { MagnifyingGlass, CaretRight } from "@phosphor-icons/react"; +import { cn } from "@/lib/utils"; +import type { Module } from "@/types/topics"; +import { getModuleStats } from "@/lib/statsStore"; + +interface ModuleSidebarProps { + modules: Module[]; + selectedModuleId: string; + onSelectModule: (id: string) => void; + searchQuery: string; + onSearchChange: (query: string) => void; + className?: string; +} + +export function ModuleSidebar({ + modules, + selectedModuleId, + onSelectModule, + searchQuery, + onSearchChange, + className, +}: ModuleSidebarProps) { + return ( +
+
+
+ + onSearchChange(e.target.value)} + /> +
+
+ {modules.length} Modules Found +
+
+ + + + +
+ ); +} diff --git a/src/components/modules/ModulesGrid.tsx b/src/components/modules/ModulesGrid.tsx new file mode 100644 index 0000000..e1e2a48 --- /dev/null +++ b/src/components/modules/ModulesGrid.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { getAllModules, getTopicsStats, Module } from "@/lib/topicsStore"; +import { ModuleCard } from "./ModuleCard"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardHeader, CardContent } from "@/components/ui/card"; +import { MagnifyingGlass, Stack, TreeStructure } from "@phosphor-icons/react"; + +/** + * Grid container that displays all modules from topicsStore. + * Includes search filtering and responsive layout. + */ +export function ModulesGrid() { + const [searchQuery, setSearchQuery] = useState(""); + + // Get data from store + const modules = getAllModules(); + const stats = getTopicsStats(); + + // Filter modules based on search + const filteredModules = useMemo(() => { + if (!searchQuery.trim()) { + return modules; + } + + const query = searchQuery.toLowerCase(); + return modules.filter((module: Module) => { + // Search in module name + if (module.name.toLowerCase().includes(query)) return true; + + // Search in subtopic names + if ( + module.subtopics.some((st) => st.name.toLowerCase().includes(query)) + ) { + return true; + } + + // Search in problem archetypes + if ( + module.problemArchetypes.some((a) => a.toLowerCase().includes(query)) + ) { + return true; + } + + // Search in problem type names + if ( + module.subtopics.some((st) => + st.problemTypes.some((pt) => pt.name.toLowerCase().includes(query)) + ) + ) { + return true; + } + + return false; + }); + }, [modules, searchQuery]); + + return ( +
+ {/* Stats Summary */} +
+
+ + + {stats.moduleCount}{" "} + modules + +
+
+ + + {stats.subtopicCount}{" "} + subtopics + +
+
+ + + {stats.problemTypeCount} + {" "} + problem types + +
+ + v{stats.version} + +
+ + {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+ + {/* Results Count */} + {searchQuery && ( +

+ Found{" "} + {filteredModules.length}{" "} + module{filteredModules.length !== 1 ? "s" : ""} matching " + {searchQuery}" +

+ )} + + {/* Modules Grid */} + {filteredModules.length === 0 ? ( +
+

+ No modules found matching your search. +

+
+ ) : ( +
+ {filteredModules.map((module) => ( + + ))} +
+ )} +
+ ); +} + +/** + * Loading skeleton for the modules grid. + */ +export function ModulesGridSkeleton() { + return ( +
+ {/* Stats skeleton */} +
+ + + +
+ + {/* Search skeleton */} + + + {/* Grid skeleton */} +
+ {[...Array(6)].map((_, i) => ( + + +
+ +
+ + +
+
+
+ +
+ + +
+
+ + + +
+ +
+
+ ))} +
+
+ ); +} diff --git a/src/components/modules/SubtopicAccordion.tsx b/src/components/modules/SubtopicAccordion.tsx new file mode 100644 index 0000000..e537864 --- /dev/null +++ b/src/components/modules/SubtopicAccordion.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { Subtopic, ProblemType } from "@/lib/topicsStore"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { CircleDashed } from "@phosphor-icons/react"; + +interface SubtopicAccordionProps { + subtopics: Subtopic[]; +} + +/** + * Accordion component that displays subtopics and their problem types. + * Used within ModuleCard to show the full hierarchy. + */ +export function SubtopicAccordion({ subtopics }: SubtopicAccordionProps) { + if (subtopics.length === 0) { + return ( +

+ No subtopics available. +

+ ); + } + + return ( + + + {subtopics.map((subtopic) => ( + + +
+ {subtopic.sectionNumber && ( + + {subtopic.sectionNumber} + + )} + {subtopic.name} + + {subtopic.problemTypes.length} + +
+
+ +
+ {subtopic.problemTypes.length === 0 ? ( +

+ No problem types defined. +

+ ) : ( + subtopic.problemTypes.map((pt) => ( + + )) + )} +
+
+
+ ))} +
+
+ ); +} + +interface ProblemTypeItemProps { + problemType: ProblemType; +} + +/** + * Single problem type item with optional tooltip for description. + */ +function ProblemTypeItem({ problemType }: ProblemTypeItemProps) { + const content = ( +
+ +
+

{problemType.name}

+ {problemType.description && ( +

+ {problemType.description} +

+ )} +
+
+ ); + + // If there's a long description, show full content in tooltip + if (problemType.description && problemType.description.length > 50) { + return ( + + {content} + +

{problemType.name}

+

+ {problemType.description} +

+
+
+ ); + } + + return content; +} diff --git a/src/components/modules/index.ts b/src/components/modules/index.ts new file mode 100644 index 0000000..6cc110e --- /dev/null +++ b/src/components/modules/index.ts @@ -0,0 +1,3 @@ +export { ModuleCard } from "./ModuleCard"; +export { ModulesGrid, ModulesGridSkeleton } from "./ModulesGrid"; +export { SubtopicAccordion } from "./SubtopicAccordion"; diff --git a/src/components/practice/ModuleSheet.tsx b/src/components/practice/ModuleSheet.tsx new file mode 100644 index 0000000..97e5f21 --- /dev/null +++ b/src/components/practice/ModuleSheet.tsx @@ -0,0 +1,260 @@ +"use client"; + +/** + * Module Sheet + * + * Right-side drawer showing module details: + * - Module header with stats + * - Accordion of subtopics + * - Problem types with Generate buttons + */ + +import { useCallback, useMemo } from "react"; +import { useRouter } from "next/navigation"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription, +} from "@/components/ui/sheet"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Play, Lightning, Brain, Rocket, Code } from "@phosphor-icons/react"; +import type { Module, Subtopic, ProblemType } from "@/lib/topicsStore"; +import { getTemplateQuestion } from "@/lib/questionService"; +import type { Difficulty } from "@/lib/types"; +import { toast } from "sonner"; + +interface ModuleSheetProps { + module: Module | null; + open: boolean; + onOpenChange: (open: boolean) => void; + difficulty: Difficulty; +} + +/** + * Get icon for difficulty + */ +function DifficultyIcon({ difficulty }: { difficulty: Difficulty }) { + switch (difficulty) { + case "beginner": + return ; + case "intermediate": + return ; + case "advanced": + return ; + } +} + +/** + * Problem type row with Generate button + */ +function ProblemTypeRow({ + problemType, + subtopic, + module, + onGenerate, +}: { + problemType: ProblemType; + subtopic: Subtopic; + module: Module; + onGenerate: ( + module: Module, + subtopic: Subtopic, + problemType: ProblemType + ) => void; +}) { + return ( +
+
+ + {problemType.name} +
+ +
+ ); +} + +/** + * Subtopic accordion item + */ +function SubtopicSection({ + subtopic, + module, + difficulty, + onGenerate, +}: { + subtopic: Subtopic; + module: Module; + difficulty: Difficulty; + onGenerate: ( + module: Module, + subtopic: Subtopic, + problemType: ProblemType + ) => void; +}) { + return ( + + +
+ {subtopic.name} + + {subtopic.problemTypes.length} + +
+
+ +
+ {subtopic.problemTypes.map((pt) => ( + + ))} +
+
+
+ ); +} + +export function ModuleSheet({ + module, + open, + onOpenChange, + difficulty, +}: ModuleSheetProps) { + const router = useRouter(); + + // Count totals + const stats = useMemo(() => { + if (!module) return { subtopics: 0, problemTypes: 0 }; + return { + subtopics: module.subtopics.length, + problemTypes: module.subtopics.reduce( + (acc, st) => acc + st.problemTypes.length, + 0 + ), + }; + }, [module]); + + // Handle Generate button + const handleGenerate = useCallback( + (mod: Module, subtopic: Subtopic, problemType: ProblemType) => { + const question = getTemplateQuestion(problemType.id, difficulty); + if (!question) { + toast.error("Failed to generate question"); + return; + } + + // Store in session and navigate + sessionStorage.setItem("pendingQuestion", JSON.stringify({ question })); + + onOpenChange(false); + router.push( + `/practice?mode=manual&module=${encodeURIComponent( + mod.id + )}&subtopic=${encodeURIComponent( + subtopic.id + )}&problemType=${encodeURIComponent( + problemType.id + )}&difficulty=${difficulty}` + ); + }, + [difficulty, router, onOpenChange] + ); + + if (!module) return null; + + return ( + + + +
+ + {module.order.toString().padStart(2, "0")} + + + + {difficulty} + +
+ {module.name} + {module.overview && ( + + {module.overview} + + )} +
+ {stats.subtopics} subtopics + {stats.problemTypes} problem types +
+
+ +
+
+ + {module.subtopics.map((subtopic) => ( + + ))} + +
+
+ + {/* Footer with quick practice */} +
+ +
+
+
+ ); +} diff --git a/src/components/practice/PracticeByProblemTypeView.tsx b/src/components/practice/PracticeByProblemTypeView.tsx new file mode 100644 index 0000000..f3a06cd --- /dev/null +++ b/src/components/practice/PracticeByProblemTypeView.tsx @@ -0,0 +1,330 @@ +"use client"; + +/** + * Practice By Problem Type View + * + * The compact dropdown-based problem type selector (legacy view). + * Wrapped as a toggleable component for the manual practice page. + */ + +import { useState, useMemo } from "react"; +import { useRouter } from "next/navigation"; +import { getAllModules } from "@/lib/topicsStore"; +import { getTemplateQuestion } from "@/lib/questionService"; +import type { Difficulty } from "@/lib/types"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { + Sparkle, + CaretRight, + Lightning, + Brain, + Rocket, +} from "@phosphor-icons/react"; +import { toast } from "sonner"; + +interface PracticeByProblemTypeViewProps { + difficulty: Difficulty; + onDifficultyChange: (difficulty: Difficulty) => void; +} + +export function PracticeByProblemTypeView({ + difficulty, + onDifficultyChange, +}: PracticeByProblemTypeViewProps) { + const router = useRouter(); + const modules = useMemo(() => getAllModules(), []); + + const [selectedModuleId, setSelectedModuleId] = useState(""); + const [selectedSubtopicId, setSelectedSubtopicId] = useState(""); + const [selectedProblemTypeId, setSelectedProblemTypeId] = + useState(""); + const [isGenerating, setIsGenerating] = useState(false); + + // Get current selections + const selectedModule = useMemo( + () => modules.find((m) => m.id === selectedModuleId), + [modules, selectedModuleId] + ); + + const selectedSubtopic = useMemo( + () => selectedModule?.subtopics.find((st) => st.id === selectedSubtopicId), + [selectedModule, selectedSubtopicId] + ); + + const selectedProblemType = useMemo( + () => + selectedSubtopic?.problemTypes.find( + (pt) => pt.id === selectedProblemTypeId + ), + [selectedSubtopic, selectedProblemTypeId] + ); + + // Reset dependent selections when parent changes + const handleModuleChange = (moduleId: string) => { + setSelectedModuleId(moduleId); + setSelectedSubtopicId(""); + setSelectedProblemTypeId(""); + }; + + const handleSubtopicChange = (subtopicId: string) => { + setSelectedSubtopicId(subtopicId); + setSelectedProblemTypeId(""); + }; + + const handleGenerate = async () => { + if (!selectedModule || !selectedSubtopic || !selectedProblemType) { + return; + } + + setIsGenerating(true); + + try { + const question = getTemplateQuestion(selectedProblemTypeId, difficulty); + + if (!question) { + toast.error("Failed to generate question for this problem type."); + setIsGenerating(false); + return; + } + + // Store generated question in sessionStorage for the practice page + sessionStorage.setItem("pendingQuestion", JSON.stringify({ question })); + + // Navigate to practice page + router.push( + `/practice?mode=manual&module=${encodeURIComponent( + selectedModuleId + )}&subtopic=${encodeURIComponent( + selectedSubtopicId + )}&problemType=${encodeURIComponent( + selectedProblemTypeId + )}&difficulty=${difficulty}` + ); + } catch (error) { + console.error("Generation failed:", error); + toast.error("Failed to generate question."); + setIsGenerating(false); + } + }; + + const canGenerate = + selectedModuleId && + selectedSubtopicId && + selectedProblemTypeId && + !isGenerating; + + return ( +
+ + +
+
+ +
+
+ Generate Practice Question +

+ Select a specific problem type to practice +

+
+ + {/* Module Selection */} +
+ + +
+ + {/* Subtopic Selection */} +
+ + +
+ + {/* Problem Type Selection */} +
+ + +
+ + {/* Difficulty Selection */} +
+ +
+ + + +
+
+ + {/* Breadcrumb Preview */} + {selectedProblemType && ( +
+

+ Your selection: +

+ + + + + {selectedModule?.name} + + + + + + {selectedSubtopic?.name} + + + + + + {selectedProblemType.name} + + + + +
+ )} + + {/* Generate Button */} + +
+
+
+ ); +} diff --git a/src/components/practice/PracticeConfigurator.tsx b/src/components/practice/PracticeConfigurator.tsx new file mode 100644 index 0000000..c32796b --- /dev/null +++ b/src/components/practice/PracticeConfigurator.tsx @@ -0,0 +1,402 @@ +"use client"; + +import { useState, useMemo, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { getAllModules } from "@/lib/topicsStore"; +import { getTemplateQuestion } from "@/lib/questionService"; +import type { Difficulty } from "@/lib/types"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { + CaretRight, + Lightning, + Brain, + Rocket, + Faders, +} from "@phosphor-icons/react"; +import { toast } from "sonner"; +import { cn } from "@/lib/utils"; + +export function PracticeConfigurator() { + const router = useRouter(); + const searchParams = useSearchParams(); + const modules = useMemo(() => getAllModules(), []); + + // Initialize from URL params if available + const initialModuleId = searchParams.get("module") || ""; + const initialSubtopicName = searchParams.get("subtopic") || ""; + const initialDiff = + (searchParams.get("difficulty") as Difficulty) || "beginner"; + + const [selectedModuleId, setSelectedModuleId] = + useState(initialModuleId); + const [selectedSubtopicId, setSelectedSubtopicId] = useState(""); + const [selectedProblemTypeId, setSelectedProblemTypeId] = + useState(""); + const [difficulty, setDifficulty] = useState(initialDiff); + + const [isGenerating, setIsGenerating] = useState(false); + + // Effect to sync state with URL params + useEffect(() => { + const modId = searchParams.get("module"); + const subName = searchParams.get("subtopic"); + const diff = searchParams.get("difficulty") as Difficulty; + + if (modId && modId !== selectedModuleId) { + setSelectedModuleId(modId); + } + + if (diff) { + setDifficulty(diff); + } + + if (modId && subName) { + const mod = modules.find((m) => m.id === modId); + if (mod) { + const sub = mod.subtopics.find((s) => s.name === subName); + if (sub && sub.id !== selectedSubtopicId) { + // Defer to next tick to avoid conflicts during hydration if needed, + // though typically safe in effect. + setSelectedSubtopicId(sub.id); + } + } + } + }, [searchParams, modules]); // Depend on searchParams effectively + + // Find module object + const selectedModule = useMemo( + () => modules.find((m) => m.id === selectedModuleId), + [modules, selectedModuleId] + ); + + const selectedSubtopic = useMemo( + () => selectedModule?.subtopics.find((st) => st.id === selectedSubtopicId), + [selectedModule, selectedSubtopicId] + ); + + const selectedProblemType = useMemo( + () => + selectedSubtopic?.problemTypes.find( + (pt) => pt.id === selectedProblemTypeId + ), + [selectedSubtopic, selectedProblemTypeId] + ); + // Suppress unused warning + void selectedProblemType; + + // Reset dependent selections when parent changes + const handleModuleChange = (moduleId: string) => { + setSelectedModuleId(moduleId); + setSelectedSubtopicId(""); + setSelectedProblemTypeId(""); + }; + + const handleSubtopicChange = (subtopicId: string) => { + setSelectedSubtopicId(subtopicId); + setSelectedProblemTypeId(""); + }; + + const handleGenerate = async () => { + // If no specific problem type selected, try to pick one randomly from subtopic? + // Requirements say "By Module/Subtopic/Problem Type". + // Let's enforce Problem Type selection for Manual Practice to be "Precise". + // Or we could auto-pick if user only selects Subtopic. + + let targetProblemTypeId = selectedProblemTypeId; + + if (!targetProblemTypeId && selectedSubtopic) { + // Auto-pick a problem type if only subtopic is selected + const types = selectedSubtopic.problemTypes; + if (types.length > 0) { + targetProblemTypeId = + types[Math.floor(Math.random() * types.length)].id; + } + } + + if (!selectedModule || !selectedSubtopic || !targetProblemTypeId) { + toast.error("Please select a module and subtopic."); + return; + } + + setIsGenerating(true); + + try { + const question = getTemplateQuestion(targetProblemTypeId, difficulty); + + if (!question) { + toast.error("Failed to generate question for this problem type."); + setIsGenerating(false); + return; + } + + // Store generated question in sessionStorage + sessionStorage.setItem("pendingQuestion", JSON.stringify({ question })); + + // Navigate to practice page + router.push( + `/practice?mode=manual&module=${encodeURIComponent( + selectedModuleId + )}&subtopic=${encodeURIComponent( + selectedSubtopicId + )}&problemType=${encodeURIComponent( + targetProblemTypeId + )}&difficulty=${difficulty}` + ); + } catch (error) { + console.error("Generation failed:", error); + toast.error("Failed to generate question."); + setIsGenerating(false); + } + }; + + const canGenerate = selectedModuleId && selectedSubtopicId && !isGenerating; + + return ( + + +
+ +
+ Configure Practice Session + + Customize your practice needs. Select a topic and difficulty to begin. + +
+ + +
+ {/* LEFT COL: Topic Selection */} +
+

+ + 1 + + Topics +

+ + {/* Module Selection */} +
+ + +
+ + {/* Subtopic Selection */} +
+ + +
+ + {/* Problem Type Selection */} +
+ + +
+
+ + {/* RIGHT COL: Difficulty & Preview */} +
+
+

+ + 2 + + Difficulty +

+ +
+ + + + + +
+
+ +
+ + {/* Preview Box */} + {selectedSubtopic && ( +
+
+ Ready to generate: +
+
+ {selectedSubtopic.name} + + + {difficulty.charAt(0).toUpperCase() + difficulty.slice(1)} + +
+
+ )} +
+
+ + +
+
+ ); +} diff --git a/src/components/practice/PracticeModuleCard.tsx b/src/components/practice/PracticeModuleCard.tsx new file mode 100644 index 0000000..b1667b6 --- /dev/null +++ b/src/components/practice/PracticeModuleCard.tsx @@ -0,0 +1,148 @@ +"use client"; + +/** + * Practice Module Card + * + * Module card designed for the practice context with: + * - Module name, order badge + * - Mastery progress bar (from statsStore) + * - Counts: subtopics, problem types + * - Quick actions: "Practice Module", "Open" (opens sheet) + */ + +import { useMemo } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Play, CaretRight, TreeStructure, Code } from "@phosphor-icons/react"; +import type { Module } from "@/lib/topicsStore"; + +interface PracticeModuleCardProps { + module: Module; + mastery: number; // 0-100 percentage + onPractice: (module: Module) => void; + onOpen: (module: Module) => void; +} + +/** + * Get color for mastery based on percentage + */ +function getMasteryColor(mastery: number): string { + if (mastery >= 80) return "text-green-500"; + if (mastery >= 50) return "text-yellow-500"; + if (mastery >= 20) return "text-orange-500"; + return "text-muted-foreground"; +} + +export function PracticeModuleCard({ + module, + mastery, + onPractice, + onOpen, +}: PracticeModuleCardProps) { + // Count totals + const subtopicCount = module.subtopics.length; + const problemTypeCount = useMemo( + () => module.subtopics.reduce((acc, st) => acc + st.problemTypes.length, 0), + [module.subtopics] + ); + + return ( + { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onOpen(module); + } + }} + > + +
+
+ + {module.order.toString().padStart(2, "0")} + + {module.name} +
+ + + + + {mastery}% + + + Module mastery + + +
+
+ + + {/* Mastery Progress */} +
+ +
+ + {/* Stats */} +
+
+ + {subtopicCount} subtopics +
+
+ + {problemTypeCount} problems +
+
+ + {/* Overview (truncated) */} + {module.overview && ( +

+ {module.overview} +

+ )} + + {/* Actions */} +
+ + +
+
+
+ ); +} diff --git a/src/components/practice/PracticeModulesView.tsx b/src/components/practice/PracticeModulesView.tsx new file mode 100644 index 0000000..e8c9045 --- /dev/null +++ b/src/components/practice/PracticeModulesView.tsx @@ -0,0 +1,185 @@ +"use client"; + +/** + * Practice Modules View + * + * Grid of module cards for the practice context. + * Shows mastery stats and allows drill-down into modules. + */ + +import { useMemo, useState, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { getAllModules, type Module } from "@/lib/topicsStore"; +import { getStats, type TopicStats } from "@/lib/statsStore"; +import { getTemplateQuestion } from "@/lib/questionService"; +import type { Difficulty } from "@/lib/types"; +import { PracticeModuleCard } from "./PracticeModuleCard"; +import { ModuleSheet } from "./ModuleSheet"; +import { Input } from "@/components/ui/input"; +import { MagnifyingGlass } from "@phosphor-icons/react"; +import { toast } from "sonner"; + +interface PracticeModulesViewProps { + difficulty: Difficulty; +} + +/** + * Calculate mastery percentage for a module based on subtopic stats. + * Falls back to 0 if no stats available. + */ +function calculateModuleMastery( + module: Module, + topicStats: TopicStats[] +): number { + // For now, we match module name to topic stats (legacy compatibility) + // TODO: Update when statsStore supports module/subtopic IDs + const stat = topicStats.find( + (s) => s.topic.toLowerCase() === module.name.toLowerCase() + ); + + if (!stat || stat.attempts === 0) return 0; + return Math.round((stat.solved / stat.attempts) * 100); +} + +/** + * Get the weakest subtopic in a module (or random if no stats). + * Returns a random problem type from that subtopic. + */ +function getWeakestProblemType(module: Module): string | null { + // For now, pick a random problem type since we don't have subtopic-level stats + // TODO: Implement proper weakest-subtopic bias when stats support it + const allProblemTypes = module.subtopics.flatMap((st) => + st.problemTypes.map((pt) => pt.id) + ); + + if (allProblemTypes.length === 0) return null; + return allProblemTypes[Math.floor(Math.random() * allProblemTypes.length)]; +} + +export function PracticeModulesView({ difficulty }: PracticeModulesViewProps) { + const router = useRouter(); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedModule, setSelectedModule] = useState(null); + const [isSheetOpen, setIsSheetOpen] = useState(false); + + // Get data + const modules = getAllModules(); + const stats = getStats(); + + // Filter modules based on search + const filteredModules = useMemo(() => { + if (!searchQuery.trim()) return modules; + + const query = searchQuery.toLowerCase(); + return modules.filter((mod) => { + if (mod.name.toLowerCase().includes(query)) return true; + if (mod.subtopics.some((st) => st.name.toLowerCase().includes(query))) + return true; + if ( + mod.subtopics.some((st) => + st.problemTypes.some((pt) => pt.name.toLowerCase().includes(query)) + ) + ) + return true; + return false; + }); + }, [modules, searchQuery]); + + const moduleMasteries = useMemo(() => { + const masteryMap: Record = {}; + for (const mod of modules) { + masteryMap[mod.id] = calculateModuleMastery(mod, stats.perTopic); + } + return masteryMap; + }, [modules, stats.perTopic]); + + // Handle "Practice Module" quick action + const handlePractice = useCallback( + (module: Module) => { + const problemTypeId = getWeakestProblemType(module); + if (!problemTypeId) { + toast.error("No problem types available in this module"); + return; + } + + // Generate question and navigate + const question = getTemplateQuestion(problemTypeId, difficulty); + if (!question) { + toast.error("Failed to generate question"); + return; + } + + // Store in session and navigate + sessionStorage.setItem("pendingQuestion", JSON.stringify({ question })); + + router.push( + `/practice?mode=manual&module=${encodeURIComponent( + module.id + )}&problemType=${encodeURIComponent( + problemTypeId + )}&difficulty=${difficulty}` + ); + }, + [difficulty, router] + ); + + // Handle opening module sheet + const handleOpen = useCallback((module: Module) => { + setSelectedModule(module); + setIsSheetOpen(true); + }, []); + + return ( +
+ {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+ + {/* Results Count */} + {searchQuery && ( +

+ Found{" "} + {filteredModules.length}{" "} + module{filteredModules.length !== 1 ? "s" : ""} +

+ )} + + {/* Modules Grid */} + {filteredModules.length === 0 ? ( +
+

+ No modules found matching your search. +

+
+ ) : ( +
+ {filteredModules.map((module) => ( + + ))} +
+ )} + + {/* Module Sheet */} + +
+ ); +} diff --git a/src/components/practice/QuestionPanel.tsx b/src/components/practice/QuestionPanel.tsx index d5d3da9..3b10f6b 100644 --- a/src/components/practice/QuestionPanel.tsx +++ b/src/components/practice/QuestionPanel.tsx @@ -5,8 +5,14 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; - import { Skeleton } from "@/components/ui/skeleton"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; interface QuestionPanelProps { question: Question | null; @@ -74,6 +80,24 @@ export function QuestionPanel({ question, isLoading }: QuestionPanelProps) { return ( + {/* Breadcrumbs - show if we have topic hierarchy info */} + {question.topic && question.topicName !== question.topic && ( + + + + + {question.topicName} + + + + + + {question.topic} + + + + + )}
{question.topicName} diff --git a/src/components/practice/TopicPicker.tsx b/src/components/practice/TopicPicker.tsx new file mode 100644 index 0000000..3133683 --- /dev/null +++ b/src/components/practice/TopicPicker.tsx @@ -0,0 +1,273 @@ +"use client"; + +/** + * Topic Picker - Hierarchical Module → Subtopic → ProblemType selection + * + * Provides a three-level cascading selection using the new topics hierarchy. + */ + +import { useState, useMemo } from "react"; +import { getAllModules } from "@/lib/topicsStore"; +import { type Difficulty } from "@/lib/types"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { + Sparkle, + CaretRight, + Lightning, + Brain, + Rocket, +} from "@phosphor-icons/react"; + +export interface TopicSelection { + moduleId: string; + moduleName: string; + subtopicId: string; + subtopicName: string; + problemTypeId: string; + problemTypeName: string; +} + +interface TopicPickerProps { + onGenerate: (selection: TopicSelection, difficulty: Difficulty) => void; + isLoading?: boolean; +} + +export function TopicPicker({ onGenerate, isLoading }: TopicPickerProps) { + const modules = useMemo(() => getAllModules(), []); + + const [selectedModuleId, setSelectedModuleId] = useState(""); + const [selectedSubtopicId, setSelectedSubtopicId] = useState(""); + const [selectedProblemTypeId, setSelectedProblemTypeId] = + useState(""); + const [difficulty, setDifficulty] = useState("beginner"); + + // Get current selections + const selectedModule = useMemo( + () => modules.find((m) => m.id === selectedModuleId), + [modules, selectedModuleId] + ); + + const selectedSubtopic = useMemo( + () => selectedModule?.subtopics.find((st) => st.id === selectedSubtopicId), + [selectedModule, selectedSubtopicId] + ); + + const selectedProblemType = useMemo( + () => + selectedSubtopic?.problemTypes.find( + (pt) => pt.id === selectedProblemTypeId + ), + [selectedSubtopic, selectedProblemTypeId] + ); + + // Reset dependent selections when parent changes + const handleModuleChange = (moduleId: string) => { + setSelectedModuleId(moduleId); + setSelectedSubtopicId(""); + setSelectedProblemTypeId(""); + }; + + const handleSubtopicChange = (subtopicId: string) => { + setSelectedSubtopicId(subtopicId); + setSelectedProblemTypeId(""); + }; + + const handleGenerate = () => { + if (!selectedModule || !selectedSubtopic || !selectedProblemType) { + return; + } + + onGenerate( + { + moduleId: selectedModule.id, + moduleName: selectedModule.name, + subtopicId: selectedSubtopic.id, + subtopicName: selectedSubtopic.name, + problemTypeId: selectedProblemType.id, + problemTypeName: selectedProblemType.name, + }, + difficulty + ); + }; + + const canGenerate = + selectedModuleId && + selectedSubtopicId && + selectedProblemTypeId && + !isLoading; + + return ( + + + + + Generate Practice Question + + + + {/* Module Selection */} +
+ + +
+ + {/* Subtopic Selection */} +
+ + +
+ + {/* Problem Type Selection */} +
+ + +
+ + {/* Difficulty Selection */} +
+ +
+ + + +
+
+ + {/* Breadcrumb Preview */} + {selectedProblemType && ( +
+ {selectedModule?.name} + + {selectedSubtopic?.name} + + + {selectedProblemType.name} + +
+ )} + + {/* Generate Button */} + +
+
+ ); +} diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..c27252e --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { CaretRight, DotsThree } from "@phosphor-icons/react"; + +import { cn } from "@/lib/utils"; + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return