From d42e47b15b8d543a5bc771d7a21fd7500d9bcee2 Mon Sep 17 00:00:00 2001 From: polwex Date: Tue, 24 Mar 2026 16:12:30 +0700 Subject: [PATCH] init --- .gitignore | 24 ++ README.md | 73 +++++ bun.lock | 515 ++++++++++++++++++++++++++++++++ eslint.config.js | 23 ++ index.html | 16 + package.json | 31 ++ src/App.tsx | 76 +++++ src/components/FreeMode.tsx | 69 +++++ src/components/GameMode.tsx | 396 ++++++++++++++++++++++++ src/components/Keyboard.tsx | 69 +++++ src/components/Layout.tsx | 58 ++++ src/components/LessonMode.tsx | 97 ++++++ src/components/LessonSelect.tsx | 52 ++++ src/components/ResultsModal.tsx | 45 +++ src/components/StatsView.tsx | 164 ++++++++++ src/components/TypingArea.tsx | 57 ++++ src/data/keyboard.ts | 100 +++++++ src/data/lessons.ts | 171 +++++++++++ src/data/quotes.ts | 37 +++ src/hooks/useStats.ts | 68 +++++ src/hooks/useTypingEngine.ts | 122 ++++++++ src/main.tsx | 10 + src/styles/game.module.css | 310 +++++++++++++++++++ src/styles/global.css | 69 +++++ src/styles/keyboard.module.css | 71 +++++ src/styles/typing.module.css | 221 ++++++++++++++ src/types.ts | 33 ++ tsconfig.app.json | 28 ++ tsconfig.json | 7 + tsconfig.node.json | 26 ++ vite.config.ts | 7 + 31 files changed, 3045 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bun.lock create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package.json create mode 100644 src/App.tsx create mode 100644 src/components/FreeMode.tsx create mode 100644 src/components/GameMode.tsx create mode 100644 src/components/Keyboard.tsx create mode 100644 src/components/Layout.tsx create mode 100644 src/components/LessonMode.tsx create mode 100644 src/components/LessonSelect.tsx create mode 100644 src/components/ResultsModal.tsx create mode 100644 src/components/StatsView.tsx create mode 100644 src/components/TypingArea.tsx create mode 100644 src/data/keyboard.ts create mode 100644 src/data/lessons.ts create mode 100644 src/data/quotes.ts create mode 100644 src/hooks/useStats.ts create mode 100644 src/hooks/useTypingEngine.ts create mode 100644 src/main.tsx create mode 100644 src/styles/game.module.css create mode 100644 src/styles/global.css create mode 100644 src/styles/keyboard.module.css create mode 100644 src/styles/typing.module.css create mode 100644 src/types.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..bedca40 --- /dev/null +++ b/bun.lock @@ -0,0 +1,515 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "temp-scaffold", + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4", + "recharts": "^3.8.0", + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^8.0.1", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + + "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], + + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "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" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.11", "", { "os": "android", "cpu": "arm64" }, "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm" }, "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "x64" }, "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.11", "", { "os": "none", "cpu": "arm64" }, "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.11", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.11", "", { "os": "win32", "cpu": "x64" }, "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/type-utils": "8.57.2", "@typescript-eslint/utils": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.2", "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.2", "@typescript-eslint/tsconfig-utils": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.321", "", {}, "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ=="], + + "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.2", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="], + + "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + + "recharts": ["recharts@3.8.0", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="], + + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "rolldown": ["rolldown@1.0.0-rc.11", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.11" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.11", "@rolldown/binding-darwin-arm64": "1.0.0-rc.11", "@rolldown/binding-darwin-x64": "1.0.0-rc.11", "@rolldown/binding-freebsd-x64": "1.0.0-rc.11", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.57.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + + "vite": ["vite@8.0.2", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.11", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.11", "", {}, "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + } +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..a491e19 --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + + Leo's Typing Tutor + + + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..d109811 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "temp-scaffold", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4", + "recharts": "^3.8.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^8.0.1" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..27eed07 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,76 @@ +import { useState, useCallback } from 'react' +import { useStats } from './hooks/useStats' +import { Layout } from './components/Layout' +import { LessonMode } from './components/LessonMode' +import { FreeMode } from './components/FreeMode' +import { GameMode } from './components/GameMode' +import { StatsView } from './components/StatsView' +import type { SessionResult } from './types' + +type Tab = 'lessons' | 'free' | 'game' | 'stats' + +export default function App() { + const [tab, setTab] = useState('lessons') + const { progress, completeLesson, addSession, updateHighScore } = useStats() + + const handleLessonComplete = useCallback(( + lessonId: number, + wpm: number, + accuracy: number, + keyStats: Record, + ) => { + const result: SessionResult = { + mode: `lesson-${lessonId}`, + wpm, + accuracy, + timestamp: Date.now(), + keyStats, + } + addSession(result) + if (accuracy >= 90) { + completeLesson(lessonId) + } + }, [addSession, completeLesson]) + + const handleFreeComplete = useCallback(( + wpm: number, + accuracy: number, + keyStats: Record, + ) => { + addSession({ + mode: 'free', + wpm, + accuracy, + timestamp: Date.now(), + keyStats, + }) + }, [addSession]) + + const handleGameOver = useCallback((score: number) => { + updateHighScore(score) + addSession({ + mode: 'game', + wpm: 0, + accuracy: 0, + timestamp: Date.now(), + keyStats: {}, + }) + }, [updateHighScore, addSession]) + + return ( + + {tab === 'lessons' && ( + + )} + {tab === 'free' && ( + + )} + {tab === 'game' && ( + + )} + {tab === 'stats' && ( + + )} + + ) +} diff --git a/src/components/FreeMode.tsx b/src/components/FreeMode.tsx new file mode 100644 index 0000000..d234d2e --- /dev/null +++ b/src/components/FreeMode.tsx @@ -0,0 +1,69 @@ +import { useState, useCallback } from 'react' +import { QUOTES } from '../data/quotes' +import { useTypingEngine } from '../hooks/useTypingEngine' +import { TypingArea } from './TypingArea' +import { Keyboard } from './Keyboard' +import { ResultsModal } from './ResultsModal' + +type Props = { + onComplete: (wpm: number, accuracy: number, keyStats: Record) => void +} + +function randomQuote() { + return QUOTES[Math.floor(Math.random() * QUOTES.length)] +} + +export function FreeMode({ onComplete }: Props) { + const [quote, setQuote] = useState(randomQuote) + const [result, setResult] = useState<{ wpm: number; accuracy: number } | null>(null) + + const handleComplete = useCallback((r: { wpm: number; accuracy: number; keyStats: Record }) => { + setResult(r) + onComplete(r.wpm, r.accuracy, r.keyStats) + }, [onComplete]) + + const engine = useTypingEngine(quote, handleComplete) + + const handleRetry = () => { + setResult(null) + engine.reset() + } + + const handleNew = () => { + const q = randomQuote() + setQuote(q) + setResult(null) + engine.reset(q) + } + + return ( +
+
+

Free Typing

+ +
+ + + +
+ +
+ + {result && ( + + )} +
+ ) +} diff --git a/src/components/GameMode.tsx b/src/components/GameMode.tsx new file mode 100644 index 0000000..9652789 --- /dev/null +++ b/src/components/GameMode.tsx @@ -0,0 +1,396 @@ +import { useState, useEffect, useRef, useCallback } from 'react' +import styles from '../styles/game.module.css' + +const WORD_POOL = [ + 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'her', + 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', + 'its', 'may', 'new', 'now', 'old', 'see', 'way', 'who', 'did', 'got', + 'let', 'say', 'she', 'too', 'use', 'big', 'cat', 'dog', 'fun', 'hat', + 'jump', 'kick', 'love', 'make', 'nice', 'open', 'play', 'quiz', 'run', + 'star', 'time', 'up', 'very', 'walk', 'xray', 'year', 'zero', 'code', + 'type', 'fast', 'hero', 'epic', 'cool', 'fire', 'bolt', 'zoom', 'dash', + 'pixel', 'blaze', 'storm', 'power', 'quest', 'magic', 'super', 'turbo', + 'cyber', 'ultra', 'royal', 'brave', 'swift', 'light', 'dream', 'shine', +] + +type FallingWord = { + id: number + text: string + x: number + y: number + speed: number + matched: number // how many chars matched +} + +type Explosion = { + id: number + text: string + x: number + y: number + points: number +} + +type Particle = { + id: number + x: number + y: number + tx: number + ty: number + color: string + duration: number +} + +const PARTICLE_COLORS = ['#2ecc71', '#27ae60', '#7c6ff7', '#f1c40f', '#e84393', '#3498db', '#00d2d3'] + +function spawnParticles(x: number, y: number, count: number): Particle[] { + return Array.from({ length: count }, (_, i) => { + const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5 + const dist = 40 + Math.random() * 80 + return { + id: Date.now() + i, + x, + y, + tx: Math.cos(angle) * dist, + ty: Math.sin(angle) * dist, + color: PARTICLE_COLORS[Math.floor(Math.random() * PARTICLE_COLORS.length)], + duration: 0.4 + Math.random() * 0.3, + } + }) +} + +type Props = { + highScore: number + onGameOver: (score: number) => void +} + +export function GameMode({ highScore, onGameOver }: Props) { + const [words, setWords] = useState([]) + const [input, setInput] = useState('') + const inputRef = useRef('') + const [score, setScore] = useState(0) + const [lives, setLives] = useState(3) + const [gameOver, setGameOver] = useState(false) + const [started, setStarted] = useState(false) + const [focused, setFocused] = useState(false) + const [explosions, setExplosions] = useState([]) + const [particles, setParticles] = useState([]) + const [flash, setFlash] = useState(0) + const [combo, setCombo] = useState(0) + const [showCombo, setShowCombo] = useState(0) + const comboTimerRef = useRef>(undefined) + const containerRef = useRef(null) + const nextIdRef = useRef(0) + const frameRef = useRef(0) + const lastSpawnRef = useRef(0) + const difficultyRef = useRef(1) + const livesRef = useRef(3) + const gameOverRef = useRef(false) + + livesRef.current = lives + gameOverRef.current = gameOver + + const spawnWord = useCallback(() => { + const difficulty = difficultyRef.current + const maxLen = Math.min(3 + Math.floor(difficulty / 3), 7) + const pool = WORD_POOL.filter(w => w.length <= maxLen) + const text = pool[Math.floor(Math.random() * pool.length)] + const word: FallingWord = { + id: nextIdRef.current++, + text, + x: Math.random() * 70 + 5, // 5%-75% from left + y: -5, + speed: 0.3 + difficulty * 0.08, + matched: 0, + } + setWords(prev => [...prev, word]) + }, []) + + const startGame = () => { + setWords([]) + updateInput('') + setScore(0) + setLives(3) + setGameOver(false) + setStarted(true) + setExplosions([]) + setParticles([]) + setCombo(0) + difficultyRef.current = 1 + lastSpawnRef.current = 0 + nextIdRef.current = 0 + containerRef.current?.focus() + } + + // Game loop + useEffect(() => { + if (!started || gameOver) return + + let lastTime = performance.now() + + const tick = (now: number) => { + if (gameOverRef.current) return + const dt = (now - lastTime) / 16.67 // normalize to ~60fps + lastTime = now + + // Spawn + const spawnInterval = Math.max(1500 - difficultyRef.current * 80, 600) + if (now - lastSpawnRef.current > spawnInterval) { + spawnWord() + lastSpawnRef.current = now + } + + // Move words + setWords(prev => { + const next: FallingWord[] = [] + let lostLife = false + for (const w of prev) { + const ny = w.y + w.speed * dt + if (ny > 100) { + lostLife = true + } else { + next.push({ ...w, y: ny }) + } + } + if (lostLife) { + setLives(l => { + const nl = l - 1 + if (nl <= 0) { + setGameOver(true) + } + return nl + }) + } + return next + }) + + // Increase difficulty + difficultyRef.current = 1 + score / 50 + + frameRef.current = requestAnimationFrame(tick) + } + + frameRef.current = requestAnimationFrame(tick) + return () => cancelAnimationFrame(frameRef.current) + }, [started, gameOver, spawnWord, score]) + + // Fire onGameOver + useEffect(() => { + if (gameOver && started) { + onGameOver(score) + } + }, [gameOver, started, score, onGameOver]) + + const updateInput = (val: string) => { + inputRef.current = val + setInput(val) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (gameOver) return + if (!started) { + startGame() + return + } + + if (e.key === 'Backspace') { + const newInput = inputRef.current.slice(0, -1) + updateInput(newInput) + setWords(prev => prev.map(w => ({ + ...w, + matched: w.text.startsWith(newInput) && newInput.length > 0 ? newInput.length : 0, + }))) + return + } + + if (e.key.length !== 1) return + e.preventDefault() + + const newInput = inputRef.current + e.key + + // Do matching + removal in one atomic setWords call + setWords(prev => { + const completed = prev.find(w => w.text === newInput) + if (completed) { + const points = completed.text.length * 10 + setScore(s => s + points) + updateInput('') + + // Spawn explosion at word position + const now = Date.now() + setExplosions(ex => [...ex, { + id: now, + text: completed.text, + x: completed.x, + y: completed.y, + points, + }]) + // Spawn particles + setParticles(p => [...p, ...spawnParticles(completed.x, completed.y, 12)]) + // Screen flash + setFlash(now) + // Combo tracking + setCombo(c => { + const next = c + 1 + if (next >= 3) setShowCombo(now) + return next + }) + clearTimeout(comboTimerRef.current) + comboTimerRef.current = setTimeout(() => setCombo(0), 2000) + + // Auto-clean effects after animation + setTimeout(() => { + setExplosions(ex => ex.filter(e => e.id !== now)) + }, 500) + setTimeout(() => { + setParticles(p => p.slice(-24)) + }, 800) + + return prev.filter(w => w.id !== completed.id) + } + // No completion — update partial matches + updateInput(newInput) + return prev.map(w => ({ + ...w, + matched: w.text.startsWith(newInput) ? newInput.length : 0, + })) + }) + } + + return ( +
+
+

Falling Words

+ + High Score: {highScore} + + {started && !gameOver && ( + + )} +
+ +
setFocused(true)} + onBlur={() => setFocused(false)} + > +
+
+ Score + {score} +
+
+ Lives + + {'❤️'.repeat(Math.max(0, lives))}{'🖤'.repeat(Math.max(0, 3 - lives))} + +
+
+ + {words.map(w => ( +
0 ? styles.wordMatched : ''}`} + style={{ left: `${w.x}%`, top: `${w.y}%` }} + > + {w.text.split('').map((ch, i) => ( + + {ch} + + ))} +
+ ))} + + {/* Exploding words */} + {explosions.map(ex => ( +
+ {ex.text} +
+ ))} + + {/* Particles */} + {particles.map(p => ( +
+ ))} + + {/* Score popups */} + {explosions.map(ex => ( +
+ +{ex.points} +
+ ))} + + {/* Screen flash */} + {flash > 0 &&
} + + {/* Combo text */} + {showCombo > 0 && combo >= 3 && ( +
+ {combo}x COMBO! +
+ )} + +
+ + {started && !gameOver && ( +
+ {input || type a word...} +
+ )} + + {!started && ( +
+
Falling Words
+
+ Type the words before they fall! +
+ +
+ )} + + {gameOver && ( +
+
Game Over!
+
Score: {score}
+ {score > highScore && score > 0 && ( +
New High Score!
+ )} +
+ +
+
+ )} +
+
+ ) +} diff --git a/src/components/Keyboard.tsx b/src/components/Keyboard.tsx new file mode 100644 index 0000000..d1db75f --- /dev/null +++ b/src/components/Keyboard.tsx @@ -0,0 +1,69 @@ +import { useState, useEffect } from 'react' +import { KEYBOARD_LAYOUT, FINGER_COLORS, FINGER_LABELS } from '../data/keyboard' +import type { KeyInfo } from '../types' +import styles from '../styles/keyboard.module.css' + +type Props = { + activeKey?: string + keyStats?: Record +} + +export function Keyboard({ activeKey, keyStats }: Props) { + const [pressedKey, setPressedKey] = useState(null) + + useEffect(() => { + const down = (e: KeyboardEvent) => setPressedKey(e.key.toLowerCase()) + const up = () => setPressedKey(null) + window.addEventListener('keydown', down) + window.addEventListener('keyup', up) + return () => { + window.removeEventListener('keydown', down) + window.removeEventListener('keyup', up) + } + }, []) + + const getKeyBg = (ki: KeyInfo) => { + const base = FINGER_COLORS[ki.finger] + if (keyStats) { + const stat = keyStats[ki.key] + if (stat) { + const total = stat.hits + stat.misses + if (total > 0) { + const acc = stat.hits / total + // Blend toward red for low accuracy + if (acc < 0.7) return `color-mix(in srgb, ${base}, var(--error) 40%)` + } + } + } + return base + } + + return ( +
+ {KEYBOARD_LAYOUT.map((row, ri) => ( +
+ {row.map(ki => { + const isActive = activeKey !== undefined && + ki.key === activeKey.toLowerCase() + const isPressed = pressedKey === ki.key + const label = ki.key === ' ' ? 'Space' : ki.key + + return ( +
+ {label} + {FINGER_LABELS[ki.finger]} +
+ ) + })} +
+ ))} +
+ ) +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..21a4745 --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,58 @@ +import { type ReactNode } from 'react' + +type Tab = 'lessons' | 'free' | 'game' | 'stats' + +type Props = { + activeTab: Tab + onTabChange: (tab: Tab) => void + children: ReactNode +} + +const TABS: { id: Tab; label: string; icon: string }[] = [ + { id: 'lessons', label: 'Lessons', icon: '📚' }, + { id: 'free', label: 'Free Type', icon: '⌨️' }, + { id: 'game', label: 'Game', icon: '🎮' }, + { id: 'stats', label: 'Stats', icon: '📊' }, +] + +export function Layout({ activeTab, onTabChange, children }: Props) { + return ( +
+
+

+ Leo's Typing Tutor +

+ +
+
{children}
+
+ ) +} diff --git a/src/components/LessonMode.tsx b/src/components/LessonMode.tsx new file mode 100644 index 0000000..e87fd79 --- /dev/null +++ b/src/components/LessonMode.tsx @@ -0,0 +1,97 @@ +import { useState, useCallback } from 'react' +import { LESSONS } from '../data/lessons' +import { useTypingEngine } from '../hooks/useTypingEngine' +import type { UserProgress } from '../types' +import { LessonSelect } from './LessonSelect' +import { TypingArea } from './TypingArea' +import { Keyboard } from './Keyboard' +import { ResultsModal } from './ResultsModal' + +type Props = { + progress: UserProgress + onComplete: (lessonId: number, wpm: number, accuracy: number, keyStats: Record) => void +} + +export function LessonMode({ progress, onComplete }: Props) { + const [activeLessonId, setActiveLessonId] = useState(null) + const [result, setResult] = useState<{ wpm: number; accuracy: number } | null>(null) + + const lesson = activeLessonId ? LESSONS.find(l => l.id === activeLessonId) : null + const text = lesson ? lesson.words.join(' ') : '' + + const handleComplete = useCallback((r: { wpm: number; accuracy: number; keyStats: Record }) => { + setResult(r) + if (activeLessonId) { + onComplete(activeLessonId, r.wpm, r.accuracy, r.keyStats) + } + }, [activeLessonId, onComplete]) + + const engine = useTypingEngine(text, handleComplete) + + const handleSelect = (id: number) => { + const l = LESSONS.find(l => l.id === id) + if (!l) return + setActiveLessonId(id) + setResult(null) + engine.reset(l.words.join(' ')) + } + + const handleRetry = () => { + setResult(null) + engine.reset() + } + + const handleBack = () => { + setActiveLessonId(null) + setResult(null) + } + + if (!lesson) { + return + } + + // Find next lesson name for unlock message + const nextLesson = LESSONS.find(l => l.unlockAfter === lesson.id) + const unlockText = result && result.accuracy >= 90 && nextLesson + ? nextLesson.name + : undefined + + return ( +
+
+ +
+

{lesson.name}

+ {lesson.newKeys.length > 0 && ( + + New keys: {lesson.newKeys.join(' ')} + + )} +
+
+ + + +
+ +
+ + {result && ( + + )} +
+ ) +} diff --git a/src/components/LessonSelect.tsx b/src/components/LessonSelect.tsx new file mode 100644 index 0000000..2754c13 --- /dev/null +++ b/src/components/LessonSelect.tsx @@ -0,0 +1,52 @@ +import { LESSONS } from '../data/lessons' +import type { UserProgress } from '../types' +import styles from '../styles/typing.module.css' + +type Props = { + progress: UserProgress + onSelect: (lessonId: number) => void +} + +export function LessonSelect({ progress, onSelect }: Props) { + const isUnlocked = (lessonId: number) => { + const lesson = LESSONS.find(l => l.id === lessonId) + if (!lesson?.unlockAfter) return true + return progress.completedLessons.includes(lesson.unlockAfter) + } + + return ( +
+
+

Lessons

+

+ Master each lesson to unlock the next ({progress.completedLessons.length}/{LESSONS.length} completed) +

+
+
+ {LESSONS.map(lesson => { + const unlocked = isUnlocked(lesson.id) + const completed = progress.completedLessons.includes(lesson.id) + + return ( +
unlocked && onSelect(lesson.id)} + > +
Lesson {lesson.id}
+
{lesson.name}
+ {lesson.newKeys.length > 0 && ( +
+ New: {lesson.newKeys.join(' ')} +
+ )} +
+ {completed ? 'Completed' : unlocked ? 'Ready' : 'Locked'} +
+
+ ) + })} +
+
+ ) +} diff --git a/src/components/ResultsModal.tsx b/src/components/ResultsModal.tsx new file mode 100644 index 0000000..60ff4a0 --- /dev/null +++ b/src/components/ResultsModal.tsx @@ -0,0 +1,45 @@ +import styles from '../styles/typing.module.css' + +type Props = { + wpm: number + accuracy: number + unlocked?: string + onRetry: () => void + onBack: () => void +} + +export function ResultsModal({ wpm, accuracy, unlocked, onRetry, onBack }: Props) { + return ( +
+
e.stopPropagation()}> +
+ {accuracy >= 90 ? 'Great Job!' : 'Keep Practicing!'} +
+
+
+ {wpm} + WPM +
+
+ {accuracy}% + Accuracy +
+
+ {unlocked && ( +
+ Unlocked: {unlocked} +
+ )} + {accuracy < 90 && ( +
+ Need 90% accuracy to unlock the next lesson +
+ )} +
+ + +
+
+
+ ) +} diff --git a/src/components/StatsView.tsx b/src/components/StatsView.tsx new file mode 100644 index 0000000..2b419cf --- /dev/null +++ b/src/components/StatsView.tsx @@ -0,0 +1,164 @@ +import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts' +import { LESSONS } from '../data/lessons' +import { KEYBOARD_LAYOUT, FINGER_COLORS } from '../data/keyboard' +import type { UserProgress } from '../types' +import styles from '../styles/game.module.css' +import kbStyles from '../styles/keyboard.module.css' + +type Props = { + progress: UserProgress +} + +export function StatsView({ progress }: Props) { + const { sessions, completedLessons, gameHighScore } = progress + + // Build chart data from sessions (last 50) + const recentSessions = sessions.slice(-50) + const chartData = recentSessions.map((s, i) => ({ + session: i + 1, + wpm: s.wpm, + accuracy: s.accuracy, + })) + + // Aggregate key stats across all sessions + const aggKeyStats: Record = {} + for (const s of sessions) { + for (const [key, stat] of Object.entries(s.keyStats)) { + if (!aggKeyStats[key]) aggKeyStats[key] = { hits: 0, misses: 0 } + aggKeyStats[key].hits += stat.hits + aggKeyStats[key].misses += stat.misses + } + } + + const lessonProgress = Math.round((completedLessons.length / LESSONS.length) * 100) + + const avgWpm = sessions.length > 0 + ? Math.round(sessions.reduce((s, r) => s + r.wpm, 0) / sessions.length) + : 0 + const avgAccuracy = sessions.length > 0 + ? Math.round(sessions.reduce((s, r) => s + r.accuracy, 0) / sessions.length) + : 0 + + return ( +
+

Stats & Progress

+ +
+ {/* Summary */} +
+
Overview
+
+
+
{avgWpm}
+
Avg WPM
+
+
+
{avgAccuracy}%
+
Avg Accuracy
+
+
+
{sessions.length}
+
Sessions
+
+
+
{gameHighScore}
+
Game High Score
+
+
+
+ + {/* Lesson Progress */} +
+
Lesson Progress
+
+ {completedLessons.length} / {LESSONS.length} +
+
+
+
+
+ + {/* WPM Chart */} +
+
WPM Over Time
+ {chartData.length > 0 ? ( + + + + + + + + + + ) : ( +
+ Complete some sessions to see your progress! +
+ )} +
+ + {/* Accuracy Chart */} +
+
Accuracy Over Time
+ {chartData.length > 0 ? ( + + + + + + + + + + ) : ( +
+ Complete some sessions to see your progress! +
+ )} +
+ + {/* Key Accuracy Heatmap */} +
+
Key Accuracy Heatmap
+
+ {KEYBOARD_LAYOUT.map((row, ri) => ( +
+ {row.map(ki => { + const stat = aggKeyStats[ki.key] + let bg = FINGER_COLORS[ki.finger] + let opacity = 0.3 + if (stat) { + const total = stat.hits + stat.misses + if (total > 0) { + const acc = stat.hits / total + opacity = 0.4 + acc * 0.6 + if (acc < 0.7) bg = 'var(--error)' + else if (acc < 0.9) bg = 'var(--warning)' + else bg = 'var(--success)' + } + } + const label = ki.key === ' ' ? 'Space' : ki.key + return ( +
+ {label} +
+ ) + })} +
+ ))} +
+
+
+
+ ) +} diff --git a/src/components/TypingArea.tsx b/src/components/TypingArea.tsx new file mode 100644 index 0000000..238c9bd --- /dev/null +++ b/src/components/TypingArea.tsx @@ -0,0 +1,57 @@ +import { useRef, useState, useEffect } from 'react' +import styles from '../styles/typing.module.css' + +type Props = { + text: string + currentIndex: number + errors: Set + wpm: number + accuracy: number + onKeyDown: (e: React.KeyboardEvent) => void +} + +export function TypingArea({ text, currentIndex, errors, wpm, accuracy, onKeyDown }: Props) { + const ref = useRef(null) + const [focused, setFocused] = useState(false) + + useEffect(() => { + ref.current?.focus() + }, []) + + return ( +
+
+
+ {wpm} + WPM +
+
+ {accuracy}% + Accuracy +
+
+
setFocused(true)} + onBlur={() => setFocused(false)} + > + {text.split('').map((char, i) => { + let cls = styles.pending + if (i < currentIndex) { + cls = errors.has(i) ? styles.incorrect : styles.correct + } else if (i === currentIndex) { + cls = styles.current + } + return ( + + {char} + + ) + })} +
+
+ ) +} diff --git a/src/data/keyboard.ts b/src/data/keyboard.ts new file mode 100644 index 0000000..c787f81 --- /dev/null +++ b/src/data/keyboard.ts @@ -0,0 +1,100 @@ +import type { Finger, KeyInfo } from '../types' + +export const FINGER_COLORS: Record = { + lpinky: '#e74c3c', + lring: '#e67e22', + lmiddle: '#f1c40f', + lindex: '#2ecc71', + rindex: '#27ae60', + rmiddle: '#3498db', + rring: '#9b59b6', + rpinky: '#e84393', + thumb: '#95a5a6', +} + +export const FINGER_LABELS: Record = { + lpinky: 'Left Pinky', + lring: 'Left Ring', + lmiddle: 'Left Middle', + lindex: 'Left Index', + rindex: 'Right Index', + rmiddle: 'Right Middle', + rring: 'Right Ring', + rpinky: 'Right Pinky', + thumb: 'Thumb', +} + +// Row 0 = number row, Row 1 = top, Row 2 = home, Row 3 = bottom, Row 4 = space +export const KEYBOARD_LAYOUT: KeyInfo[][] = [ + // Number row + [ + { key: '`', finger: 'lpinky', row: 0, col: 0 }, + { key: '1', finger: 'lpinky', row: 0, col: 1 }, + { key: '2', finger: 'lring', row: 0, col: 2 }, + { key: '3', finger: 'lmiddle', row: 0, col: 3 }, + { key: '4', finger: 'lindex', row: 0, col: 4 }, + { key: '5', finger: 'lindex', row: 0, col: 5 }, + { key: '6', finger: 'rindex', row: 0, col: 6 }, + { key: '7', finger: 'rindex', row: 0, col: 7 }, + { key: '8', finger: 'rmiddle', row: 0, col: 8 }, + { key: '9', finger: 'rring', row: 0, col: 9 }, + { key: '0', finger: 'rpinky', row: 0, col: 10 }, + { key: '-', finger: 'rpinky', row: 0, col: 11 }, + { key: '=', finger: 'rpinky', row: 0, col: 12 }, + ], + // Top row + [ + { key: 'q', finger: 'lpinky', row: 1, col: 0 }, + { key: 'w', finger: 'lring', row: 1, col: 1 }, + { key: 'e', finger: 'lmiddle', row: 1, col: 2 }, + { key: 'r', finger: 'lindex', row: 1, col: 3 }, + { key: 't', finger: 'lindex', row: 1, col: 4 }, + { key: 'y', finger: 'rindex', row: 1, col: 5 }, + { key: 'u', finger: 'rindex', row: 1, col: 6 }, + { key: 'i', finger: 'rmiddle', row: 1, col: 7 }, + { key: 'o', finger: 'rring', row: 1, col: 8 }, + { key: 'p', finger: 'rpinky', row: 1, col: 9 }, + { key: '[', finger: 'rpinky', row: 1, col: 10 }, + { key: ']', finger: 'rpinky', row: 1, col: 11 }, + { key: '\\', finger: 'rpinky', row: 1, col: 12 }, + ], + // Home row + [ + { key: 'a', finger: 'lpinky', row: 2, col: 0 }, + { key: 's', finger: 'lring', row: 2, col: 1 }, + { key: 'd', finger: 'lmiddle', row: 2, col: 2 }, + { key: 'f', finger: 'lindex', row: 2, col: 3 }, + { key: 'g', finger: 'lindex', row: 2, col: 4 }, + { key: 'h', finger: 'rindex', row: 2, col: 5 }, + { key: 'j', finger: 'rindex', row: 2, col: 6 }, + { key: 'k', finger: 'rmiddle', row: 2, col: 7 }, + { key: 'l', finger: 'rring', row: 2, col: 8 }, + { key: ';', finger: 'rpinky', row: 2, col: 9 }, + { key: "'", finger: 'rpinky', row: 2, col: 10 }, + ], + // Bottom row + [ + { key: 'z', finger: 'lpinky', row: 3, col: 0 }, + { key: 'x', finger: 'lring', row: 3, col: 1 }, + { key: 'c', finger: 'lmiddle', row: 3, col: 2 }, + { key: 'v', finger: 'lindex', row: 3, col: 3 }, + { key: 'b', finger: 'lindex', row: 3, col: 4 }, + { key: 'n', finger: 'rindex', row: 3, col: 5 }, + { key: 'm', finger: 'rmiddle', row: 3, col: 6 }, + { key: ',', finger: 'rmiddle', row: 3, col: 7 }, + { key: '.', finger: 'rring', row: 3, col: 8 }, + { key: '/', finger: 'rpinky', row: 3, col: 9 }, + ], + // Space row + [ + { key: ' ', finger: 'thumb', row: 4, col: 0 }, + ], +] + +// Flat lookup: key → KeyInfo +export const KEY_MAP: Record = {} +for (const row of KEYBOARD_LAYOUT) { + for (const ki of row) { + KEY_MAP[ki.key] = ki + } +} diff --git a/src/data/lessons.ts b/src/data/lessons.ts new file mode 100644 index 0000000..3d90851 --- /dev/null +++ b/src/data/lessons.ts @@ -0,0 +1,171 @@ +import type { Lesson } from '../types' + +export const LESSONS: Lesson[] = [ + { + id: 1, + name: 'Home Row: Left Hand', + newKeys: ['a', 's', 'd', 'f'], + words: [ + 'add', 'sad', 'dad', 'fad', 'ads', 'fas', 'das', + 'sass', 'fads', 'adds', 'dads', 'asdf', 'fdsa', + 'aff', 'sadf', 'dafs', 'fads', 'sass', 'adds', + ], + }, + { + id: 2, + name: 'Home Row: Right Hand', + newKeys: ['j', 'k', 'l', ';'], + unlockAfter: 1, + words: [ + 'all', 'fall', 'jail', 'kale', 'lake', 'fake', + 'flask', 'falls', 'jacks', 'slak', 'flak', 'alf', + 'lads', 'salad', 'flask', 'shall', 'djall', 'alks', + ], + }, + { + id: 3, + name: 'Home Row: G and H', + newKeys: ['g', 'h'], + unlockAfter: 2, + words: [ + 'had', 'has', 'gal', 'lag', 'hag', 'gash', 'lash', + 'half', 'glad', 'flash', 'gag', 'hash', 'sash', + 'shall', 'shag', 'ghalf', 'flags', 'glass', 'halls', + ], + }, + { + id: 4, + name: 'Top Row: Left Hand', + newKeys: ['q', 'w', 'e', 'r', 't'], + unlockAfter: 3, + words: [ + 'the', 'were', 'tree', 'read', 'tear', 'rest', + 'west', 'test', 'quest', 'wheat', 'street', 'great', + 'stew', 'tread', 'sweat', 'wreath', 'wet', 'set', + ], + }, + { + id: 5, + name: 'Top Row: Right Hand', + newKeys: ['y', 'u', 'i', 'o', 'p'], + unlockAfter: 4, + words: [ + 'you', 'your', 'type', 'youthful', 'quiet', 'equip', + 'ripe', 'trip', 'pour', 'our', 'proud', 'youth', + 'quite', 'quote', 'opaque', 'tulip', 'quip', 'outpour', + ], + }, + { + id: 6, + name: 'Home + Top Row Practice', + newKeys: [], + unlockAfter: 5, + words: [ + 'the quick', 'just right', 'play together', 'light house', + 'quiet whisper', 'write daily', 'type faster', 'your style', + 'keep it up', 'forge ahead', 'super power', 'world hero', + ], + }, + { + id: 7, + name: 'Bottom Row: Left Hand', + newKeys: ['z', 'x', 'c', 'v', 'b'], + unlockAfter: 6, + words: [ + 'box', 'vex', 'cab', 'back', 'cave', 'brave', + 'exact', 'verb', 'crisp', 'black', 'beach', 'voice', + 'brace', 'civic', 'blaze', 'crux', 'vibe', 'cube', + ], + }, + { + id: 8, + name: 'Bottom Row: Right Hand', + newKeys: ['n', 'm', ',', '.', '/'], + unlockAfter: 7, + words: [ + 'man', 'name', 'mine', 'moon', 'noon', 'main', + 'manner', 'common', 'command', 'mention', 'moment', + 'minion', 'melon', 'nominate', 'minimum', 'memo', + ], + }, + { + id: 9, + name: 'Full Keyboard Practice', + newKeys: [], + unlockAfter: 8, + words: [ + 'the lazy fox', 'jumped over', 'brown quick', 'from every angle', + 'maximize your', 'best typing', 'skills now', 'extra credit', + 'puzzle solved', 'victory dance', 'zero mistakes', 'nice job', + ], + }, + { + id: 10, + name: 'Capital Letters', + newKeys: ['Shift'], + unlockAfter: 9, + words: [ + 'Hello World', 'Good Morning', 'Dear Friend', 'New York', + 'San Diego', 'Leo Rules', 'Amazing Job', 'Keep Going', + 'Great Work', 'High Five', 'The Best', 'Super Star', + ], + }, + { + id: 11, + name: 'Numbers Row', + newKeys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], + unlockAfter: 10, + words: [ + '123', '456', '789', '100 cats', '250 dogs', + '42 is the answer', '365 days', '12 months', + '7 wonders', '50 states', '3 wishes', '99 stars', + ], + }, + { + id: 12, + name: 'Common Punctuation', + newKeys: ['.', ',', '!', '?', "'"], + unlockAfter: 11, + words: [ + "Hello, world!", "What's up?", "I can't wait!", + "Great job, Leo!", "Ready? Set. Go!", "Yes, please.", + "Wow, that's cool!", "Don't stop now!", + "Nice work, champ!", "Is it fun? Yes!", + ], + }, + { + id: 13, + name: 'Special Symbols', + newKeys: ['-', '=', '[', ']', '/', '\\'], + unlockAfter: 12, + words: [ + 'a-b-c', 'x=y', '[hello]', 'left/right', 'win-win', + 'self-made', 'all=equal', '[done]', 'yes/no', 'up-down', + 'hi-five', 're=set', '[start]', 'and/or', 'check-in', + ], + }, + { + id: 14, + name: 'Speed Challenge: Common Words', + newKeys: [], + unlockAfter: 13, + words: [ + 'the and for are but not you all any can had her', + 'was one our out day get has him his how its may', + 'new now old see way who did got let say she too use', + ], + }, + { + id: 15, + name: 'Speed Challenge: Sentences', + newKeys: [], + unlockAfter: 14, + words: [ + 'The quick brown fox jumps over the lazy dog.', + 'Pack my box with five dozen liquor jugs.', + 'How vexingly quick daft zebras jump!', + 'The five boxing wizards jump quickly.', + 'Sphinx of black quartz, judge my vow.', + ], + }, +] diff --git a/src/data/quotes.ts b/src/data/quotes.ts new file mode 100644 index 0000000..561dc42 --- /dev/null +++ b/src/data/quotes.ts @@ -0,0 +1,37 @@ +export const QUOTES: string[] = [ + "The only way to do great work is to love what you do.", + "In the middle of every difficulty lies opportunity.", + "It does not matter how slowly you go as long as you do not stop.", + "The future belongs to those who believe in the beauty of their dreams.", + "Believe you can and you are halfway there.", + "The best time to plant a tree was twenty years ago. The second best time is now.", + "You miss one hundred percent of the shots you never take.", + "Be yourself; everyone else is already taken.", + "Two things are infinite: the universe and human stupidity.", + "A room without books is like a body without a soul.", + "You only live once, but if you do it right, once is enough.", + "Be the change that you wish to see in the world.", + "In three words I can sum up everything I learned about life: it goes on.", + "If you tell the truth, you do not have to remember anything.", + "Life is what happens when you are busy making other plans.", + "To be yourself in a world that is constantly trying to make you something else is the greatest accomplishment.", + "The secret of getting ahead is getting started.", + "It is never too late to be what you might have been.", + "Everything you can imagine is real.", + "Do what you can, with what you have, where you are.", + "The journey of a thousand miles begins with a single step.", + "Not all those who wander are lost.", + "What we think, we become.", + "The only impossible journey is the one you never begin.", + "Turn your wounds into wisdom.", + "Stars cannot shine without darkness.", + "Dream big and dare to fail.", + "Happiness is not something ready made. It comes from your own actions.", + "The best revenge is massive success.", + "Every moment is a fresh beginning.", + "If opportunity does not knock, build a door.", + "Nothing is impossible. The word itself says I am possible.", + "Keep your face always toward the sunshine and shadows will fall behind you.", + "What you do today can improve all your tomorrows.", + "Quality is not an act, it is a habit.", +] diff --git a/src/hooks/useStats.ts b/src/hooks/useStats.ts new file mode 100644 index 0000000..4fe5e38 --- /dev/null +++ b/src/hooks/useStats.ts @@ -0,0 +1,68 @@ +import { useState, useCallback } from 'react' +import type { UserProgress, SessionResult } from '../types' + +const STORAGE_KEY = 'leo-typing-progress' + +const DEFAULT_PROGRESS: UserProgress = { + completedLessons: [], + sessions: [], + gameHighScore: 0, +} + +function loadProgress(): UserProgress { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return DEFAULT_PROGRESS + return JSON.parse(raw) + } catch { + return DEFAULT_PROGRESS + } +} + +function saveProgress(p: UserProgress) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(p)) +} + +export function useStats() { + const [progress, setProgress] = useState(loadProgress) + + const completeLesson = useCallback((lessonId: number) => { + setProgress(prev => { + const next = { + ...prev, + completedLessons: prev.completedLessons.includes(lessonId) + ? prev.completedLessons + : [...prev.completedLessons, lessonId], + } + saveProgress(next) + return next + }) + }, []) + + const addSession = useCallback((result: SessionResult) => { + setProgress(prev => { + const next = { + ...prev, + sessions: [...prev.sessions, result], + } + saveProgress(next) + return next + }) + }, []) + + const updateHighScore = useCallback((score: number) => { + setProgress(prev => { + if (score <= prev.gameHighScore) return prev + const next = { ...prev, gameHighScore: score } + saveProgress(next) + return next + }) + }, []) + + const resetProgress = useCallback(() => { + setProgress(DEFAULT_PROGRESS) + saveProgress(DEFAULT_PROGRESS) + }, []) + + return { progress, completeLesson, addSession, updateHighScore, resetProgress } +} diff --git a/src/hooks/useTypingEngine.ts b/src/hooks/useTypingEngine.ts new file mode 100644 index 0000000..02ba3fa --- /dev/null +++ b/src/hooks/useTypingEngine.ts @@ -0,0 +1,122 @@ +import { useState, useCallback, useRef, useEffect } from 'react' + +export type TypingEngineResult = { + currentIndex: number + errors: Set + wpm: number + accuracy: number + isComplete: boolean + keyStats: Record + handleKeyDown: (e: React.KeyboardEvent) => void + reset: (newText?: string) => void +} + +export function useTypingEngine( + initialText: string, + onComplete?: (result: { wpm: number; accuracy: number; keyStats: Record }) => void, +): TypingEngineResult { + const [text, setText] = useState(initialText) + const [currentIndex, setCurrentIndex] = useState(0) + const [errors, setErrors] = useState>(new Set()) + const [totalKeystrokes, setTotalKeystrokes] = useState(0) + const [correctKeystrokes, setCorrectKeystrokes] = useState(0) + const [isComplete, setIsComplete] = useState(false) + const [keyStats, setKeyStats] = useState>({}) + + const startTimeRef = useRef(null) + const [wpm, setWpm] = useState(0) + const onCompleteRef = useRef(onComplete) + onCompleteRef.current = onComplete + + const accuracy = totalKeystrokes > 0 ? Math.round((correctKeystrokes / totalKeystrokes) * 100) : 100 + + // Update WPM periodically + useEffect(() => { + if (isComplete || currentIndex === 0) return + const interval = setInterval(() => { + if (startTimeRef.current) { + const minutes = (Date.now() - startTimeRef.current) / 60000 + if (minutes > 0) { + setWpm(Math.round((currentIndex / 5) / minutes)) + } + } + }, 500) + return () => clearInterval(interval) + }, [currentIndex, isComplete]) + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (isComplete) return + // Ignore modifier keys, function keys, etc. + if (e.key.length > 1 && e.key !== 'Backspace') return + e.preventDefault() + + if (e.key === 'Backspace') { + setCurrentIndex(prev => Math.max(0, prev - 1)) + setErrors(prev => { + const next = new Set(prev) + next.delete(currentIndex - 1) + return next + }) + return + } + + if (!startTimeRef.current) { + startTimeRef.current = Date.now() + } + + const expected = text[currentIndex] + const typed = e.key + const charKey = expected?.toLowerCase() ?? typed.toLowerCase() + + setTotalKeystrokes(prev => prev + 1) + + if (typed === expected) { + setCorrectKeystrokes(prev => prev + 1) + setKeyStats(prev => ({ + ...prev, + [charKey]: { hits: (prev[charKey]?.hits ?? 0) + 1, misses: prev[charKey]?.misses ?? 0 }, + })) + const nextIndex = currentIndex + 1 + setCurrentIndex(nextIndex) + + if (nextIndex >= text.length) { + setIsComplete(true) + const minutes = (Date.now() - (startTimeRef.current ?? Date.now())) / 60000 + const finalWpm = minutes > 0 ? Math.round((text.length / 5) / minutes) : 0 + setWpm(finalWpm) + const finalAccuracy = (totalKeystrokes + 1) > 0 + ? Math.round(((correctKeystrokes + 1) / (totalKeystrokes + 1)) * 100) + : 100 + // Build final keyStats including this last keystroke + setKeyStats(prev => { + const final = { + ...prev, + [charKey]: { hits: (prev[charKey]?.hits ?? 0) + 1, misses: prev[charKey]?.misses ?? 0 }, + } + onCompleteRef.current?.({ wpm: finalWpm, accuracy: finalAccuracy, keyStats: final }) + return prev // Don't double-count, already set above + }) + } + } else { + setErrors(prev => new Set(prev).add(currentIndex)) + setKeyStats(prev => ({ + ...prev, + [charKey]: { hits: prev[charKey]?.hits ?? 0, misses: (prev[charKey]?.misses ?? 0) + 1 }, + })) + } + }, [currentIndex, text, isComplete, totalKeystrokes, correctKeystrokes]) + + const reset = useCallback((newText?: string) => { + if (newText !== undefined) setText(newText) + setCurrentIndex(0) + setErrors(new Set()) + setTotalKeystrokes(0) + setCorrectKeystrokes(0) + setIsComplete(false) + setKeyStats({}) + setWpm(0) + startTimeRef.current = null + }, []) + + return { currentIndex, errors, wpm, accuracy, isComplete, keyStats, handleKeyDown, reset } +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..b5d9579 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './styles/global.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/styles/game.module.css b/src/styles/game.module.css new file mode 100644 index 0000000..54fdd26 --- /dev/null +++ b/src/styles/game.module.css @@ -0,0 +1,310 @@ +.gameContainer { + position: relative; + background: var(--bg-card); + border-radius: var(--radius); + height: 500px; + overflow: hidden; + outline: none; + border: 2px solid transparent; + transition: border-color 0.2s; +} + +.gameContainer:focus { + border-color: var(--accent); +} + +.gameContainer.blurred::after { + content: 'Click to play!'; + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(26, 27, 46, 0.8); + border-radius: var(--radius); + color: var(--text-dim); + font-size: 18px; + z-index: 5; +} + +.gameHud { + position: absolute; + top: 0; + left: 0; + right: 0; + display: flex; + justify-content: space-between; + padding: 16px 20px; + background: linear-gradient(to bottom, rgba(26, 27, 46, 0.9), transparent); + z-index: 2; + font-family: var(--font-mono); +} + +.hudItem { + display: flex; + flex-direction: column; + align-items: center; +} + +.hudLabel { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--text-dim); +} + +.hudValue { + font-size: 22px; + font-weight: 700; + color: var(--accent); +} + +.dangerZone { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 4px; + background: var(--error); + opacity: 0.6; +} + +.word { + position: absolute; + font-family: var(--font-mono); + font-size: 20px; + font-weight: 600; + padding: 6px 14px; + border-radius: 8px; + background: var(--bg-hover); + border: 2px solid var(--accent); + color: var(--text); + transition: transform 0.05s linear; + white-space: nowrap; +} + +.wordMatched { + border-color: var(--success); +} + +.wordMatchedChar { + color: var(--success); +} + +.inputDisplay { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background: var(--bg); + padding: 10px 24px; + border-radius: var(--radius); + font-family: var(--font-mono); + font-size: 20px; + color: var(--accent); + border: 2px solid var(--accent); + min-width: 200px; + text-align: center; + z-index: 3; +} + +.lives { + font-size: 20px; + letter-spacing: 4px; +} + +.gameOver { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(26, 27, 46, 0.95); + z-index: 10; + animation: fadeIn 0.3s ease; +} + +.gameOverTitle { + font-size: 36px; + font-weight: 700; + margin-bottom: 16px; +} + +.gameOverScore { + font-size: 20px; + color: var(--text-dim); + margin-bottom: 8px; +} + +.highScore { + color: var(--warning); + font-weight: 600; +} + +.gameOverButtons { + display: flex; + gap: 12px; + margin-top: 24px; +} + +.statsGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; +} + +.statsCard { + background: var(--bg-card); + border-radius: var(--radius); + padding: 24px; +} + +.statsCardTitle { + font-size: 14px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--text-dim); + margin-bottom: 16px; +} + +.progressBar { + height: 12px; + background: var(--bg); + border-radius: 6px; + overflow: hidden; +} + +.progressFill { + height: 100%; + background: linear-gradient(90deg, var(--accent), var(--success)); + border-radius: 6px; + transition: width 0.5s ease; +} + +/* === Word destroy effects === */ + +.wordExploding { + animation: wordExplode 0.45s ease-out forwards; + pointer-events: none; + border-color: var(--success) !important; + background: var(--success) !important; + color: #fff !important; +} + +@keyframes wordExplode { + 0% { + transform: scale(1); + opacity: 1; + filter: brightness(1); + } + 30% { + transform: scale(1.3); + opacity: 1; + filter: brightness(2); + } + 100% { + transform: scale(0.2); + opacity: 0; + filter: brightness(3) blur(4px); + } +} + +.particle { + position: absolute; + width: 8px; + height: 8px; + border-radius: 50%; + pointer-events: none; + animation: particleFly var(--duration) ease-out forwards; + z-index: 20; +} + +@keyframes particleFly { + 0% { + transform: translate(0, 0) scale(1); + opacity: 1; + } + 100% { + transform: translate(var(--tx), var(--ty)) scale(0); + opacity: 0; + } +} + +.scorePopup { + position: absolute; + font-family: var(--font-mono); + font-size: 24px; + font-weight: 800; + color: var(--success); + pointer-events: none; + z-index: 20; + text-shadow: 0 0 12px var(--success), 0 0 24px rgba(46, 204, 113, 0.4); + animation: scoreFloat 0.8s ease-out forwards; +} + +@keyframes scoreFloat { + 0% { + transform: translateY(0) scale(0.5); + opacity: 0; + } + 20% { + transform: translateY(-10px) scale(1.2); + opacity: 1; + } + 100% { + transform: translateY(-60px) scale(0.8); + opacity: 0; + } +} + +.screenFlash { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 15; + animation: flash 0.3s ease-out forwards; + border-radius: var(--radius); +} + +@keyframes flash { + 0% { + background: rgba(46, 204, 113, 0.2); + } + 100% { + background: transparent; + } +} + +.comboText { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-family: var(--font-mono); + font-size: 48px; + font-weight: 800; + pointer-events: none; + z-index: 20; + animation: comboPop 0.6s ease-out forwards; + background: linear-gradient(135deg, var(--accent), #e84393, var(--warning)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: none; + filter: drop-shadow(0 0 20px var(--accent)); +} + +@keyframes comboPop { + 0% { + transform: translate(-50%, -50%) scale(0.3) rotate(-10deg); + opacity: 0; + } + 30% { + transform: translate(-50%, -50%) scale(1.3) rotate(3deg); + opacity: 1; + } + 100% { + transform: translate(-50%, -50%) scale(0.8) rotate(0deg); + opacity: 0; + } +} diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..92a58a0 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,69 @@ +:root { + --bg: #1a1b2e; + --bg-card: #242640; + --bg-hover: #2d2f52; + --text: #e8e8f0; + --text-dim: #8888a8; + --accent: #7c6ff7; + --accent-glow: rgba(124, 111, 247, 0.4); + --success: #2ecc71; + --error: #e74c3c; + --warning: #f1c40f; + --radius: 12px; + --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--bg); + color: var(--text); + min-height: 100vh; + overflow-x: hidden; +} + +#root { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +button { + font-family: inherit; + cursor: pointer; + border: none; + background: var(--bg-card); + color: var(--text); + padding: 10px 20px; + border-radius: var(--radius); + font-size: 14px; + transition: all 0.15s ease; +} + +button:hover { + background: var(--bg-hover); + transform: translateY(-1px); +} + +button:active { + transform: translateY(0); +} + +@keyframes pulse { + 0%, 100% { box-shadow: 0 0 8px var(--accent-glow); } + 50% { box-shadow: 0 0 20px var(--accent-glow), 0 0 40px var(--accent-glow); } +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.fade-in { + animation: fadeIn 0.3s ease; +} diff --git a/src/styles/keyboard.module.css b/src/styles/keyboard.module.css new file mode 100644 index 0000000..7f7c7ac --- /dev/null +++ b/src/styles/keyboard.module.css @@ -0,0 +1,71 @@ +.keyboard { + display: flex; + flex-direction: column; + gap: 6px; + padding: 20px; + background: var(--bg-card); + border-radius: var(--radius); + align-items: center; + user-select: none; +} + +.row { + display: flex; + gap: 5px; +} + +.row:nth-child(2) { margin-left: 20px; } +.row:nth-child(3) { margin-left: 35px; } +.row:nth-child(4) { margin-left: 55px; } + +.key { + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + font-family: var(--font-mono); + font-size: 14px; + font-weight: 600; + position: relative; + transition: all 0.15s ease; + border: 2px solid transparent; + text-transform: uppercase; +} + +.key:hover { + transform: translateY(-2px); + filter: brightness(1.2); +} + +.space { + width: 280px; +} + +.active { + animation: pulse 1s ease-in-out infinite; + transform: translateY(-2px); + filter: brightness(1.3); + z-index: 1; +} + +.pressed { + transform: translateY(2px) !important; + filter: brightness(0.8) !important; + transition: all 0.05s ease; +} + +.fingerLabel { + position: absolute; + bottom: -18px; + font-size: 9px; + color: var(--text-dim); + white-space: nowrap; + opacity: 0; + transition: opacity 0.15s ease; +} + +.key:hover .fingerLabel { + opacity: 1; +} diff --git a/src/styles/typing.module.css b/src/styles/typing.module.css new file mode 100644 index 0000000..64a414d --- /dev/null +++ b/src/styles/typing.module.css @@ -0,0 +1,221 @@ +.typingArea { + background: var(--bg-card); + border-radius: var(--radius); + padding: 30px; + font-family: var(--font-mono); + font-size: 22px; + line-height: 1.8; + min-height: 120px; + cursor: text; + outline: none; + position: relative; + border: 2px solid transparent; + transition: border-color 0.2s ease; +} + +.typingArea:focus { + border-color: var(--accent); +} + +.typingArea.blurred::after { + content: 'Click here to start typing...'; + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(26, 27, 46, 0.8); + border-radius: var(--radius); + color: var(--text-dim); + font-size: 16px; +} + +.char { + position: relative; + letter-spacing: 1px; +} + +.correct { + color: var(--success); +} + +.incorrect { + color: var(--error); + background: rgba(231, 76, 60, 0.15); + border-radius: 3px; +} + +.current { + border-left: 2px solid var(--accent); + animation: blink 1s step-end infinite; + margin-left: -1px; + padding-left: 1px; +} + +.pending { + color: var(--text-dim); +} + +@keyframes blink { + 50% { border-color: transparent; } +} + +.stats { + display: flex; + gap: 30px; + padding: 16px 0; +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; +} + +.statValue { + font-size: 28px; + font-weight: 700; + font-family: var(--font-mono); + color: var(--accent); +} + +.statLabel { + font-size: 12px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; +} + +.lessonSelect { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 16px; +} + +.lessonCard { + background: var(--bg-card); + border-radius: var(--radius); + padding: 20px; + cursor: pointer; + transition: all 0.2s ease; + border: 2px solid transparent; +} + +.lessonCard:hover:not(.locked) { + border-color: var(--accent); + transform: translateY(-2px); +} + +.locked { + opacity: 0.4; + cursor: not-allowed; +} + +.completed { + border-color: var(--success); +} + +.lessonId { + font-size: 12px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; +} + +.lessonName { + font-size: 18px; + font-weight: 600; + margin: 6px 0; +} + +.lessonKeys { + font-family: var(--font-mono); + color: var(--accent); + font-size: 14px; +} + +.lessonStatus { + font-size: 12px; + margin-top: 8px; + color: var(--text-dim); +} + +.modal { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + animation: fadeIn 0.2s ease; +} + +.modalContent { + background: var(--bg-card); + border-radius: var(--radius); + padding: 40px; + max-width: 440px; + width: 90%; + text-align: center; + animation: fadeIn 0.3s ease; +} + +.modalTitle { + font-size: 28px; + font-weight: 700; + margin-bottom: 20px; +} + +.modalStats { + display: flex; + justify-content: center; + gap: 40px; + margin: 24px 0; +} + +.unlockText { + color: var(--success); + font-size: 16px; + margin: 16px 0; +} + +.modalButtons { + display: flex; + gap: 12px; + justify-content: center; + margin-top: 24px; +} + +.primaryBtn { + background: var(--accent); + color: white; + font-weight: 600; + padding: 12px 28px; + font-size: 16px; +} + +.primaryBtn:hover { + filter: brightness(1.1); + background: var(--accent); +} + +.secondaryBtn { + background: var(--bg-hover); + padding: 12px 28px; + font-size: 16px; +} + +.modeHeader { + margin-bottom: 24px; +} + +.modeTitle { + font-size: 24px; + font-weight: 700; +} + +.modeSubtitle { + color: var(--text-dim); + margin-top: 4px; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..7fd8958 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,33 @@ +export type Finger = + | 'lpinky' | 'lring' | 'lmiddle' | 'lindex' + | 'rindex' | 'rmiddle' | 'rring' | 'rpinky' + | 'thumb' + +export type KeyInfo = { + key: string + finger: Finger + row: number + col: number +} + +export type Lesson = { + id: number + name: string + newKeys: string[] + words: string[] + unlockAfter?: number +} + +export type SessionResult = { + mode: string + wpm: number + accuracy: number + timestamp: number + keyStats: Record +} + +export type UserProgress = { + completedLessons: number[] + sessions: SessionResult[] + gameHighScore: number +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..af516fc --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2023", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})