Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created August 3, 2025 06:35
Show Gist options
  • Select an option

  • Save shricodev/1bab131d479a3462ad88825ace3560c7 to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/1bab131d479a3462ad88825ace3560c7 to your computer and use it in GitHub Desktop.
Typing Game similar to MonkeyType (Developed by Kimi K2 AI Model) - Blog Demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FlameType - Modern Typing Game</title>
<link rel="stylesheet" href="styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
</head>
<body data-theme="mocha">
<div class="app-container">
<!-- Header -->
<header class="header">
<h1 class="logo">FlameType</h1>
<div class="theme-toggle">
<button class="theme-btn" data-theme="latte">🌞</button>
<button class="theme-btn" data-theme="frappe">🌅</button>
<button class="theme-btn" data-theme="macchiato">🌆</button>
<button class="theme-btn active" data-theme="mocha">🌙</button>
</div>
</header>
<!-- Stats Bar -->
<div class="stats-bar">
<div class="stat">
<span class="stat-label">WPM</span>
<span class="stat-value" id="wpm">0</span>
</div>
<div class="stat">
<span class="stat-label">Accuracy</span>
<span class="stat-value" id="accuracy">100%</span>
</div>
<div class="stat">
<span class="stat-label">Time</span>
<span class="stat-value" id="time">0:00</span>
</div>
<div class="stat">
<span class="stat-label">Combo</span>
<span class="stat-value" id="combo">0</span>
</div>
</div>
<!-- Typing Area -->
<main class="typing-area">
<div class="text-container">
<div class="text-display" id="textDisplay"></div>
<div class="fire-trail" id="fireTrail"></div>
</div>
<div class="input-container">
<input type="text" id="textInput" class="text-input" placeholder="Click here to start typing..." autocomplete="off" spellcheck="false">
</div>
</main>
<!-- Virtual Keyboard -->
<div class="keyboard-container">
<div class="virtual-keyboard" id="virtualKeyboard">
<!-- Keyboard will be generated by JavaScript -->
</div>
</div>
<!-- Results Modal -->
<div class="modal-overlay" id="resultsModal">
<div class="results-modal">
<h2>🎉 Congratulations! 🎉</h2>
<div class="results-stats">
<div class="result-stat">
<span class="result-label">Final WPM</span>
<span class="result-value" id="finalWpm">0</span>
</div>
<div class="result-stat">
<span class="result-label">Accuracy</span>
<span class="result-value" id="finalAccuracy">0%</span>
</div>
<div class="result-stat">
<span class="result-label">Time</span>
<span class="result-value" id="finalTime">0:00</span>
</div>
<div class="result-stat">
<span class="result-label">Errors</span>
<span class="result-value" id="finalErrors">0</span>
</div>
</div>
<button class="restart-btn" onclick="restartGame()">Try Again</button>
<div class="fireworks" id="fireworks"></div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
// Famous texts for typing
const famousTexts = [
"The quick brown fox jumps over the lazy dog. This pangram contains every letter of the alphabet at least once.",
"To be or not to be, that is the question: Whether 'tis nobler in the mind to suffer the slings and arrows of outrageous fortune.",
"Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.",
"It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity.",
"I have a dream that one day this nation will rise up and live out the true meaning of its creed: We hold these truths to be self-evident, that all men are created equal.",
"In the beginning God created the heaven and the earth. And the earth was without form, and void; and darkness was upon the face of the deep.",
"Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world.",
"It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.",
"Happy families are all alike; every unhappy family is unhappy in its own way. Everything was in confusion in the Oblonskys' house.",
"All happy families are alike; each unhappy family is unhappy in its own way. All was confusion in the Oblonskys' house."
];
// Game state
let gameState = {
currentText: '',
currentIndex: 0,
startTime: null,
endTime: null,
errors: 0,
totalChars: 0,
combo: 0,
maxCombo: 0,
isActive: false,
isFinished: false
};
// DOM elements
const textDisplay = document.getElementById('textDisplay');
const textInput = document.getElementById('textInput');
const wpmDisplay = document.getElementById('wpm');
const accuracyDisplay = document.getElementById('accuracy');
const timeDisplay = document.getElementById('time');
const comboDisplay = document.getElementById('combo');
const virtualKeyboard = document.getElementById('virtualKeyboard');
const resultsModal = document.getElementById('resultsModal');
const fireTrail = document.getElementById('fireTrail');
// Theme toggle
const themeButtons = document.querySelectorAll('.theme-btn');
themeButtons.forEach(btn => {
btn.addEventListener('click', () => {
const theme = btn.dataset.theme;
document.body.dataset.theme = theme;
themeButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Initialize game
function initGame() {
gameState.currentText = famousTexts[Math.floor(Math.random() * famousTexts.length)];
gameState.currentIndex = 0;
gameState.startTime = null;
gameState.endTime = null;
gameState.errors = 0;
gameState.totalChars = 0;
gameState.combo = 0;
gameState.maxCombo = 0;
gameState.isActive = false;
gameState.isFinished = false;
renderText();
createVirtualKeyboard();
resetStats();
textInput.value = '';
textInput.focus();
}
// Render text with spans for each character
function renderText() {
textDisplay.innerHTML = '';
const chars = gameState.currentText.split('');
chars.forEach((char, index) => {
const span = document.createElement('span');
span.className = 'char';
span.textContent = char === ' ' ? '·' : char;
span.dataset.index = index;
if (char === ' ') {
span.classList.add('space');
}
textDisplay.appendChild(span);
});
}
// Create virtual keyboard
function createVirtualKeyboard() {
const keyboardLayout = [
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
['z', 'x', 'c', 'v', 'b', 'n', 'm'],
[' ']
];
virtualKeyboard.innerHTML = '';
keyboardLayout.forEach(row => {
const rowDiv = document.createElement('div');
rowDiv.className = 'keyboard-row';
row.forEach(key => {
const keyDiv = document.createElement('div');
keyDiv.className = 'key';
keyDiv.textContent = key === ' ' ? 'SPACE' : key.toUpperCase();
keyDiv.dataset.key = key;
if (key === ' ') {
keyDiv.classList.add('space');
}
rowDiv.appendChild(keyDiv);
});
virtualKeyboard.appendChild(rowDiv);
});
}
// Handle keyboard input
textInput.addEventListener('input', (e) => {
if (gameState.isFinished) return;
if (!gameState.isActive) {
gameState.isActive = true;
gameState.startTime = Date.now();
startTimer();
}
const inputValue = e.target.value;
const currentChar = gameState.currentText[gameState.currentIndex];
if (inputValue.length > gameState.currentIndex + 1) {
// Prevent typing ahead
textInput.value = inputValue.slice(0, gameState.currentIndex + 1);
return;
}
const typedChar = inputValue[gameState.currentIndex];
if (typedChar === currentChar) {
// Correct character
markCharacter(gameState.currentIndex, 'correct');
gameState.combo++;
gameState.maxCombo = Math.max(gameState.maxCombo, gameState.combo);
createFireAnimation(gameState.currentIndex);
// Highlight virtual key
highlightVirtualKey(typedChar);
} else {
// Incorrect character
markCharacter(gameState.currentIndex, 'incorrect');
gameState.errors++;
gameState.combo = 0;
// Add shake animation
const charElement = document.querySelector(`[data-index="${gameState.currentIndex}"]`);
if (charElement) {
charElement.style.animation = 'shake 0.3s ease-in-out';
setTimeout(() => {
charElement.style.animation = '';
}, 300);
}
}
gameState.currentIndex++;
gameState.totalChars++;
// Update current character highlight
updateCurrentHighlight();
// Update stats
updateStats();
// Check if finished
if (gameState.currentIndex >= gameState.currentText.length) {
finishGame();
}
});
// Handle backspace
textInput.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && gameState.currentIndex > 0) {
e.preventDefault();
gameState.currentIndex--;
// Remove character marking
const charElement = document.querySelector(`[data-index="${gameState.currentIndex}"]`);
if (charElement) {
charElement.className = 'char';
}
// Update input value
textInput.value = textInput.value.slice(0, gameState.currentIndex);
// Update current character highlight
updateCurrentHighlight();
}
});
// Mark character as correct or incorrect
function markCharacter(index, status) {
const charElement = document.querySelector(`[data-index="${index}"]`);
if (charElement) {
charElement.classList.add(status);
}
}
// Update current character highlight
function updateCurrentHighlight() {
// Remove previous current highlight
document.querySelectorAll('.char.current').forEach(char => {
char.classList.remove('current');
});
// Add current highlight to next character
const nextChar = document.querySelector(`[data-index="${gameState.currentIndex}"]`);
if (nextChar) {
nextChar.classList.add('current');
}
}
// Create fire animation
function createFireAnimation(index) {
const charElement = document.querySelector(`[data-index="${index}"]`);
if (!charElement) return;
const rect = charElement.getBoundingClientRect();
const fire = document.createElement('div');
fire.className = 'fire-trail';
fire.style.left = rect.left + rect.width / 2 + 'px';
fire.style.top = rect.bottom + 'px';
document.body.appendChild(fire);
// Remove fire element after animation
setTimeout(() => {
fire.remove();
}, 500);
}
// Highlight virtual key
function highlightVirtualKey(key) {
const keyElement = document.querySelector(`[data-key="${key.toLowerCase()}"]`);
if (keyElement) {
keyElement.classList.add('active');
setTimeout(() => {
keyElement.classList.remove('active');
}, 150);
}
}
// Update stats
function updateStats() {
if (!gameState.startTime) return;
const elapsedTime = (Date.now() - gameState.startTime) / 1000 / 60; // minutes
const wpm = Math.round((gameState.currentIndex / 5) / elapsedTime) || 0;
const accuracy = Math.round(((gameState.currentIndex - gameState.errors) / gameState.currentIndex) * 100) || 100;
wpmDisplay.textContent = wpm;
accuracyDisplay.textContent = accuracy + '%';
comboDisplay.textContent = gameState.combo;
}
// Timer
let timerInterval;
function startTimer() {
timerInterval = setInterval(() => {
if (!gameState.startTime || gameState.isFinished) {
clearInterval(timerInterval);
return;
}
const elapsed = Math.floor((Date.now() - gameState.startTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
timeDisplay.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
}, 100);
}
// Reset stats
function resetStats() {
wpmDisplay.textContent = '0';
accuracyDisplay.textContent = '100%';
timeDisplay.textContent = '0:00';
comboDisplay.textContent = '0';
}
// Finish game
function finishGame() {
gameState.isFinished = true;
gameState.endTime = Date.now();
clearInterval(timerInterval);
const elapsedTime = (gameState.endTime - gameState.startTime) / 1000 / 60;
const finalWpm = Math.round((gameState.currentText.length / 5) / elapsedTime);
const finalAccuracy = Math.round(((gameState.currentText.length - gameState.errors) / gameState.currentText.length) * 100);
const finalTime = Math.floor((gameState.endTime - gameState.startTime) / 1000);
// Update results modal
document.getElementById('finalWpm').textContent = finalWpm;
document.getElementById('finalAccuracy').textContent = finalAccuracy + '%';
document.getElementById('finalTime').textContent = `${Math.floor(finalTime / 60)}:${(finalTime % 60).toString().padStart(2, '0')}`;
document.getElementById('finalErrors').textContent = gameState.errors;
// Show results modal
resultsModal.classList.add('show');
// Create fireworks
createFireworks();
}
// Create fireworks animation
function createFireworks() {
const colors = ['var(--ctp-mauve)', 'var(--ctp-pink)', 'var(--ctp-green)', 'var(--ctp-yellow)', 'var(--ctp-sky)'];
for (let i = 0; i < 50; i++) {
setTimeout(() => {
const confetti = document.createElement('div');
confetti.className = 'confetti';
confetti.style.left = Math.random() * 100 + 'vw';
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
confetti.style.animationDelay = Math.random() * 0.5 + 's';
document.body.appendChild(confetti);
setTimeout(() => {
confetti.remove();
}, 3000);
}, i * 50);
}
}
// Restart game
function restartGame() {
resultsModal.classList.remove('show');
initGame();
}
// Focus input on click
document.addEventListener('click', () => {
if (!gameState.isFinished) {
textInput.focus();
}
});
// Prevent context menu on virtual keyboard
document.addEventListener('contextmenu', (e) => {
if (e.target.classList.contains('key')) {
e.preventDefault();
}
});
// Initialize game on load
initGame();
/* Catppuccin Theme Variables */
:root {
/* Catppuccin Mocha (Default) */
--ctp-rosewater: #f5e0dc;
--ctp-flamingo: #f2cdcd;
--ctp-pink: #f5c2e7;
--ctp-mauve: #cba6f7;
--ctp-red: #f38ba8;
--ctp-maroon: #eba0ac;
--ctp-peach: #fab387;
--ctp-yellow: #f9e2af;
--ctp-green: #a6e3a1;
--ctp-teal: #94e2d5;
--ctp-sky: #89dceb;
--ctp-sapphire: #74c7ec;
--ctp-blue: #89b4fa;
--ctp-lavender: #b4befe;
--ctp-text: #cdd6f4;
--ctp-subtext1: #bac2de;
--ctp-subtext0: #a6adc8;
--ctp-overlay2: #9399b2;
--ctp-overlay1: #7f849c;
--ctp-overlay0: #6c7086;
--ctp-surface2: #585b70;
--ctp-surface1: #45475a;
--ctp-surface0: #313244;
--ctp-base: #1e1e2e;
--ctp-mantle: #181825;
--ctp-crust: #11111b;
/* Typography */
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
/* Transitions */
--transition-fast: 0.15s ease;
--transition-normal: 0.3s ease;
--transition-slow: 0.5s ease;
}
/* Catppuccin Latte Theme */
[data-theme="latte"] {
--ctp-rosewater: #dc8a78;
--ctp-flamingo: #dd7878;
--ctp-pink: #ea76cb;
--ctp-mauve: #8839ef;
--ctp-red: #d20f39;
--ctp-maroon: #e64553;
--ctp-peach: #fe640b;
--ctp-yellow: #df8e1d;
--ctp-green: #40a02b;
--ctp-teal: #179299;
--ctp-sky: #04a5e5;
--ctp-sapphire: #209fb5;
--ctp-blue: #1e66f5;
--ctp-lavender: #7287fd;
--ctp-text: #4c4f69;
--ctp-subtext1: #5c5f77;
--ctp-subtext0: #6c6f85;
--ctp-overlay2: #7c7f93;
--ctp-overlay1: #8c8fa1;
--ctp-overlay0: #9ca0b0;
--ctp-surface2: #acb0be;
--ctp-surface1: #bcc0cc;
--ctp-surface0: #ccd0da;
--ctp-base: #eff1f5;
--ctp-mantle: #e6e9ef;
--ctp-crust: #dce0e8;
}
/* Catppuccin Frappe Theme */
[data-theme="frappe"] {
--ctp-rosewater: #f2d5cf;
--ctp-flamingo: #eebebe;
--ctp-pink: #f4b8e4;
--ctp-mauve: #ca9ee6;
--ctp-red: #e78284;
--ctp-maroon: #ea999c;
--ctp-peach: #ef9f76;
--ctp-yellow: #e5c890;
--ctp-green: #a6d189;
--ctp-teal: #81c8be;
--ctp-sky: #99d1db;
--ctp-sapphire: #85c1dc;
--ctp-blue: #8caaee;
--ctp-lavender: #babbf1;
--ctp-text: #c6d0f5;
--ctp-subtext1: #b5bfe2;
--ctp-subtext0: #a5adce;
--ctp-overlay2: #949cbb;
--ctp-overlay1: #838ba7;
--ctp-overlay0: #737994;
--ctp-surface2: #626880;
--ctp-surface1: #51576d;
--ctp-surface0: #414559;
--ctp-base: #303446;
--ctp-mantle: #292c3c;
--ctp-crust: #232634;
}
/* Catppuccin Macchiato Theme */
[data-theme="macchiato"] {
--ctp-rosewater: #f4dbd6;
--ctp-flamingo: #f0c6c6;
--ctp-pink: #f5bde6;
--ctp-mauve: #c6a0f6;
--ctp-red: #ed8796;
--ctp-maroon: #ee99a0;
--ctp-peach: #f5a97f;
--ctp-yellow: #eed49f;
--ctp-green: #a6da95;
--ctp-teal: #8bd5ca;
--ctp-sky: #91d7e3;
--ctp-sapphire: #7dc4e4;
--ctp-blue: #8aadf4;
--ctp-lavender: #b7bdf8;
--ctp-text: #cad3f5;
--ctp-subtext1: #b8c0e0;
--ctp-subtext0: #a5adcb;
--ctp-overlay2: #939ab7;
--ctp-overlay1: #8087a2;
--ctp-overlay0: #6e738d;
--ctp-surface2: #5b6078;
--ctp-surface1: #494d64;
--ctp-surface0: #363a4f;
--ctp-base: #24273a;
--ctp-mantle: #1e2030;
--ctp-crust: #181926;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-mono);
background-color: var(--ctp-base);
color: var(--ctp-text);
line-height: 1.6;
transition: background-color var(--transition-normal), color var(--transition-normal);
overflow-x: hidden;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing-lg);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-2xl);
padding: var(--spacing-md) 0;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
color: var(--ctp-mauve);
letter-spacing: -0.02em;
}
.theme-selector {
display: flex;
gap: var(--spacing-sm);
}
.theme-btn {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: 0.5rem;
background: var(--ctp-surface0);
color: var(--ctp-text);
cursor: pointer;
transition: all var(--transition-fast);
font-family: var(--font-sans);
font-size: 0.875rem;
}
.theme-btn:hover {
background: var(--ctp-surface1);
transform: translateY(-1px);
}
.theme-btn.active {
background: var(--ctp-mauve);
color: var(--ctp-crust);
}
/* Stats */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-2xl);
}
.stat-card {
background: var(--ctp-surface0);
padding: var(--spacing-lg);
border-radius: 1rem;
text-align: center;
box-shadow: var(--shadow-md);
transition: transform var(--transition-fast);
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-label {
font-size: 0.875rem;
color: var(--ctp-subtext0);
margin-bottom: var(--spacing-xs);
font-family: var(--font-sans);
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--ctp-mauve);
}
/* Typing Area */
.typing-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--spacing-2xl);
}
.typing-text {
font-size: 1.5rem;
line-height: 2;
max-width: 800px;
text-align: left;
padding: var(--spacing-xl);
background: var(--ctp-surface0);
border-radius: 1rem;
box-shadow: var(--shadow-lg);
position: relative;
overflow: hidden;
}
.word {
display: inline-block;
margin-right: 0.5em;
}
.char {
position: relative;
display: inline-block;
transition: all var(--transition-fast);
}
.char.correct {
color: var(--ctp-green);
}
.char.incorrect {
color: var(--ctp-red);
text-decoration: underline;
text-decoration-color: var(--ctp-red);
animation: shake 0.3s ease-in-out;
}
.char.current {
background: var(--ctp-surface2);
border-radius: 0.25rem;
}
.char.space {
width: 0.25em;
}
/* Fire Animation */
.fire-trail {
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 0;
background: linear-gradient(to top, var(--ctp-peach), var(--ctp-yellow), transparent);
border-radius: 50%;
animation: fire-rise 0.5s ease-out forwards;
pointer-events: none;
z-index: 10;
}
@keyframes fire-rise {
0% {
height: 0;
opacity: 1;
}
100% {
height: 20px;
opacity: 0;
transform: translateX(-50%) translateY(-10px);
}
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}
/* Virtual Keyboard */
.keyboard {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xl);
}
.keyboard-row {
display: flex;
gap: var(--spacing-xs);
}
.key {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.5rem;
color: var(--ctp-text);
font-family: var(--font-mono);
font-size: 0.875rem;
min-width: 2.5rem;
text-align: center;
transition: all var(--transition-fast);
box-shadow: var(--shadow-sm);
}
.key:hover {
background: var(--ctp-surface1);
transform: translateY(-1px);
}
.key.active {
background: var(--ctp-mauve);
color: var(--ctp-crust);
box-shadow: 0 0 15px var(--ctp-mauve);
transform: translateY(-2px);
}
.key.space {
min-width: 12rem;
}
/* Results Modal */
.results-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all var(--transition-normal);
}
.results-modal.show {
opacity: 1;
visibility: visible;
}
.results-content {
background: var(--ctp-surface0);
padding: var(--spacing-2xl);
border-radius: 1.5rem;
max-width: 500px;
width: 90%;
text-align: center;
box-shadow: var(--shadow-xl);
transform: scale(0.9);
transition: transform var(--transition-normal);
}
.results-modal.show .results-content {
transform: scale(1);
}
.results-title {
font-size: 2rem;
color: var(--ctp-mauve);
margin-bottom: var(--spacing-lg);
}
.results-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.results-stat {
padding: var(--spacing-md);
background: var(--ctp-surface1);
border-radius: 0.75rem;
}
.results-stat-label {
font-size: 0.875rem;
color: var(--ctp-subtext0);
margin-bottom: var(--spacing-xs);
}
.results-stat-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--ctp-green);
}
.restart-btn {
padding: var(--spacing-md) var(--spacing-xl);
background: var(--ctp-mauve);
color: var(--ctp-crust);
border: none;
border-radius: 0.75rem;
font-size: 1.125rem;
cursor: pointer;
transition: all var(--transition-fast);
font-family: var(--font-sans);
}
.restart-btn:hover {
background: var(--ctp-pink);
transform: translateY(-2px);
}
/* Confetti Animation */
.confetti {
position: absolute;
width: 10px;
height: 10px;
background: var(--ctp-mauve);
animation: confetti-fall 3s linear forwards;
pointer-events: none;
}
@keyframes confetti-fall {
0% {
transform: translateY(-100vh) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
/* App Container */
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing-lg);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
padding: var(--spacing-md) 0;
}
.logo {
font-size: 2rem;
font-weight: 700;
color: var(--ctp-mauve);
letter-spacing: -0.02em;
}
.theme-toggle {
display: flex;
gap: var(--spacing-sm);
}
.theme-btn {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: 0.5rem;
background: var(--ctp-surface0);
color: var(--ctp-text);
cursor: pointer;
transition: all var(--transition-fast);
font-family: var(--font-sans);
font-size: 0.875rem;
box-shadow: var(--shadow-sm);
}
.theme-btn:hover {
background: var(--ctp-surface1);
transform: translateY(-1px);
}
.theme-btn.active {
background: var(--ctp-mauve);
color: var(--ctp-crust);
box-shadow: 0 0 10px var(--ctp-mauve);
}
/* Stats Bar */
.stats-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-md);
margin-bottom: var(--spacing-2xl);
}
.stat {
background: var(--ctp-surface0);
padding: var(--spacing-lg);
border-radius: 1rem;
text-align: center;
box-shadow: var(--shadow-md);
transition: transform var(--transition-fast);
}
.stat:hover {
transform: translateY(-2px);
}
.stat-label {
font-size: 0.875rem;
color: var(--ctp-subtext0);
margin-bottom: var(--spacing-xs);
font-family: var(--font-sans);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-value {
font-size: 1.75rem;
font-weight: 700;
color: var(--ctp-mauve);
}
/* Typing Area */
.typing-area {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: var(--spacing-2xl);
}
.text-container {
position: relative;
max-width: 800px;
width: 100%;
}
.text-display {
font-size: 1.5rem;
line-height: 2.5;
padding: var(--spacing-xl);
background: var(--ctp-surface0);
border-radius: 1rem;
box-shadow: var(--shadow-lg);
text-align: left;
word-wrap: break-word;
position: relative;
}
.char {
position: relative;
display: inline-block;
transition: all var(--transition-fast);
padding: 0.125rem 0.0625rem;
border-radius: 0.25rem;
}
.char.correct {
color: var(--ctp-green);
background-color: rgba(166, 227, 161, 0.1);
}
.char.incorrect {
color: var(--ctp-red);
background-color: rgba(243, 139, 168, 0.1);
text-decoration: underline;
text-decoration-color: var(--ctp-red);
text-decoration-thickness: 2px;
animation: shake 0.3s ease-in-out;
}
.char.current {
background: var(--ctp-surface2);
color: var(--ctp-text);
box-shadow: 0 0 0 2px var(--ctp-mauve);
}
.char.space {
width: 0.5em;
display: inline-block;
}
/* Input Container */
.input-container {
margin-top: var(--spacing-lg);
width: 100%;
max-width: 800px;
}
.text-input {
width: 100%;
padding: var(--spacing-md);
font-size: 1.25rem;
font-family: var(--font-mono);
background: var(--ctp-surface0);
border: 2px solid var(--ctp-surface1);
border-radius: 0.75rem;
color: var(--ctp-text);
text-align: center;
transition: all var(--transition-fast);
}
.text-input:focus {
outline: none;
border-color: var(--ctp-mauve);
box-shadow: 0 0 0 3px rgba(203, 166, 247, 0.2);
}
.text-input::placeholder {
color: var(--ctp-subtext0);
}
/* Fire Animation */
.fire-trail {
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
width: 6px;
height: 0;
background: linear-gradient(to top, var(--ctp-peach), var(--ctp-yellow), transparent);
border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
animation: fire-rise 0.6s ease-out forwards;
pointer-events: none;
z-index: 10;
filter: blur(0.5px);
}
@keyframes fire-rise {
0% {
height: 0;
opacity: 1;
transform: translateX(-50%) scale(1);
}
50% {
height: 24px;
opacity: 0.8;
transform: translateX(-50%) scale(1.2);
}
100% {
height: 32px;
opacity: 0;
transform: translateX(-50%) translateY(-16px) scale(0.8);
}
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-3px); }
75% { transform: translateX(3px); }
}
/* Virtual Keyboard */
.keyboard-container {
display: flex;
justify-content: center;
margin-bottom: var(--spacing-xl);
}
.virtual-keyboard {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
padding: var(--spacing-lg);
background: var(--ctp-surface0);
border-radius: 1rem;
box-shadow: var(--shadow-md);
}
.keyboard-row {
display: flex;
gap: var(--spacing-xs);
justify-content: center;
}
.key {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--ctp-surface1);
border: 1px solid var(--ctp-surface2);
border-radius: 0.5rem;
color: var(--ctp-text);
font-family: var(--font-mono);
font-size: 0.875rem;
min-width: 2.5rem;
text-align: center;
transition: all var(--transition-fast);
box-shadow: var(--shadow-sm);
user-select: none;
}
.key:hover {
background: var(--ctp-surface2);
transform: translateY(-1px);
}
.key.active {
background: var(--ctp-mauve);
color: var(--ctp-crust);
box-shadow: 0 0 15px var(--ctp-mauve), 0 0 30px var(--ctp-mauve);
transform: translateY(-2px) scale(1.1);
}
.key.space {
min-width: 12rem;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.1em;
}
/* Results Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all var(--transition-normal);
backdrop-filter: blur(4px);
}
.modal-overlay.show {
opacity: 1;
visibility: visible;
}
.results-modal {
background: var(--ctp-surface0);
padding: var(--spacing-2xl);
border-radius: 1.5rem;
max-width: 500px;
width: 90%;
text-align: center;
box-shadow: var(--shadow-xl);
transform: scale(0.9);
transition: transform var(--transition-normal);
border: 1px solid var(--ctp-surface1);
}
.modal-overlay.show .results-modal {
transform: scale(1);
}
.results-modal h2 {
font-size: 2rem;
color: var(--ctp-mauve);
margin-bottom: var(--spacing-lg);
font-weight: 700;
}
.results-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.result-stat {
padding: var(--spacing-lg);
background: var(--ctp-surface1);
border-radius: 0.75rem;
transition: transform var(--transition-fast);
}
.result-stat:hover {
transform: translateY(-2px);
}
.result-label {
font-size: 0.875rem;
color: var(--ctp-subtext0);
margin-bottom: var(--spacing-xs);
font-family: var(--font-sans);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.result-value {
font-size: 1.75rem;
font-weight: 700;
color: var(--ctp-green);
}
.restart-btn {
padding: var(--spacing-md) var(--spacing-xl);
background: var(--ctp-mauve);
color: var(--ctp-crust);
border: none;
border-radius: 0.75rem;
font-size: 1.125rem;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-fast);
font-family: var(--font-sans);
box-shadow: var(--shadow-md);
}
.restart-btn:hover {
background: var(--ctp-pink);
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
/* Fireworks */
.fireworks {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
}
.confetti {
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
animation: confetti-fall 3s linear forwards;
pointer-events: none;
}
@keyframes confetti-fall {
0% {
transform: translateY(-100vh) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
/* Responsive Design */
@media (max-width: 768px) {
.app-container {
padding: var(--spacing-md);
}
.header {
flex-direction: column;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.logo {
font-size: 1.75rem;
}
.stats-bar {
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-sm);
}
.stat {
padding: var(--spacing-md);
}
.stat-value {
font-size: 1.5rem;
}
.text-display {
font-size: 1.25rem;
padding: var(--spacing-lg);
line-height: 2.2;
}
.virtual-keyboard {
padding: var(--spacing-md);
transform: scale(0.9);
}
.key {
min-width: 2.2rem;
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.8rem;
}
.key.space {
min-width: 8rem;
}
}
@media (max-width: 480px) {
.app-container {
padding: var(--spacing-sm);
}
.stats-bar {
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-xs);
}
.stat {
padding: var(--spacing-sm);
}
.stat-value {
font-size: 1.25rem;
}
.text-display {
font-size: 1.1rem;
padding: var(--spacing-md);
line-height: 2;
}
.virtual-keyboard {
transform: scale(0.8);
padding: var(--spacing-sm);
}
.key {
min-width: 2rem;
padding: var(--spacing-xs);
font-size: 0.75rem;
}
.key.space {
min-width: 6rem;
}
.results-modal {
padding: var(--spacing-lg);
margin: var(--spacing-md);
}
.results-modal h2 {
font-size: 1.5rem;
}
.results-stats {
grid-template-columns: 1fr;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment