-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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