From 943f73d90d8aae47377bd2680e447a07be27ab89 Mon Sep 17 00:00:00 2001 From: sh1shank Date: Tue, 9 Dec 2025 17:31:07 +0530 Subject: [PATCH 1/3] feat: Introduce module-based practice, auto-mode V2, and question management with new UI components and data structures. --- archive/legacy-backup-20251209/README.md | 39 + .../legacy-backup-20251209}/evaluateCode.ts | 0 .../generateQuestion.ts | 0 .../legacy-backup-20251209}/getHints.ts | 0 .../optimizeSolution.ts | 0 .../legacy-backup-20251209}/revealSolution.ts | 0 cleanup-report.json | 90 + docs/AUTO_MODE_V2.md | 100 + package-lock.json | 3357 +++++++++- package.json | 10 +- playwright.config.ts | 25 + scripts/convert-md-to-json.ts | 354 + scripts/migrate-stats.ts | 368 ++ src/app/modules/page.tsx | 32 + src/app/page.tsx | 11 + src/app/practice/auto/page.tsx | 569 +- src/app/practice/manual/page.tsx | 167 +- src/app/practice/page.tsx | 94 +- src/components/automode/AutoModeControls.tsx | 123 + src/components/automode/AutoModeStatsBar.tsx | 4 +- .../automode/AutoModeStatsBarV2.tsx | 244 + src/components/dashboard/ModuleStatCard.tsx | 229 + src/components/dashboard/ModuleStatsGrid.tsx | 225 + src/components/dashboard/SubtopicTile.tsx | 152 + src/components/modules/ModuleCard.tsx | 237 + src/components/modules/ModulesGrid.tsx | 178 + src/components/modules/SubtopicAccordion.tsx | 115 + src/components/modules/index.ts | 3 + src/components/practice/ModuleSheet.tsx | 260 + .../practice/PracticeByProblemTypeView.tsx | 330 + .../practice/PracticeModuleCard.tsx | 148 + .../practice/PracticeModulesView.tsx | 185 + src/components/practice/QuestionPanel.tsx | 26 +- src/components/practice/TopicPicker.tsx | 273 + src/components/ui/breadcrumb.tsx | 109 + src/components/ui/collapsible.tsx | 33 + src/components/ui/popover.tsx | 48 + src/data/topics.json | 5817 +++++++++++++++++ src/data/topics.md | 2024 ++++++ src/lib/ai/modelRouter.ts | 24 +- src/lib/autoModeService.ts | 377 +- src/lib/autoModeServiceV2.ts | 639 ++ src/lib/autoRunTypes.ts | 169 + src/lib/questionService.ts | 457 ++ src/lib/questionTemplates.ts | 691 ++ src/lib/statsStore.ts | 707 +- src/lib/topicsStore.ts | 305 + src/types/question.ts | 126 + src/types/topics.ts | 105 + tsconfig.json | 2 +- vitest.config.ts | 13 + 51 files changed, 18946 insertions(+), 648 deletions(-) create mode 100644 archive/legacy-backup-20251209/README.md rename {src/lib/ai => archive/legacy-backup-20251209}/evaluateCode.ts (100%) rename {src/lib/ai => archive/legacy-backup-20251209}/generateQuestion.ts (100%) rename {src/lib/ai => archive/legacy-backup-20251209}/getHints.ts (100%) rename {src/lib/ai => archive/legacy-backup-20251209}/optimizeSolution.ts (100%) rename {src/lib/ai => archive/legacy-backup-20251209}/revealSolution.ts (100%) create mode 100644 cleanup-report.json create mode 100644 docs/AUTO_MODE_V2.md create mode 100644 playwright.config.ts create mode 100644 scripts/convert-md-to-json.ts create mode 100644 scripts/migrate-stats.ts create mode 100644 src/app/modules/page.tsx create mode 100644 src/components/automode/AutoModeControls.tsx create mode 100644 src/components/automode/AutoModeStatsBarV2.tsx create mode 100644 src/components/dashboard/ModuleStatCard.tsx create mode 100644 src/components/dashboard/ModuleStatsGrid.tsx create mode 100644 src/components/dashboard/SubtopicTile.tsx create mode 100644 src/components/modules/ModuleCard.tsx create mode 100644 src/components/modules/ModulesGrid.tsx create mode 100644 src/components/modules/SubtopicAccordion.tsx create mode 100644 src/components/modules/index.ts create mode 100644 src/components/practice/ModuleSheet.tsx create mode 100644 src/components/practice/PracticeByProblemTypeView.tsx create mode 100644 src/components/practice/PracticeModuleCard.tsx create mode 100644 src/components/practice/PracticeModulesView.tsx create mode 100644 src/components/practice/TopicPicker.tsx create mode 100644 src/components/ui/breadcrumb.tsx create mode 100644 src/components/ui/collapsible.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/data/topics.json create mode 100644 src/data/topics.md create mode 100644 src/lib/autoModeServiceV2.ts create mode 100644 src/lib/autoRunTypes.ts create mode 100644 src/lib/questionService.ts create mode 100644 src/lib/questionTemplates.ts create mode 100644 src/lib/topicsStore.ts create mode 100644 src/types/question.ts create mode 100644 src/types/topics.ts create mode 100644 vitest.config.ts 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/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.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/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/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/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/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/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/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/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
-
-
-

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 +424,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 +435,7 @@ export function GettingStartedSection() { - Get API Key + Get Key from Google @@ -535,12 +452,12 @@ export function GettingStartedSection() { - - Open API Settings + + Enter API Key @@ -549,152 +466,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." - 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." + description="Flow state. Let Pytrix decide what you need." 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 +608,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 -

-
-
-
- - 2nd - -
-

gemini-2.5-flash

-

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

+ +
+
+ + K
+

Try it now!

-
- - 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 +663,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 +855,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 +919,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 +974,7 @@ export function FaqSection() {
-

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

+

Still have questions?

@@ -1372,14 +994,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..c87c78f --- /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/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/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/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..4b9cd4e --- /dev/null +++ b/src/components/modules/CurriculumExplorer.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useState } from "react"; +import { getAllModules } from "@/lib/topicsStore"; +import { ModuleSidebar } from "./ModuleSidebar"; +import { ModuleDetailView } from "./ModuleDetailView"; +import { cn } from "@/lib/utils"; + +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/ModuleDetailView.tsx b/src/components/modules/ModuleDetailView.tsx new file mode 100644 index 0000000..f62d3d9 --- /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, ProblemType } from "@/types/topics"; +import { getModuleStats, getSubtopicStats } from "@/lib/statsStore"; +import { cn } from "@/lib/utils"; + +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/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/lib/internalTestClient.ts b/src/lib/internalTestClient.ts new file mode 100644 index 0000000..3fcdb9a --- /dev/null +++ b/src/lib/internalTestClient.ts @@ -0,0 +1,67 @@ +"use server"; + +import { callGeminiWithFallback } from "@/lib/ai/modelRouter"; +import type { TaskType } from "@/lib/ai/modelRouter"; + +/** + * INTERNAL TEST CLIENT + * + * This module is for internal development and agentic testing ONLY. + * It provides access to the internal Gemini key defined in .env.local + * for running QA checks, migrations, and diagnostics. + * + * SECURITY RULES: + * 1. Server-side only ("use server"). + * 2. Key must never leak to client. + * 3. Disabled in production. + */ + +const INTERNAL_KEY = process.env.INTERNAL_GEMINI_KEY; +const IS_DEV = process.env.NODE_ENV === "development"; + +export async function getInternalClient() { + if (!IS_DEV) { + throw new Error("Internal client is disabled in production."); + } + + if (!INTERNAL_KEY) { + console.warn( + "[InternalTestClient] No INTERNAL_GEMINI_KEY found. Skipping test." + ); + return null; + } + + return { + /** + * Generate content using the internal developer key. + */ + generateContent: async ( + task: TaskType, + prompt: string, + parser?: (text: string) => T, + options?: { difficulty?: "beginner" | "intermediate" | "advanced" } + ) => { + // Security check again just in case + if (process.env.NODE_ENV !== "development") { + throw new Error("Internal calls blocked in production"); + } + + console.log(`[InternalClient] calling Gemini for task: ${task}`); + + return callGeminiWithFallback( + INTERNAL_KEY, + task, + prompt, + parser, + options?.difficulty + ); + }, + }; +} + +/** + * Helper to check if internal generic testing is available + */ +export async function isInternalCheckAvailable(): Promise { + return IS_DEV && !!INTERNAL_KEY; +} diff --git a/src/lib/searchIndex.ts b/src/lib/searchIndex.ts new file mode 100644 index 0000000..5cc832f --- /dev/null +++ b/src/lib/searchIndex.ts @@ -0,0 +1,189 @@ +import { getAllModules } from "@/lib/topicsStore"; + +export type SearchResultType = + | "page" + | "module" + | "subtopic" + | "archetype" + | "action"; + +export interface SearchResult { + id: string; + title: string; + subtitle?: string; // Breadcrumb or description + type: SearchResultType; + href?: string; // Direct navigation + onSelect?: () => void; // Function action + keywords?: string[]; // Fuzzy matching helpers + icon?: string; // Icon name (phosphor) + score?: number; // Ranking score +} + +// Static Page Navigation +const STATIC_PAGES: SearchResult[] = [ + { + id: "nav-dashboard", + title: "Dashboard", + type: "page", + href: "/dashboard", + icon: "House", + }, + { + id: "nav-modules", + title: "Modules", + type: "page", + href: "/modules", + icon: "Books", + }, + { + id: "nav-practice", + title: "Manual Practice", + type: "page", + href: "/practice/manual", + icon: "Code", + }, + { + id: "nav-auto", + title: "Auto Mode", + type: "page", + href: "/practice/auto", + icon: "Lightning", + }, + { + id: "nav-history", + title: "History", + type: "page", + href: "/history", + icon: "Clock", + }, + { + id: "nav-stats", + title: "Stats & Progress", + type: "page", + href: "/insights/stats", + icon: "ChartLine", + }, + { + id: "nav-api", + title: "API Usage", + type: "page", + href: "/insights/api-usage", + icon: "Cpu", + }, + { + id: "nav-settings", + title: "Settings", + type: "page", + href: "/support/settings", + icon: "Gear", + }, + { + id: "nav-help", + title: "Help & Docs", + type: "page", + href: "/support/help", + icon: "Question", + }, +]; + +/** + * flattens the curriculum into searchable items + */ +// Export raw lists for filtered access +export function getStaticPages(): SearchResult[] { + return STATIC_PAGES; +} + +export function getModuleItems(): SearchResult[] { + return getAllModules().map((mod) => ({ + id: `mod-${mod.id}`, + title: mod.name, + subtitle: `Module ${mod.order}`, + type: "module", + href: `/modules?search=${encodeURIComponent(mod.name)}`, + icon: "BookOpen", + keywords: ["module", mod.name], + })); +} + +export function getSubtopicItems(): SearchResult[] { + const modules = getAllModules(); + const results: SearchResult[] = []; + modules.forEach((mod) => { + mod.subtopics.forEach((sub) => { + results.push({ + id: `sub-${sub.id}`, + title: sub.name, + subtitle: `${mod.name} › ${sub.name}`, + type: "subtopic", + href: `/practice?mode=topic-select&topic=${encodeURIComponent( + sub.name + )}&module=${mod.id}&subtopic=${encodeURIComponent( + sub.name + )}&difficulty=beginner&problemType=${sub.problemTypes[0]?.id || ""}`, + icon: "Hash", + keywords: [sub.name, mod.name], + }); + }); + }); + return results; +} + +export function getArchetypeItems(): SearchResult[] { + const modules = getAllModules(); + const results: SearchResult[] = []; + modules.forEach((mod) => { + mod.subtopics.forEach((sub) => { + sub.problemTypes.forEach((pt) => { + results.push({ + id: `pt-${pt.id}`, + title: pt.name, + subtitle: `${mod.name} › ${sub.name} › ${pt.name}`, + type: "archetype", + href: `/practice?mode=topic-select&topic=${encodeURIComponent( + pt.name + )}&module=${mod.id}&subtopic=${encodeURIComponent( + sub.name + )}&problemType=${pt.id}&difficulty=beginner`, + icon: "Lightning", + keywords: [pt.name, sub.name, mod.name, pt.description || ""], + }); + }); + }); + }); + return results; +} + +/** + * Searches items by query string (case-insensitive fuzzy matchish) + */ +export function searchItems( + items: SearchResult[], + query: string +): SearchResult[] { + const lowerQuery = query.toLowerCase().trim(); + if (!lowerQuery) return items; + + return items.filter((item) => { + // Check title + if (item.title.toLowerCase().includes(lowerQuery)) return true; + // Check subtitle + if (item.subtitle?.toLowerCase().includes(lowerQuery)) return true; + // Check keywords + if (item.keywords?.some((k) => k.toLowerCase().includes(lowerQuery))) + return true; + return false; + }); +} + +/** + * Legacy support if needed, but prefer specific getters + */ +export function getAllSearchItems(): SearchResult[] { + return [ + ...getStaticPages(), + ...getModuleItems(), + ...getSubtopicItems(), + ...getArchetypeItems(), + ]; +} diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..eb0ec6e --- /dev/null +++ b/tests/README.md @@ -0,0 +1,22 @@ +# Testing Strategy + +This project uses a dual testing strategy: + +## Unit Tests (Vitest) + +For testing logic, utilities, and individual components. + +- Run: `npm test` +- config: `vitest.config.ts` + +## E2E Tests (Playwright) + +For testing user flows and routes. + +- Run: `npm run test:e2e` (requires dev server running or it will start one) +- config: `playwright.config.ts` + +## Files + +- `tests/unit/`: Unit tests (e.g., `questionService.test.ts`) +- `tests/e2e/`: End-to-end tests (e.g., `manual-flow.test.ts`) diff --git a/tests/e2e/command-center.test.ts b/tests/e2e/command-center.test.ts new file mode 100644 index 0000000..f05709d --- /dev/null +++ b/tests/e2e/command-center.test.ts @@ -0,0 +1,67 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Command Center", () => { + test("should handle initial state and search grouping correctly", async ({ + page, + }) => { + // Navigate to dashboard + await page.goto("/dashboard"); + + // Open Command Center + await page.keyboard.press("Meta+k"); + const dialog = page.getByRole("dialog", { name: "Command Palette" }); + await expect(dialog).toBeVisible(); + + // 1. Verify Initial State + // Should show "Go To" and "Modules" + await expect(dialog.getByRole("group", { name: "Go To" })).toBeVisible(); + await expect(dialog.getByRole("group", { name: "Modules" })).toBeVisible(); + + // Should NOT show "Subtopics" or "Problem Archetypes" + await expect(dialog.getByRole("group", { name: "Subtopics" })).toBeHidden(); + await expect( + dialog.getByRole("group", { name: "Problem Archetypes" }) + ).toBeHidden(); + + // 2. Perform Search + const searchInput = page.getByPlaceholder( + "Type a command or search topic..." + ); + await searchInput.fill("string"); + + // 3. Verify Search State + // "Go To" might be hidden if no matches, but let's check for curriculum items + // "Subtopics" and "Problem Archetypes" SHOULD appear if there are matches for "string" + // Assuming "String Manipulation" module and related subtopics exist + await expect(dialog.getByRole("group", { name: "Modules" })).toBeVisible(); // Module "String Manipulation" should match + await expect( + dialog.getByRole("group", { name: "Subtopics" }) + ).toBeVisible(); + await expect( + dialog.getByRole("group", { name: "Problem Archetypes" }) + ).toBeVisible(); + + // 4. Verify Clearing Search + await searchInput.fill(""); + await expect(dialog.getByRole("group", { name: "Subtopics" })).toBeHidden(); + + // 5. Test Navigation via Archetype + await searchInput.fill("string"); + // Click on a known archetype if possible, or just the first result in Archetypes group + const archetypeItem = dialog + .getByRole("group", { name: "Problem Archetypes" }) + .getByRole("option") + .first(); + + await expect(archetypeItem).toBeVisible(); + await archetypeItem.click(); + + // Should navigate to practice page + await expect(page).toHaveURL(/\/practice\?mode=topic-select/); + // Verify parameters are present + const url = new URL(page.url()); + expect(url.searchParams.get("module")).toBeTruthy(); + expect(url.searchParams.get("subtopic")).toBeTruthy(); + expect(url.searchParams.get("problemType")).toBeTruthy(); + }); +}); diff --git a/tests/e2e/dashboard-flow.test.ts b/tests/e2e/dashboard-flow.test.ts new file mode 100644 index 0000000..f93064d --- /dev/null +++ b/tests/e2e/dashboard-flow.test.ts @@ -0,0 +1,80 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Dashboard V2 Flow", () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem( + "pypractice_api_config_v1", + JSON.stringify({ + provider: "gemini", + apiKey: process.env.TEST_API_KEY || "dummy-key", + model: "gemini-flash-lite", + }) + ); + localStorage.setItem( + "pytrix-settings", + JSON.stringify({ + state: { + hasCompletedOnboarding: true, + onboardingStep: 5, + }, + version: 1, + }) + ); + }); + }); + + test("should load dashboard and show modules", async ({ page }) => { + // Navigate to dashboard (home) + await page.goto("/"); + + // Check for "Your Progress" and "Module Progress" headers + await expect( + page.getByRole("heading", { name: "Your Progress" }) + ).toBeVisible(); + + // Check for at least one module card (e.g. "Arrays & Lists") + // Note: The specific output name depends on topics.json + // We expect "Module Progress" area to contain grid items + const grid = page.locator(".grid"); + await expect(grid.first()).toBeVisible(); + + // Look for a known module name + await expect( + page.getByText("Strings").or(page.getByText("Lists")) + ).toBeVisible(); + }); + + test("Practice Module button should navigate with subtopic param", async ({ + page, + }) => { + await page.goto("/"); + + // Find a "Practice" button within a module card. + // We target the button that says "Practice" + const practiceBtn = page.getByRole("button", { name: "Practice" }).first(); + await expect(practiceBtn).toBeVisible(); + + await practiceBtn.click(); + + // Verify navigation + // Verify navigation + await expect(page).toHaveURL( + /\/practice\?mode=manual&topic=.*&difficulty=beginner/ + ); + }); + + test("View Details button should open sheet", async ({ page }) => { + await page.goto("/"); + + // Click "View Details" + const detailsBtn = page + .getByRole("button", { name: "View Details" }) + .first(); + await detailsBtn.click(); + + // Verify Sheet opens (look for "Mastery" or "Subtopics") + await expect(page.getByRole("dialog")).toBeVisible(); + await expect(page.getByText("Subtopics", { exact: true })).toBeVisible(); + }); +}); diff --git a/tests/e2e/manual-flow.test.ts b/tests/e2e/manual-flow.test.ts new file mode 100644 index 0000000..2e3a158 --- /dev/null +++ b/tests/e2e/manual-flow.test.ts @@ -0,0 +1,46 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Manual Practice Flow", () => { + test("should navigate to manual practice and load modules", async ({ + page, + }) => { + // Navigate to manual practice page + await page.goto("/practice/manual"); + + // Check title (use heading role to avoid sidebar/breadcrumb ambiguity) + await expect( + page.getByRole("heading", { name: "Manual Practice", level: 2 }) + ).toBeVisible(); + + // Check if modules are loaded (assuming some module names) + // We might need to wait for modules to appear if they are client-side loaded + // "Strings" or "Arrays" are likely module names + await expect( + page.getByText("Strings").or(page.getByText("Sequences")) + ).toBeVisible(); + }); + + test("should allow switching difficulty", async ({ page }) => { + await page.goto("/practice/manual"); + + // Click Intermediate + await page.getByRole("button", { name: "Intermediate" }).click(); + + // Check visually active state (optional, hard to check styles easily without knowing exact classes) + // But we can check if it stays on page without error + await expect(page.getByText("Manual Practice")).toBeVisible(); + }); + + // Note: Full E2E test of "Run & Check" is complex because of Pyodide loading + // and need for real browser execution context which Playwright provides, + // but it might be flaky depending on machine speed. + // We will verify the editor is present. + + /* + test('should load practice workspace', async ({ page }) => { + // This assumes we can click into a problem. + // Since we don't know exact selectors for "Practice Strings", we skip for now + // or need to inspect DOM structure more. + }); + */ +}); diff --git a/tests/unit/questionService.test.ts b/tests/unit/questionService.test.ts new file mode 100644 index 0000000..682e9b0 --- /dev/null +++ b/tests/unit/questionService.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { getQuestion } from "@/lib/questionService"; +import { generateTemplate } from "@/lib/questionTemplates"; + +// Mock dependencies +vi.mock("@/lib/ai/modelRouter", () => ({ + callGeminiWithFallback: vi.fn(), +})); + +vi.mock("@/lib/questionTemplates", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + generateTemplate: vi.fn(), + }; +}); + +describe("questionService", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe("getQuestion", () => { + it("should return template-based question when useLLM is false", async () => { + // Setup mock + const mockTemplate = { + id: "test-template", + problemTypeId: "binary-search", + problemTypeName: "Binary Search", + moduleId: "algorithms", + moduleName: "Algorithms", + subtopicId: "search", + subtopicName: "Search", + difficulty: "beginner", + title: "[Easy] Binary Search", + promptTemplate: "Describe binary search", + compactPrompt: "Task: Create...", + sampleInputs: ["in"], + sampleOutputs: ["out"], + edgeCases: [], + constraints: [], + hints: [], + tags: [], + estimatedMinutes: 5, + starterCode: "def solution(): pass", + testCases: [{ input: "in", expectedOutput: "out", isHidden: false }], + }; + + vi.mocked(generateTemplate).mockReturnValue(mockTemplate as any); + + const result = await getQuestion("binary-search", "beginner", { + useLLM: false, + }); + + expect(result.success).toBe(true); + expect(result.source).toBe("template"); + expect(result.question).toBeDefined(); + expect(result.question?.title).toBe("[Easy] Binary Search"); + }); + + it("should fall back to template if template generation fails", async () => { + vi.mocked(generateTemplate).mockReturnValue(undefined); + const result = await getQuestion("invalid-id", "beginner"); + expect(result.success).toBe(false); + expect(result.error).toContain("Problem type not found"); + }); + }); +}); From 755b0d66144700a8b42cb4acd6cc3f48efd5cc88 Mon Sep 17 00:00:00 2001 From: sh1shank Date: Tue, 9 Dec 2025 18:10:15 +0530 Subject: [PATCH 3/3] refactor: Simplify `generateTitle` function signature, remove unused imports and props, and update UI text and styling. --- src/app/support/help/content.tsx | 5 ++--- src/components/CommandCenter.tsx | 2 +- src/components/modules/CurriculumExplorer.tsx | 1 - src/components/modules/ModuleDetailView.tsx | 6 +++--- src/lib/questionTemplates.ts | 5 ++--- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/app/support/help/content.tsx b/src/app/support/help/content.tsx index f6184c1..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, @@ -13,12 +14,10 @@ import { Command, Gear, ListBullets, - TreeStructure, Target, TerminalWindow, Database, GitBranch, - SpinnerGap, CheckCircle, Info, ArrowRight, @@ -328,7 +327,7 @@ function FeatureCard({ title, description, icon, details }: FeatureCardProps) { export function AboutSection() { return ( <> - +
diff --git a/src/components/CommandCenter.tsx b/src/components/CommandCenter.tsx index c87c78f..2d09b5b 100644 --- a/src/components/CommandCenter.tsx +++ b/src/components/CommandCenter.tsx @@ -137,7 +137,7 @@ export function CommandCenter() { return ( <> {/* Removed trigger button since it's global shortcut, but could add a hidden one or use context */} - + ) : (
- No topics found matching "{searchQuery}" in this module. + No topics found matching “{searchQuery}” in this + module.
)}
diff --git a/src/lib/questionTemplates.ts b/src/lib/questionTemplates.ts index d190bb5..83d9a2b 100644 --- a/src/lib/questionTemplates.ts +++ b/src/lib/questionTemplates.ts @@ -511,8 +511,7 @@ function generateTemplateId( */ function generateTitle( problemType: ProblemType, - difficulty: Difficulty, - _pattern: Partial + difficulty: Difficulty ): string { const difficultyPrefix = difficulty === "beginner" @@ -597,7 +596,7 @@ export function generateTemplate( // Generate all components const sampleInputs = generateSampleInputs(difficulty, pattern); const sampleOutputs = generateSampleOutputs(difficulty, pattern); - const title = generateTitle(problemType, difficulty, pattern); + const title = generateTitle(problemType, difficulty); const promptTemplate = generatePromptTemplate( problemType, difficulty,