Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created August 3, 2025 14:44
Show Gist options
  • Select an option

  • Save shricodev/7f9c4f948562f91d28cc9b4494249e8f to your computer and use it in GitHub Desktop.

Select an option

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
<!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>
// 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');
}
});
/* 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