add stats view

This commit is contained in:
Tobias Nauen
2026-03-05 21:24:59 +01:00
parent 89c3cd91c2
commit b66da20079
5 changed files with 987 additions and 137 deletions

385
package-lock.json generated
View File

@@ -9,13 +9,13 @@
"version": "1.0.0",
"devDependencies": {
"typescript": "^5.3.3",
"vite": "^5.0.12"
"vite": "^7.3.1"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
"cpu": [
"ppc64"
],
@@ -26,13 +26,13 @@
"aix"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
"cpu": [
"arm"
],
@@ -43,13 +43,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
"cpu": [
"arm64"
],
@@ -60,13 +60,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
"cpu": [
"x64"
],
@@ -77,13 +77,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
"cpu": [
"arm64"
],
@@ -94,13 +94,13 @@
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
"cpu": [
"x64"
],
@@ -111,13 +111,13 @@
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
"cpu": [
"arm64"
],
@@ -128,13 +128,13 @@
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
"cpu": [
"x64"
],
@@ -145,13 +145,13 @@
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
"cpu": [
"arm"
],
@@ -162,13 +162,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
"cpu": [
"arm64"
],
@@ -179,13 +179,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
"cpu": [
"ia32"
],
@@ -196,13 +196,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
"cpu": [
"loong64"
],
@@ -213,13 +213,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
"cpu": [
"mips64el"
],
@@ -230,13 +230,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
"cpu": [
"ppc64"
],
@@ -247,13 +247,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
"cpu": [
"riscv64"
],
@@ -264,13 +264,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
"cpu": [
"s390x"
],
@@ -281,13 +281,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
"cpu": [
"x64"
],
@@ -298,13 +298,30 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
"cpu": [
"x64"
],
@@ -315,13 +332,30 @@
"netbsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
"cpu": [
"x64"
],
@@ -332,13 +366,30 @@
"openbsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
"cpu": [
"x64"
],
@@ -349,13 +400,13 @@
"sunos"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
"cpu": [
"arm64"
],
@@ -366,13 +417,13 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
"cpu": [
"ia32"
],
@@ -383,13 +434,13 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
"cpu": [
"x64"
],
@@ -400,7 +451,7 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
@@ -761,9 +812,9 @@
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -771,32 +822,53 @@
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
"@esbuild/aix-ppc64": "0.27.3",
"@esbuild/android-arm": "0.27.3",
"@esbuild/android-arm64": "0.27.3",
"@esbuild/android-x64": "0.27.3",
"@esbuild/darwin-arm64": "0.27.3",
"@esbuild/darwin-x64": "0.27.3",
"@esbuild/freebsd-arm64": "0.27.3",
"@esbuild/freebsd-x64": "0.27.3",
"@esbuild/linux-arm": "0.27.3",
"@esbuild/linux-arm64": "0.27.3",
"@esbuild/linux-ia32": "0.27.3",
"@esbuild/linux-loong64": "0.27.3",
"@esbuild/linux-mips64el": "0.27.3",
"@esbuild/linux-ppc64": "0.27.3",
"@esbuild/linux-riscv64": "0.27.3",
"@esbuild/linux-s390x": "0.27.3",
"@esbuild/linux-x64": "0.27.3",
"@esbuild/netbsd-arm64": "0.27.3",
"@esbuild/netbsd-x64": "0.27.3",
"@esbuild/openbsd-arm64": "0.27.3",
"@esbuild/openbsd-x64": "0.27.3",
"@esbuild/openharmony-arm64": "0.27.3",
"@esbuild/sunos-x64": "0.27.3",
"@esbuild/win32-arm64": "0.27.3",
"@esbuild/win32-ia32": "0.27.3",
"@esbuild/win32-x64": "0.27.3"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fsevents": {
@@ -840,6 +912,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
@@ -924,6 +1009,23 @@
"node": ">=0.10.0"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -939,21 +1041,24 @@
}
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -962,19 +1067,25 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"@types/node": "^20.19.0 || >=22.12.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
@@ -995,6 +1106,12 @@
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
}

View File

@@ -10,6 +10,6 @@
},
"devDependencies": {
"typescript": "^5.3.3",
"vite": "^5.0.12"
"vite": "^7.3.1"
}
}

View File

@@ -1,8 +1,32 @@
import { getTypeInfo, getEffectiveness } from './types';
import { getTypeInfo, getEffectiveness, TypeName, TYPES } from './types';
import { Question, Sampler } from './sampler';
const STORAGE_KEY = 'pokemon-type-quiz-data';
export interface CombinationStats {
key: string;
attackType: TypeName;
defenderTypes: TypeName[];
total: number;
correct: number;
accuracy: number;
}
export interface TypeChartStats {
attackType: TypeName;
total: number;
correct: number;
accuracy: number;
}
export interface FullTypeChartCell {
attackType: TypeName;
defenderTypes: TypeName[];
total: number;
correct: number;
accuracy: number;
}
export interface GameState {
currentQuestion: Question | null;
selectedAnswer: number | null;
@@ -184,4 +208,160 @@ export class Game {
return explanation;
}
getCombinationStats(): CombinationStats[] {
const history = this.sampler.getHistory();
const comboMap = new Map<string, { total: number; correct: number }>();
for (const record of history) {
const existing = comboMap.get(record.key) || { total: 0, correct: 0 };
existing.total++;
if (record.correct) existing.correct++;
comboMap.set(record.key, existing);
}
const stats: CombinationStats[] = [];
for (const [key, data] of comboMap) {
const [attackType, ...defTypes] = key.split(':');
const defenderTypes = defTypes.join(':').split('/') as TypeName[];
stats.push({
key,
attackType: attackType as TypeName,
defenderTypes,
total: data.total,
correct: data.correct,
accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0,
});
}
return stats.sort((a, b) => b.accuracy - a.accuracy);
}
getBestCombinations(limit: number = 5): CombinationStats[] {
const stats = this.getCombinationStats().filter(s => s.total >= 3);
return stats.slice(0, limit);
}
getWorstCombinations(limit: number = 5): CombinationStats[] {
const stats = this.getCombinationStats().filter(s => s.total >= 3);
return stats.slice(-limit).reverse();
}
getTypeChartStats(): TypeChartStats[] {
const history = this.sampler.getHistory();
const typeMap = new Map<TypeName, { total: number; correct: number }>();
for (const type of TYPES) {
typeMap.set(type.name, { total: 0, correct: 0 });
}
for (const record of history) {
const [attackType] = record.key.split(':');
const data = typeMap.get(attackType as TypeName);
if (data) {
data.total++;
if (record.correct) data.correct++;
}
}
const stats: TypeChartStats[] = [];
for (const type of TYPES) {
const data = typeMap.get(type.name)!;
stats.push({
attackType: type.name,
total: data.total,
correct: data.correct,
accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0,
});
}
return stats.sort((a, b) => b.accuracy - a.accuracy);
}
getDefendingTypeStats(): TypeChartStats[] {
const history = this.sampler.getHistory();
const typeMap = new Map<TypeName, { total: number; correct: number }>();
for (const type of TYPES) {
typeMap.set(type.name, { total: 0, correct: 0 });
}
for (const record of history) {
// Parse defender types from key format: "AttackType:DefType1/DefType2"
const parts = record.key.split(':');
const defenderPart = parts[1];
const defenderTypes = defenderPart.split('/');
// Track each defender type
for (const defType of defenderTypes) {
const data = typeMap.get(defType as TypeName);
if (data) {
data.total++;
if (record.correct) data.correct++;
}
}
}
const stats: TypeChartStats[] = [];
for (const type of TYPES) {
const data = typeMap.get(type.name)!;
stats.push({
attackType: type.name,
total: data.total,
correct: data.correct,
accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0,
});
}
return stats.sort((a, b) => b.accuracy - a.accuracy);
}
private getAllDefenderTypes(): TypeName[][] {
const result: TypeName[][] = [];
// Single types
for (let i = 0; i < TYPES.length; i++) {
result.push([TYPES[i].name]);
}
// Dual types
for (let i = 0; i < TYPES.length; i++) {
for (let j = i + 1; j < TYPES.length; j++) {
result.push([TYPES[i].name, TYPES[j].name]);
}
}
return result;
}
getFullTypeChartStats(): FullTypeChartCell[] {
const history = this.sampler.getHistory();
const defenderTypesList = this.getAllDefenderTypes();
// Build a map for quick lookup
const statsMap = new Map<string, { total: number; correct: number }>();
for (const record of history) {
const existing = statsMap.get(record.key) || { total: 0, correct: 0 };
existing.total++;
if (record.correct) existing.correct++;
statsMap.set(record.key, existing);
}
const cells: FullTypeChartCell[] = [];
for (const attackType of TYPES) {
for (const defenderTypes of defenderTypesList) {
const key = `${attackType.name}:${defenderTypes.sort().join('/')}`;
const data = statsMap.get(key) || { total: 0, correct: 0 };
cells.push({
attackType: attackType.name,
defenderTypes,
total: data.total,
correct: data.correct,
accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0,
});
}
}
return cells;
}
}

View File

@@ -1,9 +1,11 @@
import { Game } from './lib/game';
import { Game, CombinationStats, TypeChartStats, FullTypeChartCell } from './lib/game';
import { getTypeInfo, TypeName } from './lib/types';
import './style.css';
const app = document.getElementById('app')!;
let currentView: 'quiz' | 'stats' = 'quiz';
let typeChartView: 'attacking' | 'defending' = 'attacking';
const game = new Game(render);
game.nextQuestion();
@@ -14,6 +16,11 @@ function renderTypeBadge(name: TypeName, size: 'small' | 'large' = 'small'): str
}
function render(): void {
if (currentView === 'stats') {
renderStatsPage();
return;
}
const state = game.getState();
const { options, startKey } = game.getOptions();
const stats = game.getStats();
@@ -70,6 +77,7 @@ function render(): void {
<span class="stat">Streak: ${state.streak}</span>
<span class="stat">Accuracy: ${stats.accuracy.toFixed(1)}%</span>
<span class="stat">Total: ${stats.total}</span>
<button class="stats-btn" id="statsBtn">View Stats</button>
</div>
</header>
@@ -105,6 +113,209 @@ function render(): void {
attachEventListeners();
}
function renderCombinationItem(combo: CombinationStats): string {
const defenderHtml = combo.defenderTypes.length === 1
? renderTypeBadge(combo.defenderTypes[0], 'small')
: `${renderTypeBadge(combo.defenderTypes[0], 'small')} / ${renderTypeBadge(combo.defenderTypes[1], 'small')}`;
const accuracyClass = combo.accuracy >= 80 ? 'high' : combo.accuracy >= 50 ? 'medium' : 'low';
return `
<div class="combo-item">
<div class="combo-types">
<span class="combo-attack">${renderTypeBadge(combo.attackType, 'small')}</span>
<span class="combo-vs">vs</span>
<span class="combo-defend">${defenderHtml}</span>
</div>
<div class="combo-stats">
<span class="combo-accuracy ${accuracyClass}">${combo.accuracy.toFixed(0)}%</span>
<span class="combo-count">(${combo.correct}/${combo.total})</span>
</div>
</div>
`;
}
function renderTypeChartCell(typeStat: TypeChartStats): string {
const info = getTypeInfo(typeStat.attackType);
const accuracyClass = typeStat.total === 0 ? 'no-data' : typeStat.accuracy >= 80 ? 'high' : typeStat.accuracy >= 50 ? 'medium' : 'low';
const display = typeStat.total === 0 ? '-' : `${typeStat.accuracy.toFixed(0)}%`;
return `
<div class="type-cell" style="background: ${info.backgroundColor}; color: ${info.color};" title="${typeStat.attackType}: ${typeStat.correct}/${typeStat.total}">
<span class="type-cell-name">${typeStat.attackType.slice(0, 3)}</span>
<span class="type-cell-accuracy ${accuracyClass}">${display}</span>
</div>
`;
}
function renderFullTypeChart(cells: FullTypeChartCell[]): string {
const defenderTypes = ['Normal', 'Fire', 'Water', 'Electric', 'Grass', 'Ice', 'Fighting', 'Poison', 'Ground', 'Flying', 'Psychic', 'Bug', 'Rock', 'Ghost', 'Dragon', 'Dark', 'Steel', 'Fairy'];
// Create a map for quick cell lookup
const cellMap = new Map<string, FullTypeChartCell>();
for (const cell of cells) {
const defenderKey = cell.defenderTypes.join('/');
const key = `${cell.attackType}:${defenderKey}`;
cellMap.set(key, cell);
}
// Generate header row
const headerCells = defenderTypes.map(def => {
const info = getTypeInfo(def as TypeName);
return `<div class="full-chart-header" style="background: ${info.backgroundColor}; color: ${info.color};">
${def.slice(0, 3)}
</div>`;
}).join('');
// Generate rows
const rows = defenderTypes.map(rowType => {
const rowInfo = getTypeInfo(rowType as TypeName);
const dataCells = defenderTypes.map(colType => {
const cell = cellMap.get(`${rowType}:${colType}`);
const total = cell?.total || 0;
const accuracy = cell?.accuracy || 0;
let bgColor = 'var(--bg-accent)';
let textColor = 'var(--text-secondary)';
let display = '-';
if (total > 0) {
display = `${accuracy.toFixed(0)}`;
if (accuracy >= 80) {
bgColor = 'rgba(74, 222, 128, 0.3)';
textColor = 'var(--success)';
} else if (accuracy >= 50) {
bgColor = 'rgba(251, 191, 36, 0.3)';
textColor = '#fbbf24';
} else {
bgColor = 'rgba(248, 113, 113, 0.3)';
textColor = 'var(--error)';
}
}
return `<div class="full-chart-cell" style="background: ${bgColor}; color: ${textColor};" title="${rowType} vs ${colType}: ${cell?.correct || 0}/${total}">
${display}
</div>`;
}).join('');
return `
<div class="full-chart-row">
<div class="full-chart-row-header" style="background: ${rowInfo.backgroundColor}; color: ${rowInfo.color};">
${rowType.slice(0, 3)}
</div>
${dataCells}
</div>
`;
}).join('');
return `
<div class="full-type-chart">
<div class="full-chart-header-row">
<div class="full-chart-corner"></div>
${headerCells}
</div>
${rows}
</div>
`;
}
function renderStatsPage(): void {
const stats = game.getStats();
const bestCombos = game.getBestCombinations(5);
const worstCombos = game.getWorstCombinations(5);
const typeChartStats = typeChartView === 'attacking' ? game.getTypeChartStats() : game.getDefendingTypeStats();
const fullTypeChartCells = game.getFullTypeChartStats();
const hasData = stats.total > 0;
const bestCombosHtml = bestCombos.length > 0
? bestCombos.map(renderCombinationItem).join('')
: '<p class="no-data-text">Answer at least 3 questions to see stats</p>';
const worstCombosHtml = worstCombos.length > 0
? worstCombos.map(renderCombinationItem).join('')
: '<p class="no-data-text">Answer at least 3 questions to see stats</p>';
const typeChartHtml = typeChartStats.map(renderTypeChartCell).join('');
app.innerHTML = `
<div class="stats-container">
<header class="stats-header">
<button class="back-btn" id="backBtn">← Back to Quiz</button>
<h1>Your Statistics</h1>
</header>
<div class="stats-overview">
<div class="stat-card">
<div class="stat-value">${stats.total}</div>
<div class="stat-label">Total Questions</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.accuracy.toFixed(1)}%</div>
<div class="stat-label">Accuracy</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.correct}</div>
<div class="stat-label">Correct</div>
</div>
</div>
${hasData ? `
<section class="stats-section">
<h2>Best Combinations</h2>
<p class="section-subtitle">Highest accuracy (min. 3 attempts)</p>
<div class="combo-list">
${bestCombosHtml}
</div>
</section>
<section class="stats-section">
<h2>Needs Practice</h2>
<p class="section-subtitle">Lowest accuracy (min. 3 attempts)</p>
<div class="combo-list">
${worstCombosHtml}
</div>
</section>
<section class="stats-section">
<h2>Type Chart Performance</h2>
<div class="chart-toggle-row">
<button class="chart-toggle ${typeChartView === 'attacking' ? 'active' : ''}" data-view="attacking">Attacking</button>
<button class="chart-toggle ${typeChartView === 'defending' ? 'active' : ''}" data-view="defending">Defending</button>
</div>
<p class="section-subtitle">Accuracy by ${typeChartView} type</p>
<div class="type-chart">
${typeChartHtml}
</div>
</section>
<section class="stats-section full-chart-section">
<h2>Full Type Chart</h2>
<p class="section-subtitle">Accuracy for each type combination</p>
${renderFullTypeChart(fullTypeChartCells)}
</section>
` : `
<div class="no-data-message">
<p>No data yet. Start playing to track your statistics!</p>
</div>
`}
</div>
`;
document.getElementById('backBtn')?.addEventListener('click', () => {
currentView = 'quiz';
render();
});
document.querySelectorAll('.chart-toggle').forEach(btn => {
btn.addEventListener('click', () => {
const view = btn.getAttribute('data-view') as 'attacking' | 'defending';
typeChartView = view;
render();
});
});
}
function attachEventListeners(): void {
document.querySelectorAll('.option-btn').forEach(btn => {
btn.addEventListener('click', () => {
@@ -126,6 +337,14 @@ function attachEventListeners(): void {
game.nextQuestion();
});
}
const statsBtn = document.getElementById('statsBtn');
if (statsBtn) {
statsBtn.addEventListener('click', () => {
currentView = 'stats';
render();
});
}
}
document.addEventListener('keydown', (e) => {

View File

@@ -309,6 +309,340 @@ body {
color: var(--text-secondary);
}
/* Stats Button */
.stats-btn {
padding: 6px 14px;
background: var(--bg-accent);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
color: var(--text-primary);
font-family: 'Outfit', sans-serif;
font-size: 0.8rem;
cursor: pointer;
transition: var(--transition);
}
.stats-btn:hover {
background: var(--accent);
border-color: var(--accent);
}
/* Stats Page */
.stats-container {
display: flex;
flex-direction: column;
gap: 24px;
}
.stats-header {
display: flex;
align-items: center;
gap: 16px;
}
.stats-header h1 {
font-size: 1.8rem;
font-weight: 700;
background: linear-gradient(135deg, var(--accent), #ff6b6b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.back-btn {
padding: 10px 16px;
background: var(--bg-card);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: var(--text-primary);
font-family: 'Outfit', sans-serif;
font-size: 0.9rem;
cursor: pointer;
transition: var(--transition);
}
.back-btn:hover {
border-color: var(--accent);
background: rgba(233, 69, 96, 0.1);
}
.stats-overview {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.stat-card {
background: var(--bg-card);
border-radius: var(--border-radius);
padding: 20px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.stat-card .stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--accent);
font-family: 'JetBrains Mono', monospace;
}
.stat-card .stat-label {
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: 4px;
}
.stats-section {
background: var(--bg-card);
border-radius: var(--border-radius);
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.stats-section h2 {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 4px;
}
.section-subtitle {
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: 16px;
}
.combo-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.combo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--bg-accent);
border-radius: 8px;
}
.combo-types {
display: flex;
align-items: center;
gap: 8px;
}
.combo-vs {
font-size: 0.75rem;
color: var(--text-secondary);
font-family: 'JetBrains Mono', monospace;
}
.combo-stats {
display: flex;
align-items: center;
gap: 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
}
.combo-accuracy {
font-weight: 600;
}
.combo-accuracy.high {
color: var(--success);
}
.combo-accuracy.medium {
color: #fbbf24;
}
.combo-accuracy.low {
color: var(--error);
}
.combo-count {
color: var(--text-secondary);
font-size: 0.75rem;
}
/* Type Chart */
.type-chart {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 6px;
}
.type-cell {
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 8px;
font-size: 0.7rem;
font-weight: 600;
transition: var(--transition);
}
.type-cell:hover {
transform: scale(1.05);
}
.type-cell-name {
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.9;
}
.type-cell-accuracy {
font-family: 'JetBrains Mono', monospace;
font-size: 0.65rem;
margin-top: 2px;
padding: 2px 4px;
border-radius: 4px;
background: rgba(0, 0, 0, 0.2);
}
.type-cell-accuracy.high {
color: #4ade80;
}
.type-cell-accuracy.medium {
color: #fbbf24;
}
.type-cell-accuracy.low {
color: #f87171;
}
.type-cell-accuracy.no-data {
color: rgba(255, 255, 255, 0.5);
}
.no-data-message {
text-align: center;
padding: 48px 24px;
background: var(--bg-card);
border-radius: var(--border-radius);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.no-data-message p {
color: var(--text-secondary);
font-size: 1rem;
}
.no-data-text {
color: var(--text-secondary);
font-size: 0.9rem;
text-align: center;
padding: 16px;
}
/* Full Type Chart */
.full-chart-section {
overflow: hidden;
}
.chart-toggle-row {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.chart-toggle {
padding: 8px 16px;
background: var(--bg-accent);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: var(--text-secondary);
font-family: 'Outfit', sans-serif;
font-size: 0.85rem;
cursor: pointer;
transition: var(--transition);
}
.chart-toggle:hover {
border-color: var(--accent);
}
.chart-toggle.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.full-type-chart {
display: grid;
grid-template-columns: 50px repeat(18, 1fr);
gap: 2px;
font-size: 0.65rem;
}
.full-chart-header-row {
display: contents;
}
.full-chart-corner {
width: 50px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-accent);
border-radius: 3px;
}
.full-chart-header {
width: 100%;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.55rem;
font-weight: 600;
text-transform: uppercase;
border-radius: 3px;
}
.full-chart-row {
display: contents;
}
.full-chart-row-header {
width: 50px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.55rem;
font-weight: 600;
text-transform: uppercase;
border-radius: 3px;
}
.full-chart-cell {
width: 100%;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'JetBrains Mono', monospace;
font-size: 0.55rem;
font-weight: 600;
border-radius: 3px;
transition: var(--transition);
}
.full-chart-cell:hover {
transform: scale(1.15);
z-index: 1;
border: 1px solid rgba(255, 255, 255, 0.5);
}
@media (max-width: 480px) {
body {
padding: 12px;