diff --git a/TypeScriptRacer/package-lock.json b/TypeScriptRacer/package-lock.json index 64d8a1a..cab4dfe 100644 --- a/TypeScriptRacer/package-lock.json +++ b/TypeScriptRacer/package-lock.json @@ -8,6 +8,12 @@ "name": "typescriptracer", "version": "0.0.0", "dependencies": { + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/language": "^6.11.3", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.38.8", + "@lezer/highlight": "^1.2.3", + "@uiw/react-codemirror": "^4.25.3", "react": "^19.1.1", "react-dom": "^19.1.1" }, @@ -25,6 +31,124 @@ "vite": "^7.1.7" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", + "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz", + "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", + "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz", + "integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", + "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.8", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", + "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", @@ -673,6 +797,47 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@lezer/common": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz", + "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.3.tgz", + "integrity": "sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1546,6 +1711,59 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.3", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.3.tgz", + "integrity": "sha512-F1doRyD50CWScwGHG2bBUtUpwnOv/zqSnzkZqJcX5YAHQx6Z1CuX8jdnFMH6qktRrPU1tfpNYftTWu3QIoHiMA==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.3", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.3.tgz", + "integrity": "sha512-1wtBZTXPIp8u6F/xjHvsUAYlEeF5Dic4xZBnqJyLzv7o7GjGYEUfSz9Z7bo9aK9GAx2uojG/AuBMfhA4uhvIVQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.3", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/@vitejs/plugin-react-swc": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.1.0.tgz", @@ -1685,6 +1903,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1712,6 +1945,12 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1759,17 +1998,6 @@ "dev": true, "license": "MIT" }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/esbuild": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", @@ -2322,237 +2550,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2769,7 +2766,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2835,6 +2831,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -2994,6 +2991,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3252,6 +3255,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/TypeScriptRacer/package.json b/TypeScriptRacer/package.json index 70284ef..dae1ce6 100644 --- a/TypeScriptRacer/package.json +++ b/TypeScriptRacer/package.json @@ -10,6 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/language": "^6.11.3", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.38.8", + "@lezer/highlight": "^1.2.3", + "@uiw/react-codemirror": "^4.25.3", "react": "^19.1.1", "react-dom": "^19.1.1" }, diff --git a/TypeScriptRacer/src/App.tsx b/TypeScriptRacer/src/App.tsx index d08cfef..77407cb 100644 --- a/TypeScriptRacer/src/App.tsx +++ b/TypeScriptRacer/src/App.tsx @@ -3,10 +3,24 @@ import { useState } from 'react' import { Header } from './Components/Header' import { ViewportGuard } from './Components/ViewportGuard' import { BootSequence } from './Components/BootSequence' +import { CodeEditor } from './Components/CodeEditor' +import type { TypingStats } from './hooks/useTypingGame' function App() { const [bootComplete, setBootComplete] = useState(false) + // Sample code snippet for the typing game + const sampleCode = `function calculateWPM(chars: number, time: number) { + const words = chars / 5; + return (words / time) * 60; +}` + + // Handle game completion + const handleComplete = (stats: TypingStats) => { + console.log('Game completed!', stats) + // You can add logic here to save scores, show results modal, etc. + } + return (
setBootComplete(true)} /> @@ -28,6 +42,18 @@ function App() {

+ {/* Code Editor Typing Game */} +
+

+ Typing Game Demo +

+ +
+ {/* Color Palette Cards */}

diff --git a/TypeScriptRacer/src/Components/CodeEditor.css b/TypeScriptRacer/src/Components/CodeEditor.css new file mode 100644 index 0000000..89f0689 --- /dev/null +++ b/TypeScriptRacer/src/Components/CodeEditor.css @@ -0,0 +1,474 @@ +/* ============================================================================ + CODE EDITOR - TypeScript Racer + Styling for the typing game interface + ============================================================================ */ + +.code-editor-container { + display: flex; + flex-direction: column; + gap: var(--token-space-lg); + width: 100%; + max-width: var(--component-max-width); +} + +/* ============================================================================ + STATS PANEL + ============================================================================ */ +.stats-panel { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: var(--token-space-md); + padding: var(--token-space-md); + background-color: var(--token-bg-elevated); + border: var(--token-border-width) solid var(--token-border-default); + box-shadow: 0 0 15px var(--token-effect-glow), + 2px 2px 0 var(--token-border-muted); +} + +.stat-item { + display: flex; + flex-direction: column; + gap: var(--token-space-xs); + text-align: center; +} + +.stat-label { + font-size: var(--token-font-size-sm); + color: var(--token-text-secondary); + text-transform: uppercase; +} + +.stat-value { + font-size: var(--token-font-size-2xl); + color: var(--token-brand-primary); + font-weight: var(--token-font-weight); + text-shadow: 0 0 8px var(--token-effect-glow); +} + +/* ============================================================================ + TYPING TERMINAL (Single Terminal with Overlay) + ============================================================================ */ +.typing-terminal { + display: flex; + flex-direction: column; + background-color: var(--token-bg-elevated); + border: var(--token-border-width) double var(--token-border-default); + box-shadow: 0 0 20px var(--token-effect-glow), + 3px 3px 0 var(--token-border-muted), + inset 0 0 20px var(--token-effect-shadow-medium); + position: relative; +} + +.dark .typing-terminal { + box-shadow: 0 0 20px var(--token-effect-glow), + 3px 3px 0 var(--token-border-muted), + inset 0 0 20px var(--primitive-shadow-black-heavy); +} + +.terminal-content { + position: relative; + min-height: 250px; + overflow: hidden; +} + +.code-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--token-space-sm) var(--token-space-md); + background-color: var(--token-bg-base); + border-bottom: var(--primitive-border-1) solid var(--token-border-default); +} + +.code-header-title { + font-size: var(--token-font-size-base); + color: var(--token-brand-primary); + text-shadow: 0 0 5px var(--token-effect-glow); +} + +.code-header-title::before { + content: '> '; + opacity: 0.7; + animation: cursor-blink 1s step-end infinite; +} + +@keyframes cursor-blink { + 0%, 100% { opacity: 0.7; } + 50% { opacity: 0.2; } +} + +.code-header-hint { + font-size: var(--token-font-size-sm); + color: var(--token-text-muted); + font-style: italic; +} + +.code-header-complete { + font-size: var(--token-font-size-base); + color: var(--token-status-success); + text-shadow: 0 0 8px var(--token-status-success); + animation: pulse-glow 1s ease-in-out infinite; +} + +.paste-warning, +.error-warning { + font-size: var(--token-font-size-base); + color: var(--token-status-error); + text-shadow: 0 0 8px var(--token-status-error); + animation: shake 0.5s ease-in-out, pulse-glow 1s ease-in-out infinite; +} + +@keyframes pulse-glow { + 0%, 100% { + opacity: 1; + text-shadow: 0 0 8px var(--token-status-success); + } + 50% { + opacity: 0.7; + text-shadow: 0 0 15px var(--token-status-success); + } +} + +@keyframes shake { + 0%, 100% { + transform: translateX(0); + } + 25% { + transform: translateX(-5px); + } + 75% { + transform: translateX(5px); + } +} + +/* ============================================================================ + GHOST CODE OVERLAY (Dimmed target code) + ============================================================================ */ +.ghost-code-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: transparent; + pointer-events: none; + z-index: 10; + /* Prevent text selection and copying */ + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.ghost-code-with-lines { + display: flex; + flex-direction: column; + padding-left: calc(3rem + 1rem); +} + +.ghost-code-line { + line-height: 36px; + height: 36px; + min-height: 36px; +} + +.ghost-code-content { + font-family: var(--token-font-family); + font-size: 24px; + line-height: 36px; + color: var(--token-text-primary); + opacity: 0.45; + margin: 0; + white-space: pre; + padding: 0; +} + +.target-code-with-lines { + display: flex; + flex-direction: column; +} + +.target-code-line { + display: flex; + flex-direction: row; + line-height: var(--token-line-height-normal); +} + +.target-line-number { + font-family: var(--token-font-family); + font-size: 24px; + line-height: 1.5; + color: var(--token-text-muted); + padding: 0 1rem 0 0.5rem; + text-align: right; + min-width: 3rem; + background-color: var(--token-bg-base); + border-right: 1px solid var(--token-border-muted); + flex-shrink: 0; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.target-code-content { + font-family: var(--token-font-family); + font-size: 24px; + line-height: 1.5; + color: var(--token-text-primary); + margin: 0; + white-space: pre; + padding-left: 1rem; + flex: 1; + /* Prevent text selection and copying */ + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +/* Character highlighting for ghost code */ +.char { + position: relative; +} + +/* Chars that have been typed correctly fade out completely */ +.char-correct { + opacity: 0; +} + +/* Chars that have errors fade out like correct chars to avoid visual conflict */ +.char-error { + opacity: 0; +} + +/* Current character to type - highlighted */ +.char-current { + opacity: 1; + background-color: var(--token-brand-primary); + color: var(--token-bg-base); + animation: current-char-pulse 0.8s ease-in-out infinite; +} + +@keyframes current-char-pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } +} + +/* ============================================================================ + USER INPUT OVERLAY (CodeMirror editor on top of ghost) + ============================================================================ */ +.user-input-overlay { + position: relative; + z-index: 2; +} + +/* Error character styling in CodeMirror */ +.cm-error-char { + background-color: #ff4444 !important; + color: #ffffff !important; + border-radius: 2px; + text-decoration: underline; + text-decoration-color: #000000; + text-decoration-thickness: 2px; + text-underline-offset: 2px; + font-weight: bold; +} + +/* Dark mode adjustments - brighter red background with cyan text for max contrast */ +.dark .cm-error-char { + background-color: #ff0000 !important; + color: #00ffff !important; + text-decoration-color: #ffffff; + font-weight: bold; + text-shadow: 0 0 2px rgba(0, 0, 0, 0.8); +} + +/* Error state styling */ +.typing-terminal.has-error { + animation: error-shake 0.3s ease-in-out; +} + +@keyframes error-shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-3px); } + 75% { transform: translateX(3px); } +} + +.code-mirror-editor { + font-family: var(--token-font-family) !important; + background-color: transparent !important; +} + +/* Make CodeMirror background transparent so ghost shows through */ +.code-mirror-editor .cm-editor { + background-color: transparent !important; + padding: 0 !important; + margin: 0 !important; +} + +.code-mirror-editor .cm-scroller { + background-color: transparent !important; + padding: 0 !important; + margin: 0 !important; +} + +.code-mirror-editor .cm-gutters { + background-color: var(--token-bg-base) !important; + padding-top: 0 !important; + margin-top: 0 !important; +} + +/* Force all wrapper divs to have no padding */ +.code-mirror-editor > div { + padding: 0 !important; + margin: 0 !important; +} + +/* Override CodeMirror default styles to match theme */ +.code-mirror-editor .cm-editor { + outline: none !important; +} + +.code-mirror-editor .cm-scroller { + font-family: var(--token-font-family) !important; + overflow-x: auto; +} + +.code-mirror-editor .cm-content { + font-family: var(--token-font-family) !important; + font-size: 24px !important; + line-height: 36px !important; + padding: 0 0 0 1rem !important; +} + +.code-mirror-editor .cm-gutters { + min-width: 3rem !important; + width: 3rem !important; + padding: 0 0.8rem 0 0.5rem !important; + background-color: var(--token-bg-base) !important; + opacity: 1 !important; +} + +.code-mirror-editor .cm-lineNumbers { + font-family: var(--token-font-family) !important; + font-size: 24px !important; + line-height: 36px !important; +} + +.code-mirror-editor .cm-gutterElement { + text-align: right !important; + padding: 0 !important; + margin: 0 !important; + line-height: 36px !important; + min-height: 36px !important; + height: 36px !important; +} + +.code-mirror-editor .cm-line { + line-height: 36px !important; + padding: 0 !important; + margin: 0 !important; + height: 36px !important; +} + +/* Remove any extra spacing from the editor wrapper */ +.code-mirror-editor .cm-editor { + outline: none !important; +} + +.code-mirror-editor .cm-scroller { + padding: 0 !important; +} + +.code-mirror-editor .cm-content { + padding-top: 0 !important; + margin-top: 0 !important; +} + +/* ============================================================================ + ACTION BUTTONS + ============================================================================ */ +.editor-actions { + display: flex; + gap: var(--token-space-md); + justify-content: center; + padding: var(--token-space-md) 0; +} + +/* ============================================================================ + RESPONSIVE DESIGN + ============================================================================ */ +@media (max-width: 768px) { + .stats-panel { + grid-template-columns: repeat(2, 1fr); + } + + .stat-label { + font-size: var(--token-font-size-xs); + } + + .stat-value { + font-size: var(--token-font-size-xl); + } + + .ghost-code-content, + .ghost-line-number { + font-size: 20px; + } + + .code-mirror-editor .cm-content { + font-size: 20px !important; + } + + .code-mirror-editor .cm-lineNumbers { + font-size: 20px !important; + } +} + +@media (max-width: 480px) { + .stats-panel { + grid-template-columns: 1fr; + } + + .code-header { + flex-direction: column; + align-items: flex-start; + gap: var(--token-space-xs); + } +} + +/* ============================================================================ + ACCESSIBILITY + ============================================================================ */ +.code-editor-container:focus-within .code-header-title { + text-shadow: 0 0 10px var(--token-effect-glow-strong); +} + +/* Ensure good contrast for character states */ +.dark .char-error { + color: var(--token-bg-base); + background-color: var(--token-status-error); +} + +/* ============================================================================ + ANIMATIONS & TRANSITIONS + ============================================================================ */ +.code-editor-container { + animation: fadeInUp 0.5s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/TypeScriptRacer/src/Components/CodeEditor.tsx b/TypeScriptRacer/src/Components/CodeEditor.tsx new file mode 100644 index 0000000..afe0622 --- /dev/null +++ b/TypeScriptRacer/src/Components/CodeEditor.tsx @@ -0,0 +1,277 @@ +import { useMemo, useState } from 'react'; +import CodeMirror from '@uiw/react-codemirror'; +import { javascript } from '@codemirror/lang-javascript'; +import { EditorView, Decoration } from '@codemirror/view'; +import { StateField } from '@codemirror/state'; +import type { DecorationSet } from '@codemirror/view'; +import { useTypingGame } from '../hooks/useTypingGame'; +import type { TypingStats } from '../hooks/useTypingGame'; +import { createTerminalTheme } from '../themes/terminalTheme'; +import { useTheme } from '../context/ThemeContext'; +import './CodeEditor.css'; + +export interface CodeEditorProps { + targetCode: string; + language?: 'javascript' | 'typescript' | 'python' | 'java' | 'go'; + onComplete?: (stats: TypingStats) => void; +} + +export const CodeEditor = ({ + targetCode, + language = 'javascript', + onComplete +}: CodeEditorProps) => { + const { theme } = useTheme(); + const isDark = theme === 'dark'; + const [pasteAttempted, setPasteAttempted] = useState(false); + const [editorKey, setEditorKey] = useState(0); + + const { + userInput, + stats, + isStarted, + isCompleted, + handleInputChange, + resetGame, + currentCharIndex, + errors, + hasError, + } = useTypingGame({ targetCode, onComplete }); + + // Create the terminal theme based on current theme + const terminalTheme = useMemo(() => createTerminalTheme(isDark), [isDark]); + + // Create error decoration extension + const errorDecoration = useMemo(() => { + const errorMark = Decoration.mark({ + class: 'cm-error-char' + }); + + const errorField = StateField.define({ + create() { + return Decoration.none; + }, + update() { + // Build decorations for error positions + const builder: any[] = []; + errors.forEach(errorPos => { + if (errorPos < userInput.length) { + builder.push(errorMark.range(errorPos, errorPos + 1)); + } + }); + return Decoration.set(builder.sort((a, b) => a.from - b.from)); + }, + provide: f => EditorView.decorations.from(f) + }); + + return errorField; + }, [errors, userInput]); + + // Prevent paste extension for CodeMirror + const preventPasteExtension = useMemo(() => { + return EditorView.domEventHandlers({ + paste: (event) => { + event.preventDefault(); + setPasteAttempted(true); + setTimeout(() => setPasteAttempted(false), 2000); + return true; + }, + // Also prevent drop events (drag and drop text) + drop: (event) => { + event.preventDefault(); + return true; + }, + // Block typing when there's an error (but allow backspace/delete) + keydown: (event) => { + if (hasError) { + // Allow backspace, delete, and arrow keys + const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']; + if (!allowedKeys.includes(event.key)) { + event.preventDefault(); + return true; + } + } + return false; + }, + }); + }, [hasError]); + + // Get language support based on prop + const getLanguageSupport = () => { + switch (language) { + case 'javascript': + case 'typescript': + return javascript({ jsx: true, typescript: language === 'typescript' }); + // Add more languages here as needed + default: + return javascript(); + } + }; + + // Create decorated target code with character highlighting and line numbers + const decoratedTarget = useMemo(() => { + const lines = targetCode.split('\n'); + let charIndex = 0; + + return lines.map((line, lineIndex) => { + const lineChars = line.split('').map((char) => { + const globalCharIndex = charIndex; + charIndex++; + + let className = 'char'; + + if (globalCharIndex < currentCharIndex) { + // Character has been typed + if (errors.has(globalCharIndex)) { + className += ' char-error'; + } else { + className += ' char-correct'; + } + } else if (globalCharIndex === currentCharIndex) { + // Current character to type + className += ' char-current'; + } + + return { char, className, index: globalCharIndex }; + }); + + // Increment charIndex for the newline character (except for the last line) + if (lineIndex < lines.length - 1) { + charIndex++; + } + + return { + lineNumber: lineIndex + 1, + chars: lineChars, + }; + }); + }, [targetCode, currentCharIndex, errors]); + + // Format time as MM:SS + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + }; + + // Handle reset - also force CodeMirror to re-render + const handleReset = () => { + resetGame(); + setEditorKey(prev => prev + 1); + }; + + return ( +
+ {/* Stats Panel */} +
+
+ WPM: + {stats.wpm} +
+
+ Accuracy: + {stats.accuracy.toFixed(1)}% +
+
+ Time: + {formatTime(stats.timeElapsed)} +
+
+ Chars: + {currentCharIndex}/{targetCode.length} +
+
+ + {/* Single Terminal with Overlay */} +
+
+ $ Type the Code + {!isStarted && ( + Start typing to begin... + )} + {isCompleted && ( + Complete! + )} + {pasteAttempted && ( + No cheating! Type it out. + )} + {hasError && ( + Fix the typo to continue! + )} +
+ +
+ {/* Ghost target code overlay */} +
e.preventDefault()} + onCopy={(e) => e.preventDefault()} + > +
+ {decoratedTarget.map(({ lineNumber, chars }) => ( +
+
+                    {chars.map(({ char, className, index }) => (
+                      
+                        {char}
+                      
+                    ))}
+                  
+
+ ))} +
+
+ + {/* User input editor - overlays on top */} +
+ +
+
+
+ + {/* Action Buttons */} +
+ {isCompleted && ( + + )} + {isStarted && !isCompleted && ( + + )} +
+
+ ); +}; diff --git a/TypeScriptRacer/src/hooks/useTypingGame.ts b/TypeScriptRacer/src/hooks/useTypingGame.ts new file mode 100644 index 0000000..a83b72f --- /dev/null +++ b/TypeScriptRacer/src/hooks/useTypingGame.ts @@ -0,0 +1,227 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; + +export interface TypingStats { + wpm: number; + accuracy: number; + timeElapsed: number; + correctChars: number; + incorrectChars: number; + totalChars: number; +} + +// Helper function to normalize whitespace for comparison +const normalizeWhitespace = (str: string): string => { + // Replace multiple consecutive spaces with a single space + // Preserve newlines + return str.replace(/[^\S\n]+/g, ' ').replace(/^\s+/gm, '').replace(/\s+$/gm, ''); +}; + +// Helper function to check if character matches with whitespace flexibility +const isCharMatch = (userChar: string, targetChar: string, userInput: string, targetCode: string, index: number): boolean => { + // Exact match is always correct + if (userChar === targetChar) { + return true; + } + + // If both are whitespace characters (space, tab), consider them equivalent + // This allows flexibility between spaces and tabs + if (/\s/.test(userChar) && /\s/.test(targetChar) && userChar !== '\n' && targetChar !== '\n') { + return true; + } + + // If user typed a non-whitespace but target expects whitespace, check if we can skip it + // This handles cases where the user might skip trailing/extra spaces + if (!(/\s/.test(userChar)) && /\s/.test(targetChar) && targetChar !== '\n') { + // Look ahead to see if the next non-whitespace character in target matches current user char + let targetLookAhead = index + 1; + while (targetLookAhead < targetCode.length && /\s/.test(targetCode[targetLookAhead]) && targetCode[targetLookAhead] !== '\n') { + targetLookAhead++; + } + // If we found a non-whitespace character ahead that matches, allow it + if (targetLookAhead < targetCode.length && userChar === targetCode[targetLookAhead]) { + return true; + } + } + + // If target expects non-whitespace but user typed whitespace (except newlines), check if we can skip + if (/\s/.test(userChar) && userChar !== '\n' && !(/\s/.test(targetChar))) { + // Look ahead in user input to see if next non-whitespace matches target + let userLookAhead = index + 1; + while (userLookAhead < userInput.length && /\s/.test(userInput[userLookAhead]) && userInput[userLookAhead] !== '\n') { + userLookAhead++; + } + // If we found a non-whitespace character ahead that matches, allow it + if (userLookAhead < userInput.length && userInput[userLookAhead] === targetChar) { + return true; + } + } + + return false; +}; + +export interface UseTypingGameProps { + targetCode: string; + onComplete?: (stats: TypingStats) => void; +} + +export interface UseTypingGameReturn { + userInput: string; + stats: TypingStats; + isStarted: boolean; + isCompleted: boolean; + handleInputChange: (value: string) => void; + resetGame: () => void; + currentCharIndex: number; + errors: Set; + hasError: boolean; +} + +export const useTypingGame = ({ + targetCode, + onComplete +}: UseTypingGameProps): UseTypingGameReturn => { + const [userInput, setUserInput] = useState(''); + const [isStarted, setIsStarted] = useState(false); + const [isCompleted, setIsCompleted] = useState(false); + const [startTime, setStartTime] = useState(null); + const [timeElapsed, setTimeElapsed] = useState(0); + const [errors, setErrors] = useState>(new Set()); + const [hasError, setHasError] = useState(false); + + const intervalRef = useRef(null); + + // Normalize target code for flexible matching + const normalizedTarget = useRef(normalizeWhitespace(targetCode)); + const normalizedInput = normalizeWhitespace(userInput); + + // Calculate stats + const stats: TypingStats = { + wpm: 0, + accuracy: 0, + timeElapsed, + correctChars: 0, + incorrectChars: 0, + totalChars: userInput.length, + }; + + // Count correct and incorrect characters (using normalized strings) + for (let i = 0; i < normalizedInput.length; i++) { + if (normalizedInput[i] === normalizedTarget.current[i]) { + stats.correctChars++; + } else { + stats.incorrectChars++; + } + } + stats.totalChars = normalizedInput.length; + + // Calculate accuracy + if (stats.totalChars > 0) { + stats.accuracy = (stats.correctChars / stats.totalChars) * 100; + } + + // Calculate WPM (Words Per Minute) + // Standard: 5 characters = 1 word + if (timeElapsed > 0) { + const minutes = timeElapsed / 60; + const words = stats.correctChars / 5; + stats.wpm = Math.round(words / minutes); + } + + // Timer effect + useEffect(() => { + if (isStarted && !isCompleted) { + intervalRef.current = setInterval(() => { + setTimeElapsed((prev) => prev + 1); + }, 1000); + } else { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + } + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [isStarted, isCompleted]); + + // Handle input change + const handleInputChange = useCallback((value: string) => { + // Start the game on first input + if (!isStarted) { + setIsStarted(true); + setStartTime(Date.now()); + } + + // Don't allow input beyond target length + if (value.length > targetCode.length) { + return; + } + + setUserInput(value); + + // Track errors for display purposes and hasError state + const newErrors = new Set(); + for (let i = 0; i < value.length; i++) { + if (!isCharMatch(value[i], targetCode[i], value, targetCode, i)) { + newErrors.add(i); + } + } + setErrors(newErrors); + setHasError(newErrors.size > 0); + + // Check if completed (using normalized comparison for flexibility) + const normalizedValue = normalizeWhitespace(value); + const normalizedTargetCode = normalizeWhitespace(targetCode); + + if (value.length === targetCode.length && normalizedValue === normalizedTargetCode) { + setIsCompleted(true); + if (onComplete) { + // Calculate final stats + const finalStats: TypingStats = { + wpm: 0, + accuracy: 100, // If completed, they got it all right + timeElapsed: startTime ? Math.floor((Date.now() - startTime) / 1000) : 0, + correctChars: normalizedValue.length, + incorrectChars: 0, + totalChars: normalizedValue.length, + }; + + if (finalStats.timeElapsed > 0) { + const minutes = finalStats.timeElapsed / 60; + const words = finalStats.correctChars / 5; + finalStats.wpm = Math.round(words / minutes); + } + + onComplete(finalStats); + } + } + }, [isStarted, targetCode, onComplete, startTime, userInput]); + + // Reset game + const resetGame = useCallback(() => { + setUserInput(''); + setIsStarted(false); + setIsCompleted(false); + setStartTime(null); + setTimeElapsed(0); + setErrors(new Set()); + setHasError(false); + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }, []); + + return { + userInput, + stats, + isStarted, + isCompleted, + handleInputChange, + resetGame, + currentCharIndex: userInput.length, + errors, + hasError, + }; +}; diff --git a/TypeScriptRacer/src/themes/terminalTheme.ts b/TypeScriptRacer/src/themes/terminalTheme.ts new file mode 100644 index 0000000..94a8886 --- /dev/null +++ b/TypeScriptRacer/src/themes/terminalTheme.ts @@ -0,0 +1,172 @@ +import { EditorView } from '@codemirror/view'; +import type { Extension } from '@codemirror/state'; +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { tags as t } from '@lezer/highlight'; + +// Create a theme that matches the retro terminal aesthetic +export const createTerminalTheme = (isDark: boolean): Extension => { + const theme = EditorView.theme( + { + '&': { + backgroundColor: isDark + ? 'oklch(0.08 0.02 150) !important' // --primitive-green-975 for dark + : 'oklch(0.93 0.03 85) !important', // --primitive-amber-200 for light + color: isDark + ? 'oklch(0.78 0.12 145) !important' // --primitive-green-100 for dark + : 'oklch(0.25 0.04 40) !important', // --primitive-amber-900 for light + fontFamily: 'VT323, monospace', + fontSize: '24px', + letterSpacing: '0.5px', + lineHeight: '36px', + }, + '.cm-content': { + caretColor: isDark + ? 'oklch(0.78 0.12 145)' // green cursor in dark mode + : 'oklch(0.55 0.10 50)', // amber cursor in light mode + padding: '0', + color: isDark + ? 'oklch(0.78 0.12 145) !important' + : 'oklch(0.25 0.04 40) !important', + }, + '.cm-cursor, .cm-dropCursor': { + borderLeftColor: isDark + ? 'oklch(0.78 0.12 145)' + : 'oklch(0.55 0.10 50)', + borderLeftWidth: '2px', + }, + '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': { + backgroundColor: isDark + ? 'rgba(51, 255, 102, 0.3)' + : 'rgba(204, 136, 0, 0.3)', + }, + '.cm-activeLine': { + backgroundColor: 'transparent', + }, + '.cm-gutters': { + backgroundColor: isDark + ? 'oklch(0.08 0.02 150) !important' + : 'oklch(0.93 0.03 85) !important', + color: isDark + ? 'oklch(0.55 0.09 145) !important' + : 'oklch(0.50 0.03 40) !important', + border: 'none', + borderRight: '1px solid ' + (isDark ? 'oklch(0.40 0.08 145)' : 'oklch(0.50 0.03 40)'), + fontFamily: 'VT323, monospace', + minWidth: '3rem', + paddingRight: '1rem', + }, + '.cm-activeLineGutter': { + backgroundColor: 'transparent', + }, + '.cm-foldPlaceholder': { + backgroundColor: 'transparent', + border: 'none', + color: isDark + ? 'oklch(0.60 0.11 145)' + : 'oklch(0.40 0.08 40)', + }, + '.cm-line': { + paddingLeft: '0.5rem', + }, + }, + { dark: isDark } + ); + + // Syntax highlighting colors matching your design tokens + const highlightStyle = HighlightStyle.define([ + { + tag: t.keyword, + color: isDark + ? 'oklch(0.72 0.12 190)' // --token-syntax-keyword dark (cyan) + : 'oklch(0.50 0.10 280)', // --token-syntax-keyword light (purple) + }, + { + tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], + color: isDark + ? 'oklch(0.78 0.12 145)' + : 'oklch(0.25 0.04 40)', + }, + { + tag: [t.function(t.variableName), t.labelName], + color: isDark + ? 'oklch(0.68 0.10 200)' // --token-syntax-function dark (sky blue) + : 'oklch(0.45 0.09 240)', // --token-syntax-function light (blue) + }, + { + tag: [t.color, t.constant(t.name), t.standard(t.name)], + color: isDark + ? 'oklch(0.70 0.10 155)' + : 'oklch(0.45 0.09 240)', + }, + { + tag: [t.definition(t.name), t.separator], + color: isDark + ? 'oklch(0.78 0.12 145)' + : 'oklch(0.25 0.04 40)', + }, + { + tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], + color: isDark + ? 'oklch(0.70 0.10 155)' // --token-syntax-number dark (emerald) + : 'oklch(0.55 0.09 50)', // --token-syntax-number light (orange) + }, + { + tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], + color: isDark + ? 'oklch(0.72 0.11 95)' + : 'oklch(0.50 0.10 50)', + }, + { + tag: [t.meta, t.comment], + color: isDark + ? 'oklch(0.55 0.09 145)' // --token-text-muted dark + : 'oklch(0.50 0.03 40)', // --token-text-muted light + }, + { + tag: t.strong, + fontWeight: '400', + }, + { + tag: t.emphasis, + fontStyle: 'italic', + }, + { + tag: t.strikethrough, + textDecoration: 'line-through', + }, + { + tag: t.link, + color: isDark + ? 'oklch(0.70 0.12 190)' + : 'oklch(0.55 0.10 50)', + textDecoration: 'underline', + }, + { + tag: t.heading, + fontWeight: '400', + color: isDark + ? 'oklch(0.82 0.14 145)' + : 'oklch(0.25 0.04 40)', + }, + { + tag: [t.atom, t.bool, t.special(t.variableName)], + color: isDark + ? 'oklch(0.70 0.10 155)' + : 'oklch(0.55 0.09 50)', + }, + { + tag: [t.processingInstruction, t.string, t.inserted], + color: isDark + ? 'oklch(0.72 0.11 95)' // --token-syntax-string dark (lime) + : 'oklch(0.55 0.09 150)', // --token-syntax-string light (green) + }, + { + tag: t.invalid, + color: isDark + ? 'oklch(0.60 0.14 25)' + : 'oklch(0.50 0.11 25)', + }, + ]); + + return [theme, syntaxHighlighting(highlightStyle)]; +};