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)];
+};