-
-
Save shricodev/7f9c4f948562f91d28cc9b4494249e8f to your computer and use it in GitHub Desktop.
Typing Game similar to MonkeyType (Developed by Claude Sonnet 4 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>TypeFlame - Modern Typing Game</title> | |
| <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=Fira+Code:wght@300;400;500;600&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="styles.css"> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="logo"> | |
| <span class="flame-icon">🔥</span> | |
| <h1>TypeFlame</h1> | |
| </div> | |
| <div class="theme-toggle"> | |
| <button id="themeToggle" class="theme-btn"> | |
| <span class="theme-icon">🌙</span> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Game Area --> | |
| <main class="main-content"> | |
| <!-- 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">0s</span> | |
| </div> | |
| <div class="stat"> | |
| <span class="stat-label">Streak</span> | |
| <span class="stat-value" id="streak">0</span> | |
| </div> | |
| </div> | |
| <!-- Typing Area --> | |
| <div class="typing-container"> | |
| <div class="text-display" id="textDisplay"> | |
| <!-- Text will be populated by JavaScript --> | |
| </div> | |
| <div class="fire-trail" id="fireTrail"> | |
| <!-- Fire animations will be added here --> | |
| </div> | |
| </div> | |
| <!-- Virtual Keyboard --> | |
| <div class="virtual-keyboard" id="virtualKeyboard"> | |
| <!-- Keyboard will be generated by JavaScript --> | |
| </div> | |
| <!-- Control Buttons --> | |
| <div class="controls"> | |
| <button class="control-btn" id="restartBtn"> | |
| <span>↻</span> Restart | |
| </button> | |
| <button class="control-btn" id="newTextBtn"> | |
| <span>📝</span> New Text | |
| </button> | |
| </div> | |
| </main> | |
| <!-- Results Modal --> | |
| <div class="modal-overlay" id="resultsModal"> | |
| <div class="modal"> | |
| <div class="modal-header"> | |
| <h2>🎉 Great Job!</h2> | |
| </div> | |
| <div class="modal-content"> | |
| <div class="result-stats"> | |
| <div class="result-stat"> | |
| <div class="result-value" id="finalWpm">0</div> | |
| <div class="result-label">Words Per Minute</div> | |
| </div> | |
| <div class="result-stat"> | |
| <div class="result-value" id="finalAccuracy">100%</div> | |
| <div class="result-label">Accuracy</div> | |
| </div> | |
| <div class="result-stat"> | |
| <div class="result-value" id="finalTime">0s</div> | |
| <div class="result-label">Time Taken</div> | |
| </div> | |
| <div class="result-stat"> | |
| <div class="result-value" id="finalStreak">0</div> | |
| <div class="result-label">Best Streak</div> | |
| </div> | |
| </div> | |
| <div class="modal-actions"> | |
| <button class="modal-btn primary" id="tryAgainBtn">Try Again</button> | |
| <button class="modal-btn secondary" id="newQuoteBtn">New Quote</button> | |
| </div> | |
| </div> | |
| </div> | |
| <canvas id="celebrationCanvas"></canvas> | |
| </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 quotes and texts for typing practice | |
| const texts = [ | |
| "The only way to do great work is to love what you do. If you haven't found it yet, keep looking. Don't settle. As with all matters of the heart, you'll know when you find it.", | |
| "Innovation distinguishes between a leader and a follower. Stay hungry, stay foolish, and never stop learning.", | |
| "Life is what happens to you while you're busy making other plans. The future belongs to those who believe in the beauty of their dreams.", | |
| "The greatest glory in living lies not in never falling, but in rising every time we fall. Success is not final, failure is not fatal: it is the courage to continue that counts.", | |
| "In the end, we will remember not the words of our enemies, but the silence of our friends. Darkness cannot drive out darkness; only light can do that.", | |
| "Two things are infinite: the universe and human stupidity; and I'm not sure about the universe. Imagination is more important than knowledge.", | |
| "Be yourself; everyone else is already taken. We are all in the gutter, but some of us are looking at the stars.", | |
| "It is during our darkest moments that we must focus to see the light. The way to get started is to quit talking and begin doing.", | |
| "The future belongs to those who believe in the beauty of their dreams. It does not matter how slowly you go as long as you do not stop.", | |
| "Yesterday is history, tomorrow is a mystery, today is a gift of God, which is why we call it the present." | |
| ]; | |
| // Keyboard layout | |
| 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'] | |
| ]; | |
| // Game state | |
| let gameState = { | |
| currentText: '', | |
| currentIndex: 0, | |
| startTime: null, | |
| endTime: null, | |
| errors: 0, | |
| totalTyped: 0, | |
| streak: 0, | |
| maxStreak: 0, | |
| isGameActive: false, | |
| wpm: 0, | |
| accuracy: 100 | |
| }; | |
| // DOM elements | |
| const textDisplay = document.getElementById('textDisplay'); | |
| const fireTrail = document.getElementById('fireTrail'); | |
| const virtualKeyboard = document.getElementById('virtualKeyboard'); | |
| const wpmElement = document.getElementById('wpm'); | |
| const accuracyElement = document.getElementById('accuracy'); | |
| const timeElement = document.getElementById('time'); | |
| const streakElement = document.getElementById('streak'); | |
| const resultsModal = document.getElementById('resultsModal'); | |
| const celebrationCanvas = document.getElementById('celebrationCanvas'); | |
| const themeToggle = document.getElementById('themeToggle'); | |
| // Initialize the game | |
| function initGame() { | |
| setupKeyboard(); | |
| loadNewText(); | |
| setupEventListeners(); | |
| updateStats(); | |
| } | |
| // Setup virtual keyboard | |
| function setupKeyboard() { | |
| 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.toUpperCase(); | |
| keyDiv.dataset.key = key; | |
| rowDiv.appendChild(keyDiv); | |
| }); | |
| virtualKeyboard.appendChild(rowDiv); | |
| }); | |
| // Add spacebar | |
| const spaceRow = document.createElement('div'); | |
| spaceRow.className = 'keyboard-row'; | |
| const spaceKey = document.createElement('div'); | |
| spaceKey.className = 'key space'; | |
| spaceKey.textContent = 'SPACE'; | |
| spaceKey.dataset.key = ' '; | |
| spaceRow.appendChild(spaceKey); | |
| virtualKeyboard.appendChild(spaceRow); | |
| } | |
| // Load new text for typing | |
| function loadNewText() { | |
| gameState.currentText = texts[Math.floor(Math.random() * texts.length)]; | |
| gameState.currentIndex = 0; | |
| gameState.startTime = null; | |
| gameState.endTime = null; | |
| gameState.errors = 0; | |
| gameState.totalTyped = 0; | |
| gameState.streak = 0; | |
| gameState.isGameActive = false; | |
| renderText(); | |
| clearFireTrail(); | |
| updateStats(); | |
| } | |
| // Render text with character spans | |
| function renderText() { | |
| textDisplay.innerHTML = ''; | |
| for (let i = 0; i < gameState.currentText.length; i++) { | |
| const char = document.createElement('span'); | |
| char.className = 'char'; | |
| char.textContent = gameState.currentText[i]; | |
| if (i === gameState.currentIndex) { | |
| char.classList.add('current'); | |
| } else if (i < gameState.currentIndex) { | |
| char.classList.add('correct'); | |
| } | |
| textDisplay.appendChild(char); | |
| } | |
| } | |
| // Handle keypress | |
| function handleKeyPress(event) { | |
| const key = event.key; | |
| // Prevent default behavior for most keys | |
| if (key !== 'F5' && key !== 'F12') { | |
| event.preventDefault(); | |
| } | |
| // Start game on first keypress | |
| if (!gameState.isGameActive && gameState.currentIndex === 0) { | |
| gameState.startTime = Date.now(); | |
| gameState.isGameActive = true; | |
| } | |
| // Handle typing | |
| if (gameState.currentIndex < gameState.currentText.length) { | |
| const expectedChar = gameState.currentText[gameState.currentIndex]; | |
| const chars = textDisplay.querySelectorAll('.char'); | |
| const currentChar = chars[gameState.currentIndex]; | |
| // Highlight pressed key | |
| highlightKey(key); | |
| if (key === expectedChar) { | |
| // Correct character | |
| currentChar.classList.remove('current'); | |
| currentChar.classList.add('correct'); | |
| // Create fire animation | |
| createFireParticle(currentChar); | |
| gameState.currentIndex++; | |
| gameState.totalTyped++; | |
| gameState.streak++; | |
| gameState.maxStreak = Math.max(gameState.maxStreak, gameState.streak); | |
| // Check if text is complete | |
| if (gameState.currentIndex >= gameState.currentText.length) { | |
| endGame(); | |
| return; | |
| } | |
| // Update current character | |
| chars[gameState.currentIndex].classList.add('current'); | |
| } else { | |
| // Incorrect character | |
| currentChar.classList.add('incorrect'); | |
| gameState.errors++; | |
| gameState.totalTyped++; | |
| gameState.streak = 0; | |
| // Remove incorrect class after animation | |
| setTimeout(() => { | |
| currentChar.classList.remove('incorrect'); | |
| }, 300); | |
| } | |
| updateStats(); | |
| } | |
| } | |
| // Create fire particle animation | |
| function createFireParticle(charElement) { | |
| const rect = charElement.getBoundingClientRect(); | |
| const containerRect = textDisplay.getBoundingClientRect(); | |
| const particle = document.createElement('div'); | |
| particle.className = 'fire-particle'; | |
| // Position relative to text container | |
| const x = rect.left - containerRect.left + rect.width / 2; | |
| const y = rect.top - containerRect.top + rect.height; | |
| particle.style.left = x + 'px'; | |
| particle.style.top = y + 'px'; | |
| fireTrail.appendChild(particle); | |
| // Remove particle after animation | |
| setTimeout(() => { | |
| if (particle.parentNode) { | |
| particle.parentNode.removeChild(particle); | |
| } | |
| }, 800); | |
| } | |
| // Highlight virtual keyboard key | |
| function highlightKey(key) { | |
| const keyElement = document.querySelector(`[data-key="${key.toLowerCase()}"]`); | |
| if (keyElement) { | |
| keyElement.classList.add('active'); | |
| setTimeout(() => { | |
| keyElement.classList.remove('active'); | |
| }, 150); | |
| } | |
| } | |
| // Clear fire trail | |
| function clearFireTrail() { | |
| fireTrail.innerHTML = ''; | |
| } | |
| // Update statistics | |
| function updateStats() { | |
| if (gameState.startTime && gameState.isGameActive) { | |
| const timeElapsed = (Date.now() - gameState.startTime) / 1000; | |
| const wordsTyped = gameState.currentIndex / 5; // Standard: 5 characters = 1 word | |
| gameState.wpm = Math.round((wordsTyped / timeElapsed) * 60) || 0; | |
| timeElement.textContent = Math.round(timeElapsed) + 's'; | |
| } | |
| if (gameState.totalTyped > 0) { | |
| gameState.accuracy = Math.round(((gameState.totalTyped - gameState.errors) / gameState.totalTyped) * 100); | |
| } | |
| wpmElement.textContent = gameState.wpm; | |
| accuracyElement.textContent = gameState.accuracy + '%'; | |
| streakElement.textContent = gameState.streak; | |
| } | |
| // End game and show results | |
| function endGame() { | |
| gameState.isGameActive = false; | |
| gameState.endTime = Date.now(); | |
| // Final stats calculation | |
| const timeElapsed = (gameState.endTime - gameState.startTime) / 1000; | |
| const wordsTyped = gameState.currentText.length / 5; | |
| gameState.wpm = Math.round((wordsTyped / timeElapsed) * 60); | |
| updateStats(); | |
| showResults(); | |
| } | |
| // Show results modal with celebration | |
| function showResults() { | |
| document.getElementById('finalWpm').textContent = gameState.wpm; | |
| document.getElementById('finalAccuracy').textContent = gameState.accuracy + '%'; | |
| document.getElementById('finalTime').textContent = Math.round((gameState.endTime - gameState.startTime) / 1000) + 's'; | |
| document.getElementById('finalStreak').textContent = gameState.maxStreak; | |
| resultsModal.classList.add('show'); | |
| startCelebration(); | |
| } | |
| // Celebration animation | |
| function startCelebration() { | |
| const canvas = celebrationCanvas; | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| const particles = []; | |
| const colors = ['#f38ba8', '#fab387', '#f9e2af', '#a6e3a1', '#89b4fa', '#cba6f7']; | |
| // Create particles | |
| for (let i = 0; i < 100; i++) { | |
| particles.push({ | |
| x: Math.random() * canvas.width, | |
| y: canvas.height + 10, | |
| vx: (Math.random() - 0.5) * 4, | |
| vy: -Math.random() * 8 - 2, | |
| color: colors[Math.floor(Math.random() * colors.length)], | |
| size: Math.random() * 6 + 2, | |
| gravity: 0.1, | |
| life: 1, | |
| decay: Math.random() * 0.02 + 0.005 | |
| }); | |
| } | |
| function animate() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| for (let i = particles.length - 1; i >= 0; i--) { | |
| const p = particles[i]; | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| p.vy += p.gravity; | |
| p.life -= p.decay; | |
| if (p.life <= 0) { | |
| particles.splice(i, 1); | |
| continue; | |
| } | |
| ctx.save(); | |
| ctx.globalAlpha = p.life; | |
| ctx.fillStyle = p.color; | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| if (particles.length > 0) { | |
| requestAnimationFrame(animate); | |
| } | |
| } | |
| animate(); | |
| } | |
| // Theme toggle functionality | |
| function toggleTheme() { | |
| const body = document.body; | |
| const themeIcon = document.querySelector('.theme-icon'); | |
| if (body.dataset.theme === 'latte') { | |
| body.dataset.theme = ''; | |
| themeIcon.textContent = '🌙'; | |
| localStorage.setItem('theme', 'mocha'); | |
| } else { | |
| body.dataset.theme = 'latte'; | |
| themeIcon.textContent = '☀️'; | |
| localStorage.setItem('theme', 'latte'); | |
| } | |
| } | |
| // Load saved theme | |
| function loadTheme() { | |
| const savedTheme = localStorage.getItem('theme'); | |
| const body = document.body; | |
| const themeIcon = document.querySelector('.theme-icon'); | |
| if (savedTheme === 'latte') { | |
| body.dataset.theme = 'latte'; | |
| themeIcon.textContent = '☀️'; | |
| } else { | |
| body.dataset.theme = ''; | |
| themeIcon.textContent = '🌙'; | |
| } | |
| } | |
| // Setup event listeners | |
| function setupEventListeners() { | |
| // Keyboard events | |
| document.addEventListener('keydown', handleKeyPress); | |
| // Theme toggle | |
| themeToggle.addEventListener('click', toggleTheme); | |
| // Control buttons | |
| document.getElementById('restartBtn').addEventListener('click', () => { | |
| loadNewText(); | |
| }); | |
| document.getElementById('newTextBtn').addEventListener('click', () => { | |
| loadNewText(); | |
| }); | |
| // Modal buttons | |
| document.getElementById('tryAgainBtn').addEventListener('click', () => { | |
| resultsModal.classList.remove('show'); | |
| loadNewText(); | |
| }); | |
| document.getElementById('newQuoteBtn').addEventListener('click', () => { | |
| resultsModal.classList.remove('show'); | |
| loadNewText(); | |
| }); | |
| // Close modal on background click | |
| resultsModal.addEventListener('click', (e) => { | |
| if (e.target === resultsModal) { | |
| resultsModal.classList.remove('show'); | |
| } | |
| }); | |
| // Focus on text display for better UX | |
| textDisplay.addEventListener('click', () => { | |
| textDisplay.focus(); | |
| }); | |
| // Prevent context menu on right click | |
| document.addEventListener('contextmenu', (e) => { | |
| e.preventDefault(); | |
| }); | |
| // Handle window resize for celebration canvas | |
| window.addEventListener('resize', () => { | |
| if (resultsModal.classList.contains('show')) { | |
| celebrationCanvas.width = window.innerWidth; | |
| celebrationCanvas.height = window.innerHeight; | |
| } | |
| }); | |
| } | |
| // Real-time stats update | |
| function startStatsTimer() { | |
| setInterval(() => { | |
| if (gameState.isGameActive) { | |
| updateStats(); | |
| } | |
| }, 100); | |
| } | |
| // Initialize everything when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| loadTheme(); | |
| initGame(); | |
| startStatsTimer(); | |
| // Focus on text display initially | |
| textDisplay.focus(); | |
| textDisplay.setAttribute('tabindex', '0'); | |
| }); | |
| // Handle visibility change to pause/resume game | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.hidden && gameState.isGameActive) { | |
| // Pause game logic could be added here | |
| } | |
| }); | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| // Restart with Ctrl+R or Cmd+R | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'r') { | |
| e.preventDefault(); | |
| loadNewText(); | |
| } | |
| // New text with Ctrl+N or Cmd+N | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'n') { | |
| e.preventDefault(); | |
| loadNewText(); | |
| } | |
| // Toggle theme with Ctrl+T or Cmd+T | |
| if ((e.ctrlKey || e.metaKey) && e.key === 't') { | |
| e.preventDefault(); | |
| toggleTheme(); | |
| } | |
| // Close modal with Escape | |
| if (e.key === 'Escape' && resultsModal.classList.contains('show')) { | |
| resultsModal.classList.remove('show'); | |
| } | |
| }); |
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 Color Themes */ | |
| :root { | |
| /* Mocha Theme (Dark) */ | |
| --ctp-base: #1e1e2e; | |
| --ctp-mantle: #181825; | |
| --ctp-crust: #11111b; | |
| --ctp-surface0: #313244; | |
| --ctp-surface1: #45475a; | |
| --ctp-surface2: #585b70; | |
| --ctp-overlay0: #6c7086; | |
| --ctp-overlay1: #7f849c; | |
| --ctp-overlay2: #9399b2; | |
| --ctp-subtext0: #a6adc8; | |
| --ctp-subtext1: #bac2de; | |
| --ctp-text: #cdd6f4; | |
| --ctp-lavender: #b4befe; | |
| --ctp-blue: #89b4fa; | |
| --ctp-sapphire: #74c7ec; | |
| --ctp-sky: #89dceb; | |
| --ctp-teal: #94e2d5; | |
| --ctp-green: #a6e3a1; | |
| --ctp-yellow: #f9e2af; | |
| --ctp-peach: #fab387; | |
| --ctp-maroon: #eba0ac; | |
| --ctp-red: #f38ba8; | |
| --ctp-mauve: #cba6f7; | |
| --ctp-pink: #f5c2e7; | |
| --ctp-flamingo: #f2cdcd; | |
| --ctp-rosewater: #f5e0dc; | |
| } | |
| [data-theme="latte"] { | |
| /* Latte Theme (Light) */ | |
| --ctp-base: #eff1f5; | |
| --ctp-mantle: #e6e9ef; | |
| --ctp-crust: #dce0e8; | |
| --ctp-surface0: #ccd0da; | |
| --ctp-surface1: #bcc0cc; | |
| --ctp-surface2: #acb0be; | |
| --ctp-overlay0: #9ca0b0; | |
| --ctp-overlay1: #8c8fa1; | |
| --ctp-overlay2: #7c7f93; | |
| --ctp-subtext0: #6c6f85; | |
| --ctp-subtext1: #5c5f77; | |
| --ctp-text: #4c4f69; | |
| --ctp-lavender: #7287fd; | |
| --ctp-blue: #1e66f5; | |
| --ctp-sapphire: #209fb5; | |
| --ctp-sky: #04a5e5; | |
| --ctp-teal: #179299; | |
| --ctp-green: #40a02b; | |
| --ctp-yellow: #df8e1d; | |
| --ctp-peach: #fe640b; | |
| --ctp-maroon: #e64553; | |
| --ctp-red: #d20f39; | |
| --ctp-mauve: #8839ef; | |
| --ctp-pink: #ea76cb; | |
| --ctp-flamingo: #dd7878; | |
| --ctp-rosewater: #dc8a78; | |
| } | |
| /* Global Styles */ | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Fira Code', monospace; | |
| background: var(--ctp-base); | |
| color: var(--ctp-text); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| transition: all 0.3s ease; | |
| } | |
| .app { | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Header */ | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1.5rem 2rem; | |
| background: var(--ctp-mantle); | |
| border-bottom: 1px solid var(--ctp-surface0); | |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .flame-icon { | |
| font-size: 1.5rem; | |
| animation: flicker 2s ease-in-out infinite alternate; | |
| } | |
| @keyframes flicker { | |
| 0% { transform: scale(1) rotate(-2deg); } | |
| 100% { transform: scale(1.1) rotate(2deg); } | |
| } | |
| .logo h1 { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| color: var(--ctp-peach); | |
| text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); | |
| } | |
| .theme-toggle { | |
| position: relative; | |
| } | |
| .theme-btn { | |
| background: var(--ctp-surface0); | |
| border: none; | |
| border-radius: 50%; | |
| width: 3rem; | |
| height: 3rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
| } | |
| .theme-btn:hover { | |
| background: var(--ctp-surface1); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); | |
| } | |
| .theme-icon { | |
| font-size: 1.2rem; | |
| transition: transform 0.3s ease; | |
| } | |
| .theme-btn:hover .theme-icon { | |
| transform: rotate(20deg); | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 2rem; | |
| gap: 2rem; | |
| } | |
| /* Stats Bar */ | |
| .stats-bar { | |
| display: flex; | |
| gap: 2rem; | |
| background: var(--ctp-mantle); | |
| padding: 1rem 2rem; | |
| border-radius: 1rem; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); | |
| border: 1px solid var(--ctp-surface0); | |
| } | |
| .stat { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 0.25rem; | |
| } | |
| .stat-label { | |
| font-size: 0.75rem; | |
| color: var(--ctp-subtext0); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .stat-value { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| color: var(--ctp-text); | |
| min-width: 3rem; | |
| text-align: center; | |
| } | |
| /* Typing Container */ | |
| .typing-container { | |
| position: relative; | |
| max-width: 800px; | |
| width: 100%; | |
| background: var(--ctp-mantle); | |
| border-radius: 1rem; | |
| padding: 2rem; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | |
| border: 1px solid var(--ctp-surface0); | |
| } | |
| .text-display { | |
| font-size: 1.25rem; | |
| line-height: 2; | |
| letter-spacing: 0.5px; | |
| color: var(--ctp-text); | |
| position: relative; | |
| z-index: 2; | |
| min-height: 200px; | |
| cursor: text; | |
| } | |
| .char { | |
| position: relative; | |
| transition: all 0.2s ease; | |
| } | |
| .char.correct { | |
| color: var(--ctp-green); | |
| background: rgba(166, 227, 161, 0.1); | |
| border-radius: 3px; | |
| } | |
| .char.incorrect { | |
| color: var(--ctp-red); | |
| background: rgba(243, 139, 168, 0.2); | |
| border-radius: 3px; | |
| animation: shake 0.3s ease-in-out; | |
| } | |
| .char.current { | |
| background: var(--ctp-blue); | |
| color: var(--ctp-base); | |
| border-radius: 3px; | |
| animation: pulse 1s ease-in-out infinite; | |
| } | |
| @keyframes shake { | |
| 0%, 100% { transform: translateX(0); } | |
| 25% { transform: translateX(-2px); } | |
| 75% { transform: translateX(2px); } | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.7; } | |
| } | |
| /* Fire Trail */ | |
| .fire-trail { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 1; | |
| } | |
| .fire-particle { | |
| position: absolute; | |
| width: 4px; | |
| height: 8px; | |
| background: linear-gradient(to top, var(--ctp-red), var(--ctp-yellow)); | |
| border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; | |
| animation: fireRise 0.8s ease-out forwards; | |
| pointer-events: none; | |
| } | |
| @keyframes fireRise { | |
| 0% { | |
| transform: translateY(0) scale(1); | |
| opacity: 1; | |
| } | |
| 50% { | |
| transform: translateY(-10px) scale(1.2); | |
| opacity: 0.8; | |
| } | |
| 100% { | |
| transform: translateY(-20px) scale(0.5); | |
| opacity: 0; | |
| } | |
| } | |
| /* Virtual Keyboard */ | |
| .virtual-keyboard { | |
| background: var(--ctp-mantle); | |
| border-radius: 1rem; | |
| padding: 1rem; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); | |
| border: 1px solid var(--ctp-surface0); | |
| max-width: 800px; | |
| width: 100%; | |
| } | |
| .keyboard-row { | |
| display: flex; | |
| justify-content: center; | |
| gap: 0.25rem; | |
| margin-bottom: 0.25rem; | |
| } | |
| .key { | |
| background: var(--ctp-surface0); | |
| border: 1px solid var(--ctp-surface1); | |
| border-radius: 0.5rem; | |
| padding: 0.5rem; | |
| min-width: 2.5rem; | |
| height: 2.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 0.875rem; | |
| color: var(--ctp-text); | |
| transition: all 0.2s ease; | |
| cursor: pointer; | |
| } | |
| .key:hover { | |
| background: var(--ctp-surface1); | |
| transform: translateY(-1px); | |
| } | |
| .key.active { | |
| background: var(--ctp-blue); | |
| color: var(--ctp-base); | |
| box-shadow: 0 0 15px rgba(137, 180, 250, 0.5); | |
| transform: translateY(-2px); | |
| } | |
| .key.space { | |
| min-width: 12rem; | |
| } | |
| /* Controls */ | |
| .controls { | |
| display: flex; | |
| gap: 1rem; | |
| } | |
| .control-btn { | |
| background: var(--ctp-surface0); | |
| border: 1px solid var(--ctp-surface1); | |
| border-radius: 0.75rem; | |
| padding: 0.75rem 1.5rem; | |
| color: var(--ctp-text); | |
| font-family: inherit; | |
| font-size: 0.875rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .control-btn:hover { | |
| background: var(--ctp-surface1); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); | |
| } | |
| /* Modal */ | |
| .modal-overlay { | |
| 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 0.3s ease; | |
| } | |
| .modal-overlay.show { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| .modal { | |
| background: var(--ctp-mantle); | |
| border-radius: 1rem; | |
| padding: 2rem; | |
| max-width: 500px; | |
| width: 90%; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| border: 1px solid var(--ctp-surface0); | |
| transform: translateY(20px); | |
| transition: transform 0.3s ease; | |
| position: relative; | |
| z-index: 1001; | |
| } | |
| .modal-overlay.show .modal { | |
| transform: translateY(0); | |
| } | |
| .modal-header { | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| } | |
| .modal-header h2 { | |
| font-size: 1.5rem; | |
| color: var(--ctp-text); | |
| } | |
| .result-stats { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .result-stat { | |
| text-align: center; | |
| padding: 1rem; | |
| background: var(--ctp-surface0); | |
| border-radius: 0.75rem; | |
| border: 1px solid var(--ctp-surface1); | |
| } | |
| .result-value { | |
| font-size: 2rem; | |
| font-weight: 600; | |
| color: var(--ctp-blue); | |
| margin-bottom: 0.5rem; | |
| } | |
| .result-label { | |
| font-size: 0.875rem; | |
| color: var(--ctp-subtext0); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .modal-actions { | |
| display: flex; | |
| gap: 1rem; | |
| justify-content: center; | |
| } | |
| .modal-btn { | |
| padding: 0.75rem 1.5rem; | |
| border: none; | |
| border-radius: 0.75rem; | |
| font-family: inherit; | |
| font-size: 0.875rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .modal-btn.primary { | |
| background: var(--ctp-blue); | |
| color: var(--ctp-base); | |
| } | |
| .modal-btn.secondary { | |
| background: var(--ctp-surface0); | |
| color: var(--ctp-text); | |
| border: 1px solid var(--ctp-surface1); | |
| } | |
| .modal-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); | |
| } | |
| /* Celebration Canvas */ | |
| #celebrationCanvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 1000; | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 768px) { | |
| .header { | |
| padding: 1rem; | |
| } | |
| .main-content { | |
| padding: 1rem; | |
| gap: 1.5rem; | |
| } | |
| .stats-bar { | |
| gap: 1rem; | |
| padding: 0.75rem 1rem; | |
| flex-wrap: wrap; | |
| } | |
| .typing-container { | |
| padding: 1.5rem; | |
| } | |
| .text-display { | |
| font-size: 1rem; | |
| line-height: 1.8; | |
| } | |
| .result-stats { | |
| grid-template-columns: 1fr; | |
| gap: 1rem; | |
| } | |
| .modal-actions { | |
| flex-direction: column; | |
| } | |
| .virtual-keyboard { | |
| padding: 0.75rem; | |
| } | |
| .key { | |
| min-width: 2rem; | |
| height: 2rem; | |
| font-size: 0.75rem; | |
| } | |
| .key.space { | |
| min-width: 8rem; | |
| } | |
| } | |
| /* Accessibility */ | |
| @media (prefers-reduced-motion: reduce) { | |
| * { | |
| animation-duration: 0.01ms !important; | |
| animation-iteration-count: 1 !important; | |
| transition-duration: 0.01ms !important; | |
| } | |
| } | |
| /* Focus styles */ | |
| button:focus, | |
| .key:focus { | |
| outline: 2px solid var(--ctp-blue); | |
| outline-offset: 2px; | |
| } | |
| /* High contrast mode support */ | |
| @media (prefers-contrast: high) { | |
| .char.correct { | |
| background: var(--ctp-green); | |
| color: var(--ctp-base); | |
| } | |
| .char.incorrect { | |
| background: var(--ctp-red); | |
| color: var(--ctp-base); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment