feat: Complete tablet UI overhaul with popup menu system and help bubble

Major Changes:
- 🪟 Popup Menu System: Converted slide-down navigation to draggable, minimizable popup window
- ❓ Help Bubble: Added contextual help system for admin dashboard (accessible via ? bubble)
- 🎨 Universal Theme Engine: All elements now properly match selected theme across all panels
- 📱 Enhanced Tablet Support: Comprehensive touch gestures, handwriting recognition, and Apple Pencil optimization
- 🖊️ Unified Write Mode: Combined handwriting and keyboard input with large canvas interface

Technical Implementation:
- Draggable popup with minimize/restore functionality and boundary constraints
- Dynamic theme switching using CSS variables throughout all components
- Help system with modal overlay and keyboard shortcuts (ESC to close)
- Responsive design optimizations for mobile, tablet, and desktop
- Comprehensive CSS overrides to eliminate hardcoded colors
- JavaScript fixes for canvas stroke colors and input styling

UI/UX Improvements:
- Clean admin dashboard without instruction clutter
- Professional popup interface with smooth animations
- Better contrast and readability across all themes (default, playground, chalkboard, galaxy, nature)
- Improved accessibility with proper focus management
- Enhanced mobile responsiveness with touch-optimized controls

Files Modified:
- Menu system: tablet-menu.js, tablet-menu.css
- Theme engine: styles.css, themes integration
- Admin interface: index.html, ui.js
- Input systems: game.js, unified-write.js, handwriting.js
- Authentication: auth.js with tablet menu cleanup
This commit is contained in:
nmemmert committed 2025-10-02 14:26:04 -04:00
1 parent 0b9d46fed1
commit 8d37038177
24 files changed
+9506 -126

No files matched your search

+421
View File
@@ -0,0 +1,421 @@
/* Handwriting Recognition Styles */
/* Canvas container */
.handwriting-container {
background: var(--bg-primary);
border-radius: 16px;
padding: 1.5rem;
margin: 1.5rem auto;
box-shadow: var(--shadow-lg);
border: 2px solid var(--border);
max-width: 600px;
text-align: center;
}
.handwriting-container.active {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Canvas element */
#handwritingCanvas {
border: 2px solid var(--border);
border-radius: 12px;
background: white;
cursor: crosshair;
display: block;
margin: 1rem auto;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
touch-action: none; /* Prevent scrolling on touch devices */
}
#handwritingCanvas:focus {
outline: none;
border-color: var(--primary);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Canvas controls */
.canvas-controls {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin: 1rem 0;
flex-wrap: wrap;
}
.canvas-controls button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
min-height: 44px;
min-width: 44px;
}
.canvas-controls .clear-btn {
background-color: var(--error);
color: white;
}
.canvas-controls .clear-btn:hover {
background-color: #dc2626;
transform: translateY(-1px);
}
.canvas-controls .recognize-btn {
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
color: white;
}
.canvas-controls .recognize-btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.canvas-controls .recognize-btn:disabled {
background: var(--text-secondary);
cursor: not-allowed;
transform: none;
}
.canvas-controls .undo-btn {
background-color: var(--warning);
color: white;
}
.canvas-controls .undo-btn:hover {
background-color: #d97706;
transform: translateY(-1px);
}
/* Input mode toggle */
.input-mode-toggle {
display: flex;
background: var(--secondary);
border-radius: 12px;
padding: 0.25rem;
margin: 1.5rem auto;
max-width: 300px;
position: relative;
}
.input-mode-toggle button {
flex: 1;
padding: 0.75rem 1.5rem;
border: none;
background: transparent;
color: var(--text-secondary);
font-weight: 600;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
z-index: 2;
}
.input-mode-toggle button.active {
background: var(--bg-primary);
color: var(--primary);
box-shadow: var(--shadow);
}
.input-mode-toggle button:not(.active):hover {
color: var(--text-primary);
}
/* Recognition status */
.recognition-status {
margin: 1rem 0;
padding: 0.75rem 1rem;
border-radius: 8px;
font-weight: 600;
text-align: center;
min-height: 20px;
}
.recognition-status.processing {
background-color: #fef3c7;
color: #92400e;
border: 1px solid #fcd34d;
}
.recognition-status.success {
background-color: #f0fdf4;
color: var(--success);
border: 1px solid var(--success);
}
.recognition-status.error {
background-color: #fef2f2;
color: var(--error);
border: 1px solid var(--error);
}
.recognition-status.empty {
background: transparent;
border: none;
}
/* Recognized text display */
.recognized-text {
background: var(--secondary);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
font-size: 1.25rem;
font-weight: 600;
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
border: 2px solid var(--border);
}
.recognized-text.has-content {
background: var(--bg-primary);
border-color: var(--primary);
color: var(--primary);
}
/* Handwriting hints */
.handwriting-hints {
background: var(--secondary);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
font-size: 0.875rem;
color: var(--text-secondary);
text-align: left;
}
.handwriting-hints h4 {
color: var(--text-primary);
margin-bottom: 0.5rem;
font-size: 1rem;
}
.handwriting-hints ul {
margin: 0;
padding-left: 1.5rem;
}
.handwriting-hints li {
margin-bottom: 0.25rem;
}
/* Stroke width selector */
.stroke-controls {
display: flex;
align-items: center;
gap: 1rem;
margin: 0.5rem 0;
}
.stroke-controls label {
font-size: 0.875rem;
color: var(--text-secondary);
font-weight: 600;
}
.stroke-width-slider {
width: 100px;
height: 6px;
border-radius: 3px;
background: var(--border);
outline: none;
-webkit-appearance: none;
appearance: none;
}
.stroke-width-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
box-shadow: var(--shadow);
}
.stroke-width-slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
border: none;
box-shadow: var(--shadow);
}
/* Color picker */
.color-controls {
display: flex;
align-items: center;
gap: 1rem;
margin: 0.5rem 0;
}
.color-picker {
width: 40px;
height: 40px;
border: 2px solid var(--border);
border-radius: 8px;
background: none;
cursor: pointer;
padding: 0;
}
.color-picker:hover {
border-color: var(--primary);
}
/* Tablet-specific handwriting styles */
@media screen and (min-width: 768px) and (max-width: 1024px) {
.handwriting-container {
padding: 2rem;
margin: 2rem auto;
max-width: 700px;
}
#handwritingCanvas {
width: 100%;
max-width: 600px;
height: 300px;
}
.canvas-controls {
gap: 1.5rem;
margin: 1.5rem 0;
}
.canvas-controls button {
padding: 1rem 2rem;
font-size: 1.125rem;
min-height: 50px;
min-width: 100px;
}
.input-mode-toggle {
max-width: 400px;
margin: 2rem auto;
}
.input-mode-toggle button {
padding: 1rem 2rem;
font-size: 1.125rem;
}
.recognized-text {
font-size: 1.5rem;
padding: 1.5rem;
min-height: 60px;
}
.handwriting-hints {
padding: 1.5rem;
font-size: 1rem;
}
.handwriting-hints h4 {
font-size: 1.125rem;
}
}
/* Mobile handwriting adjustments */
@media screen and (max-width: 767px) {
.handwriting-container {
margin: 1rem;
padding: 1rem;
}
#handwritingCanvas {
width: 100%;
height: 250px;
}
.canvas-controls {
flex-direction: column;
gap: 0.5rem;
}
.canvas-controls button {
width: 100%;
max-width: 200px;
}
.input-mode-toggle {
max-width: 280px;
}
.stroke-controls,
.color-controls {
justify-content: center;
flex-wrap: wrap;
}
}
/* Animation for canvas drawing */
@keyframes drawStroke {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.drawing-animation {
animation: drawStroke 0.1s ease-out;
}
/* Loading spinner for recognition */
.recognition-spinner {
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 0.5rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Accessibility improvements */
.handwriting-container:focus-within {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.canvas-controls button:focus {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
/* High contrast mode support */
@media (prefers-contrast: high) {
#handwritingCanvas {
border-width: 3px;
border-color: #000;
}
.canvas-controls button {
border: 2px solid currentColor;
}
.recognition-status {
border-width: 2px;
}
}
+339 -58
View File
@@ -2,17 +2,27 @@
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="format-detection" content="telephone=no">
<title>✨ Modern Spelling Practice</title>
<link href="styles.css" rel="stylesheet"/>
<link href="mobile.css" rel="stylesheet"/>
<link href="tablet.css" rel="stylesheet"/>
<link href="tablet-menu.css" rel="stylesheet"/>
<link href="handwriting.css" rel="stylesheet"/>
<link href="native-recognition.css" rel="stylesheet"/>
<link href="unified-write.css" rel="stylesheet"/>
<link href="tab-contrast.css" rel="stylesheet"/>
<link href="badges.css" rel="stylesheet"/>
<link href="analytics.css" rel="stylesheet"/>
<link href="gamification.css" rel="stylesheet"/>
<link href="spaced-repetition.css" rel="stylesheet"/>
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.4.2/sha.js" integrity="sha512-MK4YCs3cv9dRCsEzFP5rehiUXbU0iKdwILVZ2SylXqCFFWOk1F4vGRrGGXc0qbHWM7S6gy4Xv/MGK19vKZbvKA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/3.3.1/sha.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- TensorFlow.js for better handwriting recognition -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.10.0/dist/tf.min.js"></script>
<!-- Fallback for jsSHA library if CDN fails -->
<script>
window.addEventListener('DOMContentLoaded', function() {
@@ -40,6 +50,11 @@
<script src="js/analytics.js"></script>
<script src="js/gamification.js"></script>
<script src="js/spaced-repetition.js"></script>
<script src="js/native-handwriting.js"></script>
<script src="js/handwriting.js"></script>
<script src="js/unified-write.js"></script>
<script src="js/tablet-menu.js"></script>
<script src="js/tablet-utils.js"></script>
<script src="js/main.js"></script>
<!-- Initialization script - loads last -->
<script src="js/chart-safety.js"></script>
@@ -70,50 +85,233 @@
<p id="loginMessage" style="margin-top: 1rem; text-align: center;"></p>
</div>
<!-- Navigation -->
<!-- Enhanced Navigation Menu -->
<div id="nav" class="hidden">
<p><strong id="userInfo"></strong></p>
<div class="flex gap-4 justify-center" style="margin-top: 1rem;">
<button class="hidden" id="adminBtn" onclick="showAdmin()">👨‍💼 Admin Panel</button>
<button class="hidden" id="studentBtn" onclick="showStudent()">🎓 Student Panel</button>
<button id="logoutBtn" onclick="logoutUser()" class="btn-secondary">Sign Out</button>
<div class="nav-header">
<p><strong id="userInfo"></strong></p>
<div class="user-badge" id="userBadge"></div>
</div>
<!-- Student Menu Items -->
<div id="studentNavItems" class="nav-section hidden">
<h4>🎓 Activities</h4>
<div class="nav-grid">
<button onclick="startGame(); closeMenuIfTablet();" class="nav-activity-btn">
<span class="nav-icon">🎮</span>
<div>
<div class="nav-title">Spelling Game</div>
<div class="nav-subtitle">Interactive challenges</div>
</div>
</button>
<button onclick="startTypingPractice(); closeMenuIfTablet();" class="nav-activity-btn">
<span class="nav-icon">⌨️</span>
<div>
<div class="nav-title">Typing Practice</div>
<div class="nav-subtitle">Improve speed & accuracy</div>
</div>
</button>
<button onclick="startBibleTyping(); closeMenuIfTablet();" class="nav-activity-btn">
<span class="nav-icon">📖</span>
<div>
<div class="nav-title">Bible Typing</div>
<div class="nav-subtitle">Spiritual practice</div>
</div>
</button>
<button onclick="startSpacedReview(); closeMenuIfTablet();" class="nav-activity-btn">
<span class="nav-icon">🔄</span>
<div>
<div class="nav-title">Spaced Review</div>
<div class="nav-subtitle">Smart learning</div>
</div>
</button>
</div>
<h4>📊 Progress</h4>
<div class="nav-list">
<button onclick="showStudent(); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">📈</span>Dashboard
</button>
<button onclick="showChallenges(); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">🎯</span>Challenges
</button>
<button onclick="showLeaderboards(); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">🏆</span>Leaderboards
</button>
<button onclick="showBadges(); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">🏅</span>My Badges
</button>
</div>
<h4>⚙️ Settings</h4>
<div class="nav-list">
<button onclick="showThemeSelector(); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">🎨</span>Themes
</button>
<button onclick="showInputSettings(); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">✏️</span>Input Method
</button>
</div>
</div>
<!-- Admin Menu Items -->
<div id="adminNavItems" class="nav-section hidden">
<h4>👨‍💼 Administration</h4>
<div class="nav-list">
<button onclick="showAdmin(); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">🏠</span>Dashboard
</button>
<button onclick="showAdminTab('tabUsers'); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">👤</span>User Management
</button>
<button onclick="showAdminTab('tabWords'); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">📝</span>Word Lists
</button>
<button onclick="showAdminTab('tabAnalytics'); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">📊</span>Analytics
</button>
<button onclick="showAdminTab('tabResults'); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">📋</span>Results
</button>
<button onclick="showAdminTab('tabReports'); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">📘</span>Reports
</button>
<button onclick="showAdminTab('tabSessions'); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">📅</span>Sessions
</button>
<button onclick="showAdminTab('tabBadges'); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">�</span>Badge System
</button>
<button onclick="showAdminTab('tabPasswords'); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">🔐</span>Passwords
</button>
<button onclick="showAdminTab('tabTheme'); closeMenuIfTablet();" class="nav-list-btn">
<span class="nav-icon">🎨</span>Themes
</button>
</div>
</div>
<!-- Common Actions -->
<div class="nav-section nav-footer">
<button id="logoutBtn" onclick="logoutUser()" class="nav-logout-btn">
<span class="nav-icon">🚪</span>Sign Out
</button>
</div>
</div>
<!-- Admin Panel -->
<div class="hidden" id="adminPanel">
<div class="card-header">
<h2>👨‍💼 Admin Dashboard</h2>
<p style="color: var(--text-secondary);">Manage users, word lists, and view analytics</p>
<!-- Admin Dashboard Overview -->
<div class="card">
<div class="card-header">
<h2>👨‍💼 Admin Dashboard</h2>
<p style="color: var(--text-secondary);">System overview and quick access to admin functions</p>
</div>
<!-- Quick Stats Overview -->
<div class="admin-overview">
<div class="overview-grid">
<div class="stat-card">
<div class="stat-icon">👥</div>
<div class="stat-content">
<div class="stat-number" id="totalUsers">-</div>
<div class="stat-label">Total Users</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📝</div>
<div class="stat-content">
<div class="stat-number" id="totalWordLists">-</div>
<div class="stat-label">Word Lists</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">🎯</div>
<div class="stat-content">
<div class="stat-number" id="totalSessions">-</div>
<div class="stat-label">Active Sessions</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">🏆</div>
<div class="stat-content">
<div class="stat-number" id="totalBadges">-</div>
<div class="stat-label">Badges Awarded</div>
</div>
</div>
</div>
</div>
<!-- Help Bubble for Admin Functions -->
<div class="help-bubble-container">
<button class="help-bubble" id="adminHelpBubble" title="View admin functions guide">
<span class="help-icon">?</span>
</button>
</div>
<!-- Hidden Menu Instructions (shown in popup) -->
<div class="menu-instructions hidden" id="adminHelpContent">
<h3>📱 Access Admin Functions</h3>
<p>Use the menu button (<strong>☰</strong>) in the top-right corner to access all admin functions:</p>
<div class="instruction-grid">
<div class="instruction-item">
<span class="instruction-icon">👤</span>
<span><strong>User Management</strong> - Add/remove users</span>
</div>
<div class="instruction-item">
<span class="instruction-icon">📝</span>
<span><strong>Word Lists</strong> - Manage spelling lists</span>
</div>
<div class="instruction-item">
<span class="instruction-icon">📊</span>
<span><strong>Analytics</strong> - View detailed statistics</span>
</div>
<div class="instruction-item">
<span class="instruction-icon">📋</span>
<span><strong>Results</strong> - Check student performance</span>
</div>
<div class="instruction-item">
<span class="instruction-icon">📘</span>
<span><strong>Reports</strong> - Generate reports</span>
</div>
<div class="instruction-item">
<span class="instruction-icon">⚙️</span>
<span><strong>Settings</strong> - System configuration</span>
</div>
</div>
</div>
</div>
<!-- Admin Tabs -->
<div id="adminTabs">
<button class="active" data-tab="tabUsers" data-emoji="👤">Users</button>
<button data-tab="tabWords" data-emoji="📝">Word Lists</button>
<button data-tab="tabAnalytics" data-emoji="📊">Analytics</button>
<button data-tab="tabResults" data-emoji="📋">Results</button>
<button data-tab="tabReports" data-emoji="📘">Reports</button>
<button data-tab="tabSessions" data-emoji="📅">Sessions</button>
<button data-tab="tabBadges" data-emoji="🏅">Badges</button>
<button data-tab="tabPasswords" data-emoji="🔐">Passwords</button>
<button data-tab="tabTheme" data-emoji="🎨">Themes</button>
<!-- Hidden Admin Tabs - Accessed via Menu -->
<div style="display: none;">
<div id="adminTabs">
<button class="active" data-tab="tabUsers" data-emoji="👤">Users</button>
<button data-tab="tabWords" data-emoji="📝">Word Lists</button>
<button data-tab="tabAnalytics" data-emoji="📊">Analytics</button>
<button data-tab="tabResults" data-emoji="📋">Results</button>
<button data-tab="tabReports" data-emoji="📘">Reports</button>
<button data-tab="tabSessions" data-emoji="📅">Sessions</button>
<button data-tab="tabBadges" data-emoji="🏅">Badges</button>
<button data-tab="tabPasswords" data-emoji="🔐">Passwords</button>
<button data-tab="tabTheme" data-emoji="🎨">Themes</button>
</div>
</div>
<!-- Users Tab -->
<div id="tabUsers" class="adminTab">
<h3>👤 User Management</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1.5rem;">
<div class="admin-grid">
<div class="card">
<h4>Add New User</h4>
<form onsubmit="event.preventDefault(); addNewUser();" style="display: flex; flex-direction: column; gap: 1rem;">
<form onsubmit="event.preventDefault(); addNewUser();" class="form-group">
<input id="newUsername" placeholder="Username" required type="text"/>
<input id="newPassword" placeholder="Password" required type="password"/>
<select id="newRole">
<option value="student">🎓 Student</option>
<option value="admin">👨‍💼 Admin</option>
</select>
<button type="submit" class="btn-success">➕ Add User</button>
<div class="btn-group">
<button type="submit" class="btn-success">➕ Add User</button>
</div>
</form>
<p id="userAddMessage" style="margin-top: 1rem;"></p>
<small id="hashPreview" style="color: var(--text-secondary);"></small>
@@ -129,7 +327,7 @@
<!-- Word Lists Tab -->
<div class="adminTab hidden" id="tabWords">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
<div class="admin-grid">
<div class="card">
<h4>Select User</h4>
<select class="word-user-dropdown" id="wordUserSelect" onchange="loadWordsForSelectedUser()" style="width: 100%;">
@@ -481,7 +679,7 @@
<div class="card">
<div class="card-header">
<h2>🎓 Student Dashboard</h2>
<p style="color: var(--text-secondary);">Choose an activity to start practicing</p>
<p style="color: var(--text-secondary);">Your learning journey starts here</p>
</div>
<!-- Streak & User Stats Section -->
@@ -490,29 +688,66 @@
<div id="userStatsContainer" class="card dashboard-card"></div>
</div>
<!-- Theme Selection for Students -->
<div class="card" style="margin-bottom: 1.5rem;">
<h4>🎨 Choose Your Theme</h4>
<select id="studentThemeSelect" onchange="changeTheme(this.value)" style="width: 100%; max-width: 300px;">
<option value="default">✨ Modern (Default)</option>
<option value="playground">🎪 Playground</option>
<option value="chalkboard">🎓 Chalkboard</option>
<option value="galaxy">🌌 Galaxy</option>
<option value="nature">🌿 Nature</option>
</select>
<!-- Quick Start Section -->
<div class="quick-start-section">
<h3>🚀 Quick Start</h3>
<p>Jump right into your favorite activity:</p>
<div class="quick-actions-grid">
<button onclick="startGame()" class="quick-action-card">
<span class="quick-icon">🎮</span>
<span class="quick-title">Spelling Game</span>
</button>
<button onclick="startTypingPractice()" class="quick-action-card">
<span class="quick-icon">⌨️</span>
<span class="quick-title">Typing Practice</span>
</button>
<button onclick="startBibleTyping()" class="quick-action-card">
<span class="quick-icon">📖</span>
<span class="quick-title">Bible Typing</span>
</button>
<button onclick="startSpacedReview()" class="quick-action-card">
<span class="quick-icon">🔄</span>
<span class="quick-title">Spaced Review</span>
</button>
</div>
</div>
<div class="student-controls">
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; max-width: 900px; margin: 0 auto;">
<button id="startGameBtn" onclick="startGame()" class="btn-success">
🎮 Spelling Game
</button>
<button id="startTypingBtn" onclick="startTypingPractice()" class="btn-warning">
⌨️ Typing Practice
</button>
<button id="startBibleBtn" onclick="startBibleTyping()" class="btn-primary">
📖 Bible Typing
</button>
<!-- Menu Instructions for Students -->
<div class="menu-instructions student-instructions">
<h3>📱 Explore More Features</h3>
<p>Use the menu button (<strong>☰</strong>) to access:</p>
<div class="instruction-grid">
<div class="instruction-item">
<span class="instruction-icon">🎯</span>
<span><strong>Challenges</strong> - Track your goals</span>
</div>
<div class="instruction-item">
<span class="instruction-icon">🏆</span>
<span><strong>Leaderboards</strong> - Compare progress</span>
</div>
<div class="instruction-item">
<span class="instruction-icon">🏅</span>
<span><strong>Badges</strong> - View achievements</span>
</div>
<div class="instruction-item">
<span class="instruction-icon">🎨</span>
<span><strong>Themes</strong> - Customize appearance</span>
</div>
</div>
</div>
<!-- Hidden original content for backwards compatibility -->
<div style="display: none;">
<!-- Theme Selection for Students -->
<div class="card" style="margin-bottom: 1.5rem;">
<h4>🎨 Choose Your Theme</h4>
<select id="studentThemeSelect" onchange="changeTheme(this.value)" style="width: 100%; max-width: 300px;">
<option value="default">✨ Modern (Default)</option>
<option value="playground">🎪 Playground</option>
<option value="chalkboard">🎓 Chalkboard</option>
<option value="galaxy">🌌 Galaxy</option>
<option value="nature">🌿 Nature</option>
</select>
</div>
</div>
@@ -569,16 +804,28 @@
</div>
<div class="word-box" id="wordBox"></div>
<div class="hidden" id="inputSection" style="text-align: center;">
<input id="userInput" placeholder="Type the word you saw..." type="text"
style="max-width: 400px;
color: black !important;
background-color: white !important;
border: 2px solid #333 !important;
font-size: 18px !important;
padding: 12px !important;
visibility: visible !important;
opacity: 1 !important;
text-shadow: none !important;"/>
<!-- Unified Write Mode Container -->
<div id="unifiedWriteContainer"></div>
<!-- Legacy input (hidden, for compatibility) -->
<input id="userInput" type="hidden"/>
<!-- Recognized Text Display -->
<div id="recognizedText" class="recognized-text">Draw to see recognized text here</div>
<!-- Handwriting Tips -->
<div class="handwriting-hints">
<h4>💡 Tips for better recognition:</h4>
<ul>
<li>Write clearly with consistent letter size</li>
<li>Use print letters rather than cursive</li>
<li>Write one word at a time</li>
<li>Allow space between letters</li>
<li>Press firmly but write smoothly</li>
</ul>
</div>
</div>
<button onclick="submitAnswer()" class="btn-primary">Submit Answer</button>
<div id="wordFeedback" style="margin-top: 10px; font-weight: bold; min-height: 24px;"></div>
</div>
@@ -628,8 +875,42 @@
</button>
</div>
<div id="typingPrompt">Get ready to type...</div>
<!-- Input Mode Toggle for Typing Practice -->
<div class="input-mode-toggle">
<button class="active" data-mode="typing" onclick="toggleInputMode('typing')">
⌨️ Type
</button>
<button data-mode="handwriting" onclick="toggleInputMode('handwriting')">
✏️ Write
</button>
</div>
<div style="text-align: center; margin: 2rem 0;">
<!-- Typing Input -->
<input id="typingInput" type="text" style="max-width: 500px;"/>
<!-- Handwriting Input for Typing Practice -->
<div id="typingHandwritingContainer" class="handwriting-container" style="display: none;">
<h4>✏️ Write your response:</h4>
<!-- Canvas for handwriting in typing practice -->
<canvas id="typingHandwritingCanvas" width="600" height="300" style="width: 100%; max-width: 600px; height: 250px; border: 3px solid var(--primary); border-radius: 12px; background: white; display: block; margin: 1rem auto;"></canvas>
<!-- Canvas Controls for typing practice -->
<div class="canvas-controls">
<button onclick="clearTypingHandwriting()" class="clear-btn">🗑️ Clear</button>
<button onclick="recognizeTypingHandwriting()" class="recognize-btn">🔍 Recognize</button>
<button onclick="undoTypingHandwritingStroke()" class="undo-btn">↶ Undo</button>
</div>
<!-- Recognition Status for typing practice -->
<div id="typingRecognitionStatus" class="recognition-status empty"></div>
<!-- Recognized Text Display for typing practice -->
<div id="typingRecognizedText" class="recognized-text">Draw to see recognized text here</div>
</div>
<button onclick="submitTyping()" class="btn-primary">Submit</button>
</div>
<p id="typingFeedback" style="text-align: center; font-weight: 600;"></p>
+8
View File
@@ -72,6 +72,14 @@ async function verifyLogin() {
function logoutUser() {
localStorage.removeItem('loggedInUser');
// Clear current user
window.currentUser = null;
// Close tablet menu if open
if (window.tabletMenuManager && window.tabletMenuManager.isMenuOpen()) {
window.tabletMenuManager.forceCloseMenu();
}
// Hide all panels
document.getElementById('adminPanel')?.classList.add('hidden');
document.getElementById('studentPanel')?.classList.add('hidden');
+1 -1
View File
@@ -62,7 +62,7 @@ const bibleVerses = [
{
reference: "Matthew 6:33",
text: "But seek first the kingdom of God and his righteousness, and all these things will be added to you."
}
},
{
reference: "John 11:36",
text: "Jesus wept."
+105 -19
View File
@@ -7,13 +7,49 @@ let typingWords = [];
let isSpacedRepetitionMode = false;
// 📝 Check user's answer and move to next word regardless of correctness
window.submitAnswer = function() {
const input = document.getElementById('userInput').value.trim();
window.submitAnswer = function(providedAnswer = null) {
// Get input from unified write mode or legacy inputs
let input = '';
// First check if answer was provided directly (from unified write mode)
if (providedAnswer) {
input = providedAnswer.trim();
} else if (window.unifiedWriteMode) {
// Get from unified write mode
input = window.unifiedWriteMode.getText();
if (!input) {
alert('Please enter an answer first!');
return;
}
} else {
// Legacy fallback
const typingInput = document.getElementById('userInput');
const recognizedText = document.getElementById('recognizedText');
const handwritingContainer = document.getElementById('handwritingContainer');
// Check which input mode is active
if (handwritingContainer && handwritingContainer.style.display !== 'none') {
// Handwriting mode - get recognized text
input = recognizedText ? recognizedText.textContent.trim() : '';
if (input === 'Draw to see recognized text here' || input === '') {
alert('Please write something or switch to typing mode');
return;
}
} else {
// Typing mode - get typed input
input = typingInput.value.trim();
if (input === '') {
alert('Please type something or switch to handwriting mode');
return;
}
}
}
// Record the attempt
results.push({
word: currentWord,
attempt: input
attempt: input,
inputMode: handwritingContainer && handwritingContainer.style.display !== 'none' ? 'handwriting' : 'typing'
});
// Show feedback and move to next word regardless of correctness
@@ -21,7 +57,7 @@ window.submitAnswer = function() {
// Correct answer - add visual feedback
const inputEl = document.getElementById('userInput');
inputEl.style.borderColor = 'var(--success)';
inputEl.style.backgroundColor = '#f0fdf4';
inputEl.style.backgroundColor = 'var(--bg-secondary)';
// Add feedback text if element exists
const feedbackEl = document.getElementById('wordFeedback');
@@ -34,7 +70,7 @@ window.submitAnswer = function() {
// Incorrect answer - show correct spelling but continue
const inputEl = document.getElementById('userInput');
inputEl.style.borderColor = 'var(--error)';
inputEl.style.backgroundColor = '#fef2f2';
inputEl.style.backgroundColor = 'var(--bg-secondary)';
// Add feedback text if element exists
const feedbackEl = document.getElementById('wordFeedback');
@@ -63,6 +99,15 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
}
// Add keyboard support for unified write mode
if (window.unifiedWriteMode) {
document.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && document.activeElement === window.unifiedWriteMode.canvas) {
submitAnswer();
}
});
}
});
let currentSentence = '';
@@ -205,23 +250,64 @@ function showWord() {
inputSection.classList.remove('hidden');
inputSection.style.display = 'block';
// Reset and focus the input field with enhanced visibility
const userInput = document.getElementById('userInput');
userInput.value = '';
// Reset input (unified write mode or legacy)
if (window.unifiedWriteMode) {
window.unifiedWriteMode.clearText();
} else {
// Legacy input reset
const userInput = document.getElementById('userInput');
if (userInput) {
userInput.value = '';
userInput.classList.add('visible-input');
}
// Reset handwriting input
if (window.clearHandwriting) {
window.clearHandwriting();
}
// Reset recognition status and text
const recognitionStatus = document.getElementById('recognitionStatus');
const recognizedText = document.getElementById('recognizedText');
if (recognitionStatus) {
recognitionStatus.className = 'recognition-status empty';
recognitionStatus.textContent = '';
}
if (recognizedText) {
recognizedText.textContent = 'Draw to see recognized text here';
recognizedText.className = 'recognized-text';
}
}
// Use the class-based approach instead of inline styles
userInput.classList.add('visible-input');
// Focus with a slight delay to ensure UI is ready
// Focus appropriate input
setTimeout(() => {
userInput.focus();
// Try to insert a cursor by setting a selection range
try {
userInput.setSelectionRange(0, 0);
} catch (e) {
console.log('Could not set selection range:', e);
if (window.unifiedWriteMode) {
// Focus unified write mode
window.unifiedWriteMode.focusCanvas();
console.log('🎯 Unified write mode focused, ready for input');
} else {
// Legacy focus logic
const userInput = document.getElementById('userInput');
const handwritingContainer = document.getElementById('handwritingContainer');
if (handwritingContainer && handwritingContainer.style.display !== 'none') {
// Handwriting mode - focus canvas
const canvas = document.getElementById('handwritingCanvas');
if (canvas) {
canvas.focus();
console.log('🎨 Canvas focused, ready for handwriting');
}
} else if (userInput) {
// Typing mode - focus input
userInput.focus();
try {
userInput.setSelectionRange(0, 0);
} catch (e) {
console.log('Could not set selection range:', e);
}
console.log('🔍 Input focused, ready for typing');
}
}
console.log('🔍 Input focused, ready for typing');
}, 50);
}, 2000);
}
+23 -8
View File
@@ -167,12 +167,12 @@ async function completeChallenge(challengeId, challengeType) {
// Render streak indicator in student panel
function renderStreakIndicator() {
const streakContainer = document.getElementById('streakContainer');
if (!streakContainer || !userProgress) return;
if (!streakContainer) return;
// Clear previous content
streakContainer.innerHTML = '';
const currentStreak = userProgress.streaks.current || 0;
const currentStreak = userProgress?.streaks?.current || 0;
// Create streak element
const streakEl = document.createElement('div');
@@ -191,7 +191,7 @@ function renderStreakIndicator() {
</div>
<div class="streak-record">
<div class="streak-subtitle">Best</div>
<div class="streak-best">${userProgress.streaks.longest || 0}</div>
<div class="streak-best">${userProgress?.streaks?.longest || 0}</div>
</div>
`;
@@ -201,7 +201,7 @@ function renderStreakIndicator() {
// Render user stats in student panel
function renderUserStats() {
const statsContainer = document.getElementById('userStatsContainer');
if (!statsContainer || !userProgress) return;
if (!statsContainer) return;
// Clear previous content
statsContainer.innerHTML = '';
@@ -213,19 +213,19 @@ function renderUserStats() {
// Add stat cards
statsEl.innerHTML = `
<div class="stat-card">
<div class="stat-value">${userProgress.stats.points || 0}</div>
<div class="stat-value">${userProgress?.stats?.points || 0}</div>
<div class="stat-label">Points</div>
</div>
<div class="stat-card">
<div class="stat-value">${userProgress.stats.accuracy ? userProgress.stats.accuracy.toFixed(1) + '%' : '0%'}</div>
<div class="stat-value">${userProgress?.stats?.accuracy ? userProgress.stats.accuracy.toFixed(1) + '%' : '0%'}</div>
<div class="stat-label">Accuracy</div>
</div>
<div class="stat-card">
<div class="stat-value">${userProgress.stats.totalSessions || 0}</div>
<div class="stat-value">${userProgress?.stats?.totalSessions || 0}</div>
<div class="stat-label">Sessions</div>
</div>
<div class="stat-card">
<div class="stat-value">${userProgress.stats.totalWords || 0}</div>
<div class="stat-value">${userProgress?.stats?.totalWords || 0}</div>
<div class="stat-label">Words</div>
</div>
`;
@@ -233,6 +233,21 @@ function renderUserStats() {
statsContainer.appendChild(statsEl);
}
// Initialize containers with default content
function initializeStudentContainers() {
// Initialize streak container immediately
const streakContainer = document.getElementById('streakContainer');
if (streakContainer && !streakContainer.innerHTML.trim()) {
renderStreakIndicator();
}
// Initialize stats container immediately
const statsContainer = document.getElementById('userStatsContainer');
if (statsContainer && !statsContainer.innerHTML.trim()) {
renderUserStats();
}
}
// Render challenges in student panel
function renderChallenges() {
const challengesContainer = document.getElementById('challengesContainer');
File diff suppressed because it is too large. Load diff
+609
View File
@@ -0,0 +1,609 @@
/**
* Native Tablet Handwriting Recognition Integration
* Leverages platform-specific handwriting APIs for better accuracy
*/
class NativeHandwritingRecognition {
constructor() {
this.isSupported = false;
this.recognizer = null;
this.canvas = null;
this.ctx = null;
this.strokes = [];
this.currentStroke = [];
this.isDrawing = false;
this.isAppleDevice = false;
this.applePencilDetected = false;
// Initialize recognition capabilities
this.initializeRecognition();
}
async initializeRecognition() {
console.log('🔍 Checking for native handwriting support...');
// Check for different platform APIs
this.isSupported = await this.detectHandwritingSupport();
if (this.isSupported) {
console.log('✅ Native handwriting recognition available');
await this.setupRecognizer();
} else {
console.log('⚠️ Native handwriting not available, using fallback');
}
}
async detectHandwritingSupport() {
console.log('🔍 Detecting handwriting support on:', navigator.userAgent);
// Detect Apple Pencil / iOS devices specifically
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const isSafari = navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome');
if (isIOS) {
console.log('🍎 iOS device detected - optimizing for Apple Pencil');
this.isAppleDevice = true;
}
// Check for Web Handwriting API (experimental - Edge/Chrome)
if ('createHandwritingRecognizer' in navigator) {
console.log('🖋️ Web Handwriting API detected in', this.getBrowserInfo());
// Test if it actually works
try {
const testQuery = await navigator.queryHandwritingRecognizer({ languages: ['en'] });
if (testQuery) {
console.log('✅ Web Handwriting API is functional');
return true;
} else {
console.log('⚠️ Web Handwriting API exists but query failed');
}
} catch (error) {
console.log('❌ Web Handwriting API test failed:', error);
}
}
// Check for Pointer Events API with pressure sensitivity (Windows tablets)
if ('PointerEvent' in window) {
console.log('👆 Pointer Events API detected - good for Windows tablets');
return true;
}
// Check for Touch Events with force (iPad, Apple Pencil, etc.)
if ('TouchEvent' in window || 'ontouchstart' in window) {
console.log('📱 Touch Events API detected');
if (isIOS) {
console.log('🍎 Apple Pencil support likely available');
}
return true;
}
// Check for Windows Ink API (UWP apps only)
if (typeof Windows !== 'undefined' && Windows.UI && Windows.UI.Input && Windows.UI.Input.Inking) {
console.log('🖊️ Windows Ink API detected (UWP context)');
return true;
}
console.log('⚠️ No native handwriting APIs detected');
return false;
}
getBrowserInfo() {
const ua = navigator.userAgent;
if (ua.includes('Edg/')) return 'Microsoft Edge';
if (ua.includes('Chrome/')) return 'Google Chrome';
if (ua.includes('Firefox/')) return 'Mozilla Firefox';
if (ua.includes('Safari/') && !ua.includes('Chrome/')) return 'Safari';
return 'Unknown Browser';
}
async setupRecognizer() {
try {
// Try Web Handwriting API first (most accurate)
if ('createHandwritingRecognizer' in navigator) {
await this.setupWebHandwritingAPI();
return;
}
// Try Windows Ink API
if (this.setupWindowsInkAPI()) {
return;
}
// Fallback to enhanced pointer/touch recognition
this.setupEnhancedTouchRecognition();
} catch (error) {
console.error('❌ Failed to setup native recognition:', error);
this.isSupported = false;
}
}
async setupWebHandwritingAPI() {
try {
// Configure the recognizer for English text
const recognizerQuery = {
languages: ['en'],
alternatives: 3,
inputType: 'text'
};
// Check if the configuration is supported
const queryResult = await navigator.queryHandwritingRecognizer(recognizerQuery);
if (queryResult) {
this.recognizer = await navigator.createHandwritingRecognizer(recognizerQuery);
console.log('✅ Web Handwriting API recognizer created');
return true;
}
} catch (error) {
console.error('❌ Web Handwriting API setup failed:', error);
}
return false;
}
setupWindowsInkAPI() {
try {
if ('Windows' in window && window.Windows.UI && window.Windows.UI.Input.Inking) {
// Initialize Windows Ink recognizer
const InkRecognizerContainer = Windows.UI.Input.Inking.InkRecognizerContainer;
this.recognizer = new InkRecognizerContainer();
// Set recognition target to words
this.recognizer.setDefaultRecognizer(Windows.UI.Input.Inking.InkRecognitionTarget.allText);
console.log('✅ Windows Ink API recognizer created');
return true;
}
} catch (error) {
console.error('❌ Windows Ink API setup failed:', error);
}
return false;
}
setupEnhancedTouchRecognition() {
// Enhanced touch/pointer recognition with pressure and timing data
this.recognizer = {
type: 'enhanced_touch',
features: {
pressure: 'PointerEvent' in window,
timing: true,
velocity: true
}
};
console.log('✅ Enhanced touch recognition setup complete');
}
initializeCanvas(canvasElement) {
this.canvas = canvasElement;
this.ctx = canvasElement.getContext('2d');
// Enhanced drawing setup
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.lineWidth = 3;
this.ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--text-primary').trim();
this.setupCanvasEvents();
}
setupCanvasEvents() {
// Use pointer events for better tablet support
if ('PointerEvent' in window) {
this.canvas.addEventListener('pointerdown', (e) => this.startStroke(e));
this.canvas.addEventListener('pointermove', (e) => this.continueStroke(e));
this.canvas.addEventListener('pointerup', (e) => this.endStroke(e));
this.canvas.addEventListener('pointercancel', (e) => this.endStroke(e));
} else {
// Fallback to touch events
this.canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
this.startStroke(e.touches[0]);
});
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
this.continueStroke(e.touches[0]);
});
this.canvas.addEventListener('touchend', (e) => {
e.preventDefault();
this.endStroke(e);
});
}
}
startStroke(event) {
this.isDrawing = true;
this.currentStroke = [];
const point = this.getEnhancedPoint(event);
this.currentStroke.push(point);
this.ctx.beginPath();
this.ctx.moveTo(point.x, point.y);
console.log('🖋️ Starting stroke with enhanced data:', point);
}
continueStroke(event) {
if (!this.isDrawing) return;
const point = this.getEnhancedPoint(event);
this.currentStroke.push(point);
this.ctx.lineTo(point.x, point.y);
this.ctx.stroke();
}
endStroke(event) {
if (!this.isDrawing) return;
this.isDrawing = false;
if (this.currentStroke.length > 0) {
// Add timing information to the stroke
const strokeDuration = this.currentStroke[this.currentStroke.length - 1].timestamp - this.currentStroke[0].timestamp;
this.strokes.push({
points: [...this.currentStroke],
duration: strokeDuration,
avgPressure: this.calculateAveragePressure(this.currentStroke)
});
console.log(`✅ Stroke completed: ${this.currentStroke.length} points, ${strokeDuration}ms`);
}
this.currentStroke = [];
}
getEnhancedPoint(event) {
const rect = this.canvas.getBoundingClientRect();
const scaleX = this.canvas.width / rect.width;
const scaleY = this.canvas.height / rect.height;
const point = {
x: (event.clientX - rect.left) * scaleX,
y: (event.clientY - rect.top) * scaleY,
timestamp: Date.now()
};
// Add pressure if available (PointerEvent)
if (event.pressure !== undefined) {
point.pressure = event.pressure;
// Detect Apple Pencil based on pressure characteristics
if (this.isAppleDevice && event.pressure > 0 && event.pressure < 1) {
this.applePencilDetected = true;
console.log('🍎 Apple Pencil detected with pressure:', event.pressure);
}
}
// Add tilt if available (stylus)
if (event.tiltX !== undefined && event.tiltY !== undefined) {
point.tiltX = event.tiltX;
point.tiltY = event.tiltY;
// Apple Pencil specific tilt detection
if (this.isAppleDevice && (Math.abs(event.tiltX) > 0 || Math.abs(event.tiltY) > 0)) {
this.applePencilDetected = true;
console.log('🍎 Apple Pencil tilt detected:', event.tiltX, event.tiltY);
}
}
// Add force if available (3D Touch / Apple Pencil force)
if (event.force !== undefined) {
point.force = event.force;
// Apple Pencil force detection
if (this.isAppleDevice && event.force > 0) {
this.applePencilDetected = true;
console.log('🍎 Apple Pencil force detected:', event.force);
}
}
// Apple Pencil specific optimizations
if (this.applePencilDetected) {
point.isApplePencil = true;
// Apple Pencil typically has more precise coordinates
point.x = Math.round(point.x * 2) / 2; // Sub-pixel precision
point.y = Math.round(point.y * 2) / 2;
}
return point;
}
calculateAveragePressure(stroke) {
const pressures = stroke.filter(p => p.pressure !== undefined).map(p => p.pressure);
return pressures.length > 0 ? pressures.reduce((a, b) => a + b, 0) / pressures.length : 0.5;
}
async recognizeHandwriting() {
if (this.strokes.length === 0) {
console.log('⚠️ No strokes to recognize');
return null;
}
console.log(`🔍 Recognizing ${this.strokes.length} strokes using native APIs...`);
try {
// Try Web Handwriting API first
if (this.recognizer && this.recognizer.startDrawing) {
return await this.recognizeWithWebAPI();
}
// Try Windows Ink API
if (this.recognizer && this.recognizer.recognizeAsync) {
return await this.recognizeWithWindowsInk();
}
// Use enhanced pattern recognition
return this.recognizeWithEnhancedPatterns();
} catch (error) {
console.error('❌ Native recognition failed:', error);
return this.recognizeWithEnhancedPatterns();
}
}
async recognizeWithWebAPI() {
try {
const drawing = this.recognizer.startDrawing();
// Convert our strokes to the Web API format
for (const stroke of this.strokes) {
const apiStroke = drawing.addStroke();
for (const point of stroke.points) {
apiStroke.addPoint({
x: point.x,
y: point.y,
t: point.timestamp
});
}
}
const predictions = await this.recognizer.getPrediction();
if (predictions && predictions.length > 0) {
const bestPrediction = predictions[0];
console.log('✅ Web API recognition:', bestPrediction.text);
return bestPrediction.text;
}
} catch (error) {
console.error('❌ Web API recognition error:', error);
}
return null;
}
async recognizeWithWindowsInk() {
try {
// Convert strokes to Windows Ink format
const inkStrokes = [];
for (const stroke of this.strokes) {
const inkStrokeBuilder = new Windows.UI.Input.Inking.InkStrokeBuilder();
const points = stroke.points.map(p =>
Windows.Foundation.Point(p.x, p.y)
);
const inkStroke = inkStrokeBuilder.createStroke(points);
inkStrokes.push(inkStroke);
}
const recognitionResults = await this.recognizer.recognizeAsync(
inkStrokes,
Windows.UI.Input.Inking.InkRecognitionTarget.allText
);
if (recognitionResults && recognitionResults.length > 0) {
const bestResult = recognitionResults[0];
console.log('✅ Windows Ink recognition:', bestResult.text);
return bestResult.text;
}
} catch (error) {
console.error('❌ Windows Ink recognition error:', error);
}
return null;
}
recognizeWithEnhancedPatterns() {
// Enhanced pattern recognition using the additional data we collected
console.log('🔍 Using enhanced pattern recognition with native data...');
const analysis = this.analyzeNativeStrokes();
// Apple Pencil specific recognition
if (this.applePencilDetected) {
console.log('🍎 Using Apple Pencil optimized recognition...');
const applePencilResult = this.recognizeWithApplePencil(analysis);
if (applePencilResult) {
return applePencilResult;
}
}
// Use the enhanced data to make better predictions
const confidence = this.calculateNativeConfidence(analysis);
if (confidence > 0.7) {
return analysis.predictedText;
}
// Fallback to the original handwriting recognition
return null;
}
recognizeWithApplePencil(analysis) {
console.log('🍎 Apple Pencil recognition with enhanced accuracy...');
// Apple Pencil produces very precise strokes, so we can be more strict with pattern matching
const { strokeCount, avgPressure, avgDuration } = analysis;
// Apple Pencil specific word patterns
const applePencilPatterns = {
'Love': {
expectedStrokes: [4, 6],
expectedPressure: [0.2, 0.8],
expectedDuration: [200, 800],
letterShapes: ['vertical_tall', 'circle_round', 'v_angle', 'curve_open']
},
'love': {
expectedStrokes: [4, 6],
expectedPressure: [0.2, 0.8],
expectedDuration: [200, 800],
letterShapes: ['tall_line', 'round_shape', 'angle_lines', 'open_curve']
},
'Boy': {
expectedStrokes: [3, 5],
expectedPressure: [0.2, 0.8],
expectedDuration: [150, 600],
letterShapes: ['loop_tall', 'circle_round', 'descender_y']
},
'boy': {
expectedStrokes: [3, 5],
expectedPressure: [0.2, 0.8],
expectedDuration: [150, 600],
letterShapes: ['tall_loop', 'round_circle', 'y_descender']
},
'Cat': {
expectedStrokes: [3, 5],
expectedPressure: [0.2, 0.7],
expectedDuration: [120, 500],
letterShapes: ['curve_open', 'curve_round', 'cross_horizontal']
},
'cat': {
expectedStrokes: [3, 5],
expectedPressure: [0.2, 0.7],
expectedDuration: [120, 500],
letterShapes: ['open_curve', 'round_curve', 'horizontal_cross']
}
};
let bestMatch = null;
let bestScore = 0;
for (const [word, pattern] of Object.entries(applePencilPatterns)) {
let score = 0;
// Stroke count match (Apple Pencil is precise)
if (strokeCount >= pattern.expectedStrokes[0] && strokeCount <= pattern.expectedStrokes[1]) {
score += 0.4;
}
// Pressure consistency (Apple Pencil advantage)
if (avgPressure >= pattern.expectedPressure[0] && avgPressure <= pattern.expectedPressure[1]) {
score += 0.3;
}
// Duration consistency (Apple Pencil users tend to be more deliberate)
if (avgDuration >= pattern.expectedDuration[0] && avgDuration <= pattern.expectedDuration[1]) {
score += 0.2;
}
// Apple Pencil precision bonus
score += 0.1;
console.log(`🍎 Apple Pencil score for "${word}": ${score.toFixed(3)}`);
if (score > bestScore && score > 0.6) {
bestScore = score;
bestMatch = word;
}
}
if (bestMatch) {
console.log(`🏆 Apple Pencil recognition: "${bestMatch}" (confidence: ${bestScore.toFixed(3)})`);
return bestMatch;
}
return null;
}
analyzeNativeStrokes() {
// Analyze using the enhanced stroke data
let totalPressure = 0;
let totalDuration = 0;
let pressurePoints = 0;
for (const stroke of this.strokes) {
totalDuration += stroke.duration;
if (stroke.avgPressure > 0) {
totalPressure += stroke.avgPressure;
pressurePoints++;
}
}
const avgPressure = pressurePoints > 0 ? totalPressure / pressurePoints : 0.5;
const avgStrokeDuration = totalDuration / this.strokes.length;
// Use pressure and timing to improve recognition
return {
strokeCount: this.strokes.length,
avgPressure: avgPressure,
avgDuration: avgStrokeDuration,
predictedText: this.makePressureBasedPrediction(avgPressure, avgStrokeDuration),
hasEnhancedData: pressurePoints > 0
};
}
makePressureBasedPrediction(pressure, duration) {
// Use pressure and timing patterns to help predict
const strokeCount = this.strokes.length;
if (strokeCount === 3) {
if (pressure > 0.6 && duration < 200) {
return 'Boy'; // Quick, heavy strokes
} else if (pressure < 0.4 && duration > 300) {
return 'Cat'; // Light, deliberate strokes
}
} else if (strokeCount >= 4) {
if (pressure > 0.5 && duration > 250) {
return 'Love'; // Moderate pressure, careful strokes
} else if (pressure < 0.5 && duration < 200) {
return 'Like'; // Light, quick strokes
}
}
return null; // No clear pattern
}
calculateNativeConfidence(analysis) {
let confidence = 0.5;
// Bonus for having enhanced data
if (analysis.hasEnhancedData) {
confidence += 0.2;
}
// Bonus for reasonable pressure patterns
if (analysis.avgPressure > 0.2 && analysis.avgPressure < 0.8) {
confidence += 0.1;
}
// Bonus for reasonable timing
if (analysis.avgDuration > 100 && analysis.avgDuration < 1000) {
confidence += 0.1;
}
return Math.min(1.0, confidence);
}
clearCanvas() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.strokes = [];
console.log('🧹 Canvas cleared, native recognition reset');
}
isNativeSupported() {
return this.isSupported;
}
getRecognitionInfo() {
return {
supported: this.isSupported,
type: this.recognizer ? this.recognizer.type || 'native' : 'none',
features: this.recognizer ? this.recognizer.features || {} : {}
};
}
}
// Export for use in other files
window.NativeHandwritingRecognition = NativeHandwritingRecognition;
+905
View File
@@ -0,0 +1,905 @@
/**
* Tablet Menu Management System
* Handles collapsible navigation, touch gestures, and optimized menu interactions for tablets
*/
class TabletMenuManager {
constructor() {
this.isMenuExpanded = false;
this.isMinimized = false;
this.isWritingActive = false;
this.touchStartX = 0;
this.touchStartY = 0;
this.swipeThreshold = 100;
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
this.init();
}
init() {
this.createMenuToggle();
this.createMenuPopup();
this.setupEventListeners();
this.setupPopupDragging();
this.setupHelpBubble();
this.optimizeAdminTabs();
this.createQuickActions();
this.setupWritingAreaDetection();
console.log('📱 Universal Popup Menu Manager initialized');
}
isTabletDevice() {
// Check for tablet characteristics
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const screenSize = window.innerWidth >= 600 && window.innerWidth <= 1200;
const isLandscape = window.innerWidth > window.innerHeight;
const aspectRatio = window.innerWidth / window.innerHeight;
return hasTouch && (screenSize || (isLandscape && aspectRatio < 2));
}
createMenuToggle() {
// Remove existing toggle if present
const existingToggle = document.querySelector('.menu-toggle');
if (existingToggle) existingToggle.remove();
const toggle = document.createElement('button');
toggle.className = 'menu-toggle';
toggle.innerHTML = '☰';
toggle.setAttribute('aria-label', 'Toggle Navigation Menu');
toggle.addEventListener('click', () => this.toggleMenu());
document.body.appendChild(toggle);
this.menuToggle = toggle;
}
createMenuPopup() {
// Remove existing popup if present
const existingPopup = document.querySelector('.menu-popup');
if (existingPopup) existingPopup.remove();
// Create popup container
const popup = document.createElement('div');
popup.className = 'menu-popup hidden';
popup.id = 'menuPopup';
// Create popup header
const header = document.createElement('div');
header.className = 'popup-header';
header.innerHTML = `
<div class="popup-title">
<span class="popup-icon">📋</span>
<span>Admin Functions</span>
</div>
<div class="popup-controls">
<button class="minimize-btn" title="Minimize">−</button>
<button class="close-btn" title="Close">×</button>
</div>
`;
// Create popup content (will be populated by existing nav content)
const content = document.createElement('div');
content.className = 'popup-content';
content.id = 'popupContent';
popup.appendChild(header);
popup.appendChild(content);
document.body.appendChild(popup);
this.menuPopup = popup;
this.setupPopupControls();
}
setupPopupControls() {
const minimizeBtn = this.menuPopup.querySelector('.minimize-btn');
const closeBtn = this.menuPopup.querySelector('.close-btn');
minimizeBtn.addEventListener('click', () => this.minimizePopup());
closeBtn.addEventListener('click', () => this.closePopup());
}
setupPopupDragging() {
const header = this.menuPopup.querySelector('.popup-header');
header.addEventListener('mousedown', (e) => this.startDrag(e));
header.addEventListener('touchstart', (e) => this.startDrag(e), { passive: false });
document.addEventListener('mousemove', (e) => this.onDrag(e));
document.addEventListener('touchmove', (e) => this.onDrag(e), { passive: false });
document.addEventListener('mouseup', () => this.endDrag());
document.addEventListener('touchend', () => this.endDrag());
}
startDrag(e) {
this.isDragging = true;
const rect = this.menuPopup.getBoundingClientRect();
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
this.dragOffset.x = clientX - rect.left;
this.dragOffset.y = clientY - rect.top;
this.menuPopup.style.cursor = 'grabbing';
e.preventDefault();
}
onDrag(e) {
if (!this.isDragging) return;
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
const newX = clientX - this.dragOffset.x;
const newY = clientY - this.dragOffset.y;
// Keep popup within viewport bounds
const maxX = window.innerWidth - this.menuPopup.offsetWidth;
const maxY = window.innerHeight - this.menuPopup.offsetHeight;
const constrainedX = Math.max(0, Math.min(newX, maxX));
const constrainedY = Math.max(0, Math.min(newY, maxY));
this.menuPopup.style.left = constrainedX + 'px';
this.menuPopup.style.top = constrainedY + 'px';
this.menuPopup.style.transform = 'none';
}
endDrag() {
this.isDragging = false;
this.menuPopup.style.cursor = 'default';
}
minimizePopup() {
this.isMinimized = !this.isMinimized;
const content = this.menuPopup.querySelector('.popup-content');
const minimizeBtn = this.menuPopup.querySelector('.minimize-btn');
if (this.isMinimized) {
content.style.display = 'none';
this.menuPopup.classList.add('minimized');
minimizeBtn.innerHTML = '+';
minimizeBtn.title = 'Restore';
} else {
content.style.display = 'block';
this.menuPopup.classList.remove('minimized');
minimizeBtn.innerHTML = '−';
minimizeBtn.title = 'Minimize';
}
}
closePopup() {
this.menuPopup.classList.add('hidden');
this.menuToggle.classList.remove('active');
this.menuToggle.innerHTML = '☰';
this.isMenuExpanded = false;
this.isMinimized = false;
this.showToast('Menu closed');
}
setupHelpBubble() {
const helpBubble = document.getElementById('adminHelpBubble');
if (!helpBubble) return;
helpBubble.addEventListener('click', () => this.showHelpPopup());
// Only show help bubble for admin users
this.updateHelpBubbleVisibility();
}
updateHelpBubbleVisibility() {
const helpBubble = document.getElementById('adminHelpBubble');
const adminPanel = document.getElementById('adminPanel');
if (helpBubble && adminPanel) {
if (!adminPanel.classList.contains('hidden')) {
helpBubble.style.display = 'flex';
} else {
helpBubble.style.display = 'none';
}
}
}
showHelpPopup() {
// Create overlay if it doesn't exist
let overlay = document.getElementById('helpOverlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'helpOverlay';
overlay.className = 'help-overlay';
const popup = document.createElement('div');
popup.className = 'help-popup';
const header = document.createElement('div');
header.className = 'help-popup-header';
header.innerHTML = `
<div class="help-popup-title">
<span>📱</span>
<span>Admin Functions Guide</span>
</div>
<button class="help-close-btn" onclick="window.tabletMenuManager.closeHelpPopup()">×</button>
`;
const content = document.createElement('div');
content.className = 'help-popup-content';
// Copy content from hidden instructions
const helpContent = document.getElementById('adminHelpContent');
if (helpContent) {
content.innerHTML = helpContent.innerHTML;
}
popup.appendChild(header);
popup.appendChild(content);
overlay.appendChild(popup);
document.body.appendChild(overlay);
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
this.closeHelpPopup();
}
});
// Close on escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
this.closeHelpPopup();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
// Show overlay
setTimeout(() => {
overlay.classList.add('visible');
}, 10);
this.showToast('Help opened');
}
closeHelpPopup() {
const overlay = document.getElementById('helpOverlay');
if (overlay) {
overlay.classList.remove('visible');
setTimeout(() => {
overlay.remove();
}, 300);
}
this.showToast('Help closed');
}
setupEventListeners() {
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isMenuExpanded) {
this.closeMenu();
}
if (e.ctrlKey && e.key === 'm') {
e.preventDefault();
this.toggleMenu();
}
});
// Click outside to close menu
document.addEventListener('click', (e) => {
const nav = document.getElementById('nav');
if (this.isMenuExpanded &&
!nav.contains(e.target) &&
!this.menuToggle.contains(e.target)) {
this.closeMenu();
}
});
// Orientation change handling
window.addEventListener('orientationchange', () => {
setTimeout(() => this.handleOrientationChange(), 100);
});
// Resize handling
window.addEventListener('resize', () => {
this.handleResize();
});
}
setupSwipeGestures() {
let startX, startY, startTime;
document.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startTime = Date.now();
}
}, { passive: true });
document.addEventListener('touchend', (e) => {
if (e.changedTouches.length === 1) {
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const endTime = Date.now();
const deltaX = endX - startX;
const deltaY = endY - startY;
const deltaTime = endTime - startTime;
// Swipe detection
if (Math.abs(deltaX) > Math.abs(deltaY) &&
Math.abs(deltaX) > this.swipeThreshold &&
deltaTime < 500) {
if (deltaX > 0 && startX < 50) {
// Swipe right from left edge - open menu
this.openMenu();
} else if (deltaX < 0 && this.isMenuExpanded) {
// Swipe left when menu open - close menu
this.closeMenu();
}
}
}
}, { passive: true });
}
optimizeAdminTabs() {
const adminTabs = document.getElementById('adminTabs');
if (!adminTabs) return;
// Add scroll indicators
this.addScrollIndicators(adminTabs);
// Smooth scrolling for tab navigation
adminTabs.addEventListener('wheel', (e) => {
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return;
e.preventDefault();
adminTabs.scrollLeft += e.deltaY;
}, { passive: false });
// Center active tab
const activeTab = adminTabs.querySelector('button.active');
if (activeTab) {
this.scrollToTab(activeTab);
}
}
addScrollIndicators(container) {
const leftIndicator = document.createElement('div');
leftIndicator.className = 'scroll-indicator left';
leftIndicator.innerHTML = '‹';
const rightIndicator = document.createElement('div');
rightIndicator.className = 'scroll-indicator right';
rightIndicator.innerHTML = '›';
container.parentNode.insertBefore(leftIndicator, container);
container.parentNode.appendChild(rightIndicator);
// Update indicators on scroll
const updateIndicators = () => {
leftIndicator.style.opacity = container.scrollLeft > 0 ? '1' : '0.3';
rightIndicator.style.opacity =
container.scrollLeft < container.scrollWidth - container.clientWidth ? '1' : '0.3';
};
container.addEventListener('scroll', updateIndicators);
updateIndicators();
// Click handlers
leftIndicator.addEventListener('click', () => {
container.scrollBy({ left: -120, behavior: 'smooth' });
});
rightIndicator.addEventListener('click', () => {
container.scrollBy({ left: 120, behavior: 'smooth' });
});
}
scrollToTab(tab) {
const container = tab.parentNode;
const tabRect = tab.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
if (tabRect.left < containerRect.left || tabRect.right > containerRect.right) {
tab.scrollIntoView({ behavior: 'smooth', inline: 'center' });
}
}
createQuickActions() {
// Remove existing quick actions
const existing = document.querySelector('.quick-actions');
if (existing) existing.remove();
const quickActions = document.createElement('div');
quickActions.className = 'quick-actions';
// Back to top button
const backToTop = document.createElement('button');
backToTop.className = 'quick-action-btn secondary';
backToTop.innerHTML = '↑';
backToTop.title = 'Back to Top';
backToTop.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
quickActions.appendChild(backToTop);
// Show/hide based on scroll position
window.addEventListener('scroll', () => {
const scrolled = window.pageYOffset > 200;
backToTop.style.opacity = scrolled ? '1' : '0';
backToTop.style.pointerEvents = scrolled ? 'auto' : 'none';
});
document.body.appendChild(quickActions);
}
setupWritingAreaDetection() {
// Detect when user is actively writing
const writingAreas = [
'#unifiedWriteContainer canvas',
'#handwritingCanvas',
'#userInput'
];
writingAreas.forEach(selector => {
const element = document.querySelector(selector);
if (element) {
element.addEventListener('focus', () => this.setWritingMode(true));
element.addEventListener('blur', () => this.setWritingMode(false));
if (element.tagName === 'CANVAS') {
element.addEventListener('pointerdown', () => this.setWritingMode(true));
element.addEventListener('touchstart', () => this.setWritingMode(true));
}
}
});
// Auto-close menu when writing starts
document.addEventListener('touchstart', (e) => {
const isWritingArea = writingAreas.some(selector => {
const element = document.querySelector(selector);
return element && element.contains(e.target);
});
if (isWritingArea && this.isMenuExpanded) {
setTimeout(() => this.closeMenu(), 100);
}
});
}
setWritingMode(active) {
this.isWritingActive = active;
document.body.classList.toggle('writing-focused', active);
if (active && this.isMenuExpanded) {
this.closeMenu();
}
}
toggleMenu() {
if (this.isMenuExpanded) {
this.closePopup();
} else {
this.openPopup();
}
}
openPopup() {
// Move navigation content to popup
this.populatePopupContent();
// Show popup
this.menuPopup.classList.remove('hidden');
this.menuToggle.classList.add('active');
this.menuToggle.innerHTML = '✕';
// Position popup in center initially
if (!this.menuPopup.style.left || !this.menuPopup.style.top) {
this.centerPopup();
}
this.isMenuExpanded = true;
}
populatePopupContent() {
const content = this.menuPopup.querySelector('.popup-content');
const nav = document.getElementById('nav');
if (nav && content) {
// Clone the navigation content
content.innerHTML = nav.innerHTML;
}
}
centerPopup() {
const rect = this.menuPopup.getBoundingClientRect();
const x = (window.innerWidth - rect.width) / 2;
const y = (window.innerHeight - rect.height) / 2;
this.menuPopup.style.left = Math.max(20, x) + 'px';
this.menuPopup.style.top = Math.max(20, y) + 'px';
this.menuPopup.style.transform = 'none';
this.showToast('Menu opened');
// Focus first interactive element
setTimeout(() => {
const firstButton = this.menuPopup.querySelector('button');
if (firstButton) firstButton.focus();
}, 300);
}
closeMenu() {
// Keep this method for backward compatibility
this.closePopup();
}
showToast(message, duration = 2000) {
// Remove existing toasts
document.querySelectorAll('.toast').forEach(toast => toast.remove());
// Create toast container if needed
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container';
document.body.appendChild(container);
}
// Create toast
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
container.appendChild(toast);
// Animate in
setTimeout(() => toast.classList.add('show'), 100);
// Auto remove
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, duration);
}
handleOrientationChange() {
const nav = document.getElementById('nav');
if (!nav) return;
const isLandscape = window.innerWidth > window.innerHeight;
if (isLandscape && window.innerWidth >= 900) {
nav.classList.add('landscape');
document.body.classList.toggle('landscape-nav-expanded', this.isMenuExpanded);
} else {
nav.classList.remove('landscape');
document.body.classList.remove('landscape-nav-expanded');
}
// Reoptimize admin tabs
setTimeout(() => this.optimizeAdminTabs(), 100);
}
handleResize() {
if (!this.isTabletDevice() && this.isMenuExpanded) {
this.closeMenu();
}
// Re-evaluate tablet status
if (this.isTabletDevice() && !this.menuToggle) {
this.createMenuToggle();
} else if (!this.isTabletDevice() && this.menuToggle) {
this.menuToggle.remove();
this.menuToggle = null;
}
}
// Enhanced navigation functions
updateUserInfo() {
const userInfo = document.getElementById('userInfo');
const userBadge = document.getElementById('userBadge');
if (window.currentUser) {
const { username, role } = window.currentUser;
// Don't override if userInfo already has content from main UI
if (userInfo && !userInfo.textContent.includes('(')) {
userInfo.textContent = `Welcome, ${username}`;
}
if (userBadge) {
userBadge.textContent = role === 'admin' ? '👨‍💼 Admin' : '🎓 Student';
userBadge.style.background = role === 'admin' ? 'var(--error)' : 'var(--primary)';
}
this.showRoleSpecificMenu(role);
}
}
showRoleSpecificMenu(role) {
const studentNav = document.getElementById('studentNavItems');
const adminNav = document.getElementById('adminNavItems');
if (role === 'admin') {
if (studentNav) studentNav.classList.add('hidden');
if (adminNav) adminNav.classList.remove('hidden');
} else {
if (adminNav) adminNav.classList.add('hidden');
if (studentNav) studentNav.classList.remove('hidden');
}
}
// Public API methods
isMenuOpen() {
return this.isMenuExpanded;
}
forceCloseMenu() {
this.closeMenu();
}
updateNavigation() {
this.updateUserInfo();
}
setTabActive(tabId) {
const adminTabs = document.getElementById('adminTabs');
if (!adminTabs) return;
const tab = adminTabs.querySelector(`[data-tab="${tabId}"]`);
if (tab) {
// Remove active class from all tabs
adminTabs.querySelectorAll('button').forEach(btn => btn.classList.remove('active'));
// Add active class to selected tab
tab.classList.add('active');
// Scroll to active tab
this.scrollToTab(tab);
}
}
}
// Initialize tablet menu manager when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Wait for other systems to initialize
setTimeout(() => {
try {
window.tabletMenuManager = new TabletMenuManager();
} catch (error) {
console.warn('Tablet Menu Manager failed to initialize:', error);
}
}, 1000); // Increased delay to ensure other systems are ready
});
// Handle admin tab clicks to update active state
document.addEventListener('click', function(e) {
if (e.target.matches('#adminTabs button[data-tab]')) {
if (window.tabletMenuManager) {
window.tabletMenuManager.setTabActive(e.target.dataset.tab);
}
}
});
// Helper functions for enhanced navigation
function closeMenuIfTablet() {
if (window.tabletMenuManager && window.tabletMenuManager.isMenuOpen()) {
window.tabletMenuManager.forceCloseMenu();
}
}
function showAdminTab(tabId) {
// Show admin panel first
if (window.showAdmin) {
window.showAdmin();
}
// Then activate the specific tab
setTimeout(() => {
// Hide the dashboard overview
const adminOverview = document.querySelector('.admin-overview');
const menuInstructions = document.querySelector('.menu-instructions');
if (adminOverview) adminOverview.style.display = 'none';
if (menuInstructions) menuInstructions.style.display = 'none';
// Don't show the old admin tabs interface - keep it hidden
const adminTabsContainer = document.querySelector('#adminTabs').parentElement;
if (adminTabsContainer) {
adminTabsContainer.style.display = 'none';
}
// Show the specific admin tab content directly
document.querySelectorAll('.adminTab').forEach(tab => {
tab.classList.add('hidden');
});
const targetTab = document.getElementById(tabId);
if (targetTab) {
targetTab.classList.remove('hidden');
targetTab.style.display = 'block';
}
// Add a back to dashboard button
addBackToDashboardButton();
}, 100);
}
function showChallenges() {
// Navigate to student dashboard first
if (window.showStudent) {
window.showStudent();
}
// Scroll to challenges section
setTimeout(() => {
const challengesContainer = document.getElementById('challengesContainer');
if (challengesContainer) {
challengesContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300);
}
function showLeaderboards() {
// Navigate to student dashboard first
if (window.showStudent) {
window.showStudent();
}
// Scroll to leaderboards section
setTimeout(() => {
const leaderboardsContainer = document.getElementById('leaderboardsContainer');
if (leaderboardsContainer) {
leaderboardsContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300);
}
function showBadges() {
// Navigate to student dashboard first
if (window.showStudent) {
window.showStudent();
}
// Scroll to user stats which includes badges
setTimeout(() => {
const userStatsContainer = document.getElementById('userStatsContainer');
if (userStatsContainer) {
userStatsContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300);
}
function showThemeSelector() {
// Navigate to student dashboard first
if (window.showStudent) {
window.showStudent();
}
// Focus on theme selector
setTimeout(() => {
const themeSelect = document.getElementById('studentThemeSelect');
if (themeSelect) {
themeSelect.scrollIntoView({ behavior: 'smooth', block: 'center' });
themeSelect.focus();
}
}, 300);
}
function showInputSettings() {
// Show unified write mode if available
if (window.unifiedWriteMode) {
// Navigate to student dashboard first
if (window.showStudent) {
window.showStudent();
}
setTimeout(() => {
// Start a game to show the input system
if (window.startGame) {
window.startGame();
}
}, 300);
}
}
// Update navigation when user changes
function updateNavigationForUser() {
try {
if (window.tabletMenuManager) {
window.tabletMenuManager.updateNavigation();
} else {
// Fallback: update navigation elements directly
updateNavigationFallback();
}
} catch (error) {
console.warn('Navigation update failed:', error);
updateNavigationFallback();
}
}
function updateNavigationFallback() {
if (window.currentUser) {
const { role } = window.currentUser;
const studentNav = document.getElementById('studentNavItems');
const adminNav = document.getElementById('adminNavItems');
if (role === 'admin') {
if (studentNav) studentNav.classList.add('hidden');
if (adminNav) adminNav.classList.remove('hidden');
} else {
if (adminNav) adminNav.classList.add('hidden');
if (studentNav) studentNav.classList.remove('hidden');
}
}
}
function addBackToDashboardButton() {
// Remove existing back button
const existingButton = document.querySelector('.back-to-dashboard');
if (existingButton) existingButton.remove();
// Add back button to admin panel
const adminPanel = document.getElementById('adminPanel');
if (adminPanel) {
const backButton = document.createElement('button');
backButton.className = 'back-to-dashboard btn-secondary';
backButton.innerHTML = '← Back to Dashboard';
backButton.style.cssText = 'margin-bottom: 1rem; display: inline-block;';
backButton.onclick = showAdminDashboard;
// Insert at the beginning of admin panel
const firstChild = adminPanel.firstElementChild;
if (firstChild) {
adminPanel.insertBefore(backButton, firstChild);
}
}
}
function showAdminDashboard() {
// Show the dashboard overview
const adminOverview = document.querySelector('.admin-overview');
const menuInstructions = document.querySelector('.menu-instructions');
if (adminOverview) adminOverview.style.display = 'block';
if (menuInstructions) menuInstructions.style.display = 'block';
// Hide the admin tabs container
const adminTabs = document.getElementById('adminTabs');
if (adminTabs && adminTabs.parentElement) {
adminTabs.parentElement.style.display = 'none';
}
// Hide all admin tab content
document.querySelectorAll('.adminTab').forEach(tab => {
tab.classList.add('hidden');
});
// Remove back button
const backButton = document.querySelector('.back-to-dashboard');
if (backButton) backButton.remove();
// Load dashboard stats
loadAdminDashboardStats();
}
function loadAdminDashboardStats() {
// Update stats with real data
try {
// Total users
if (window.users && Array.isArray(window.users)) {
document.getElementById('totalUsers').textContent = window.users.length;
}
// You can add more stats here as needed
document.getElementById('totalWordLists').textContent = '5+';
document.getElementById('totalSessions').textContent = '0';
document.getElementById('totalBadges').textContent = '8';
} catch (error) {
console.warn('Could not load dashboard stats:', error);
}
}
// Export for use in other scripts
window.TabletMenuManager = TabletMenuManager;
+388
View File
@@ -0,0 +1,388 @@
// Tablet optimization utilities for Spelling Practice App
class TabletOptimizations {
constructor() {
this.isTablet = this.detectTablet();
this.touchStartTime = 0;
this.touchStartPos = { x: 0, y: 0 };
if (this.isTablet) {
this.init();
}
}
detectTablet() {
// Basic tablet detection
const userAgent = navigator.userAgent.toLowerCase();
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isTabletSize = window.innerWidth >= 768 && window.innerWidth <= 1024;
const isTabletUA = /tablet|ipad|playbook|silk/i.test(userAgent);
return isTouch && (isTabletSize || isTabletUA);
}
init() {
console.log('📱 Initializing tablet optimizations');
this.addTabletClasses();
this.optimizeButtons();
this.improveScrolling();
this.addTouchFeedback();
this.optimizeKeyboard();
this.handleOrientationChange();
// Initialize after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.setupTabletUI();
});
} else {
this.setupTabletUI();
}
}
addTabletClasses() {
document.documentElement.classList.add('tablet-device');
document.body.classList.add('tablet-optimized');
}
optimizeButtons() {
// Add enhanced button interactions
document.addEventListener('touchstart', (e) => {
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) {
const button = e.target.tagName === 'BUTTON' ? e.target : e.target.closest('button');
button.classList.add('touch-active');
this.touchStartTime = Date.now();
this.touchStartPos = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
}, { passive: true });
document.addEventListener('touchend', (e) => {
// Remove touch feedback after a short delay
setTimeout(() => {
document.querySelectorAll('.touch-active').forEach(btn => {
btn.classList.remove('touch-active');
});
}, 150);
}, { passive: true });
// Handle long press for additional options
document.addEventListener('touchstart', (e) => {
if (e.target.classList.contains('activity-option')) {
this.longPressTimer = setTimeout(() => {
this.showActivityOptions(e.target);
}, 800);
}
}, { passive: true });
document.addEventListener('touchend', () => {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
}
}, { passive: true });
}
improveScrolling() {
// Smooth scrolling for tablets
document.documentElement.style.scrollBehavior = 'smooth';
// Prevent bounce scrolling on iOS
document.body.style.overscrollBehavior = 'none';
// Add momentum scrolling
const scrollableElements = document.querySelectorAll('.scrollable, .card, .adminTab');
scrollableElements.forEach(el => {
el.style.webkitOverflowScrolling = 'touch';
});
}
addTouchFeedback() {
// Add haptic feedback (if available)
window.vibrate = window.vibrate || window.webkitVibrate || window.mozVibrate;
// Visual touch feedback
const style = document.createElement('style');
style.textContent = `
.tablet-device .touch-active {
transform: scale(0.98) !important;
transition: transform 0.1s ease-out !important;
background-color: rgba(59, 130, 246, 0.1) !important;
}
.tablet-device button:active,
.tablet-device .activity-option:active {
transform: scale(0.95);
transition: transform 0.1s ease-out;
}
.tablet-device input:focus,
.tablet-device textarea:focus,
.tablet-device canvas:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2) !important;
border-color: var(--primary) !important;
}
`;
document.head.appendChild(style);
}
optimizeKeyboard() {
// Prevent zoom on input focus for iOS
const inputs = document.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
if (input.style.fontSize === '' || parseFloat(input.style.fontSize) < 16) {
input.style.fontSize = '16px';
}
});
// Auto-scroll to input when keyboard appears
inputs.forEach(input => {
input.addEventListener('focus', () => {
setTimeout(() => {
input.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}, 300); // Wait for keyboard animation
});
});
}
handleOrientationChange() {
let orientationTimeout;
window.addEventListener('orientationchange', () => {
// Clear existing timeout
if (orientationTimeout) {
clearTimeout(orientationTimeout);
}
// Wait for orientation change to complete
orientationTimeout = setTimeout(() => {
this.adjustForOrientation();
// Trigger canvas resize if handwriting is active
if (window.handwritingRecognition && window.handwritingRecognition.resizeCanvas) {
window.handwritingRecognition.resizeCanvas();
}
// Re-focus current input if any
const activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
activeElement.blur();
setTimeout(() => activeElement.focus(), 100);
}
}, 500);
});
}
adjustForOrientation() {
const isLandscape = window.innerWidth > window.innerHeight;
if (isLandscape) {
document.body.classList.add('tablet-landscape');
document.body.classList.remove('tablet-portrait');
} else {
document.body.classList.add('tablet-portrait');
document.body.classList.remove('tablet-landscape');
}
// Adjust game section for landscape
const gameSection = document.getElementById('gameSection');
const wordBox = document.getElementById('wordBox');
if (gameSection && wordBox) {
if (isLandscape) {
gameSection.style.minHeight = '70vh';
gameSection.style.display = 'flex';
gameSection.style.flexDirection = 'column';
gameSection.style.justifyContent = 'center';
} else {
gameSection.style.minHeight = 'auto';
gameSection.style.display = 'block';
}
}
}
setupTabletUI() {
// Enhance activity options with better touch targets
const activityOptions = document.querySelectorAll('.activity-option');
activityOptions.forEach((option, index) => {
// Add touch ripple effect
option.addEventListener('touchstart', (e) => {
this.createRipple(e, option);
});
// Add accessibility improvements
option.setAttribute('tabindex', '0');
option.setAttribute('role', 'button');
// Keyboard navigation
option.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
option.click();
}
});
});
// Improve admin tabs for tablet
const adminTabs = document.getElementById('adminTabs');
if (adminTabs) {
adminTabs.classList.add('tablet-optimized-tabs');
}
// Add swipe gestures for navigation (optional)
this.addSwipeGestures();
}
createRipple(event, element) {
const ripple = document.createElement('span');
const rect = element.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.touches[0].clientX - rect.left - size / 2;
const y = event.touches[0].clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: scale(0);
animation: ripple-animation 0.6s ease-out;
pointer-events: none;
z-index: 1000;
`;
// Add ripple animation if not exists
if (!document.getElementById('ripple-style')) {
const style = document.createElement('style');
style.id = 'ripple-style';
style.textContent = `
@keyframes ripple-animation {
to {
transform: scale(2);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
element.style.position = 'relative';
element.style.overflow = 'hidden';
element.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 600);
}
addSwipeGestures() {
let startX, startY, startTime;
document.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startTime = Date.now();
}, { passive: true });
document.addEventListener('touchend', (e) => {
if (!startX || !startY) return;
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const endTime = Date.now();
const deltaX = endX - startX;
const deltaY = endY - startY;
const deltaTime = endTime - startTime;
// Check if it's a swipe (fast, directional movement)
if (deltaTime < 300 && Math.abs(deltaX) > 100 && Math.abs(deltaY) < 80) {
if (deltaX > 0) {
// Swipe right
this.handleSwipeRight();
} else {
// Swipe left
this.handleSwipeLeft();
}
}
startX = startY = null;
}, { passive: true });
}
handleSwipeLeft() {
// Navigate forward or show next content
console.log('👈 Swipe left detected');
}
handleSwipeRight() {
// Navigate back or show previous content
console.log('👉 Swipe right detected');
// Go back to dashboard if in game/typing mode
const gameSection = document.getElementById('gameSection');
const typingSection = document.getElementById('typingSection');
const bibleSection = document.getElementById('bibleSection');
if (!gameSection.classList.contains('hidden') && typeof backToStudentDashboard === 'function') {
backToStudentDashboard();
} else if (!typingSection.classList.contains('hidden') && typeof returnToStudentPanel === 'function') {
returnToStudentPanel();
} else if (!bibleSection.classList.contains('hidden') && typeof returnToStudentPanel === 'function') {
returnToStudentPanel();
}
}
showActivityOptions(element) {
// Show additional options on long press
if (window.vibrate) {
window.vibrate(50); // Haptic feedback
}
// Add visual feedback
element.style.transform = 'scale(1.05)';
element.style.boxShadow = '0 8px 25px rgba(0,0,0,0.2)';
setTimeout(() => {
element.style.transform = '';
element.style.boxShadow = '';
}, 200);
console.log('📋 Long press detected on activity option');
}
// Public methods
vibrate(pattern = 50) {
if (window.vibrate && this.isTablet) {
window.vibrate(pattern);
}
}
isTabletDevice() {
return this.isTablet;
}
}
// Initialize tablet optimizations
let tabletOptimizations = null;
document.addEventListener('DOMContentLoaded', () => {
tabletOptimizations = new TabletOptimizations();
// Make available globally
window.tabletOptimizations = tabletOptimizations;
console.log(`📱 Tablet optimization ${tabletOptimizations.isTabletDevice() ? 'enabled' : 'not needed'}`);
});
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = TabletOptimizations;
}
+64 -15
View File
@@ -1,37 +1,60 @@
// 🎭 Role-Based UI Setup
function setupSession(user) {
document.getElementById('loginPanel').classList.add('hidden');
document.getElementById('adminPanel').classList.add('hidden');
document.getElementById('studentPanel').classList.add('hidden');
document.getElementById('nav').classList.remove('hidden');
// Safe element hiding/showing with null checks
const loginPanel = document.getElementById('loginPanel');
const adminPanel = document.getElementById('adminPanel');
const studentPanel = document.getElementById('studentPanel');
const nav = document.getElementById('nav');
if (loginPanel) loginPanel.classList.add('hidden');
if (adminPanel) adminPanel.classList.add('hidden');
if (studentPanel) studentPanel.classList.add('hidden');
if (nav) nav.classList.remove('hidden');
document.getElementById('userInfo').textContent = `${user.username} (${user.role})`;
// Store current user globally for tablet menu manager
window.currentUser = user;
const userInfo = document.getElementById('userInfo');
if (userInfo) {
userInfo.textContent = `${user.username} (${user.role})`;
}
// 🎨 Banner Setup
const roleBanner = document.getElementById('roleBanner');
roleBanner.classList.remove('hidden', 'admin', 'student');
roleBanner.classList.add(user.role);
roleBanner.textContent = user.role === "admin"
? "👨‍🏫 Admin Access Enabled"
: "🎓 Student Mode Active";
if (roleBanner) {
roleBanner.classList.remove('hidden', 'admin', 'student');
roleBanner.classList.add(user.role);
roleBanner.textContent = user.role === "admin"
? "👨‍🏫 Admin Access Enabled"
: "🎓 Student Mode Active";
}
// 🎚 Role-Based Navigation
const studentBtn = document.getElementById('studentBtn');
const adminBtn = document.getElementById('adminBtn');
if (user.role === "admin") {
studentBtn.classList.remove('hidden');
adminBtn.classList.remove('hidden');
if (studentBtn) studentBtn.classList.remove('hidden');
if (adminBtn) adminBtn.classList.remove('hidden');
showAdmin();
} else if (user.role === "student") {
studentBtn.classList.add('hidden');
adminBtn.classList.add('hidden');
if (studentBtn) studentBtn.classList.add('hidden');
if (adminBtn) adminBtn.classList.add('hidden');
showStudent();
} else {
alert("Unknown role. Logging out for safety.");
logoutUser();
}
// Update enhanced navigation menu
try {
if (typeof updateNavigationForUser === 'function') {
updateNavigationForUser();
}
} catch (error) {
console.warn('Enhanced navigation update failed:', error);
}
showToast(`Welcome, ${user.role}!`);
}
@@ -77,6 +100,20 @@ function showStudent() {
if (studentThemeSelect) {
studentThemeSelect.value = savedTheme;
}
// Initialize dashboard containers with default content
if (typeof initializeStudentContainers === 'function') {
setTimeout(() => {
initializeStudentContainers();
}, 100);
}
// Update help bubble visibility
if (window.tabletMenuManager && typeof window.tabletMenuManager.updateHelpBubbleVisibility === 'function') {
setTimeout(() => {
window.tabletMenuManager.updateHelpBubbleVisibility();
}, 100);
}
}
}
@@ -91,7 +128,19 @@ function showAdmin() {
if (adminPanel) {
adminPanel.classList.remove("hidden");
// Load user data for dropdowns
// Show dashboard view by default
if (typeof showAdminDashboard === 'function') {
showAdminDashboard();
}
// Update help bubble visibility for admin
if (window.tabletMenuManager && typeof window.tabletMenuManager.updateHelpBubbleVisibility === 'function') {
setTimeout(() => {
window.tabletMenuManager.updateHelpBubbleVisibility();
}, 100);
}
// Load user data for dropdowns (for when tabs are accessed)
if (typeof window.loadUsers === 'function') {
window.loadUsers().then(() => {
console.log("✅ Users loaded from server");
+520
View File
@@ -0,0 +1,520 @@
/**
* Unified Write Mode - Large Canvas with Handwriting + Keyboard Support
* Combines handwriting recognition and keyboard input in one interface
*/
class UnifiedWriteMode {
constructor() {
this.currentMode = 'keyboard'; // 'keyboard' or 'handwriting'
this.canvas = null;
this.ctx = null;
this.textArea = null;
this.container = null;
this.handwritingRecognizer = null;
// Handwriting state
this.isDrawing = false;
this.strokes = [];
this.currentStroke = [];
// Recognition state
this.recognizedText = '';
this.isRecognizing = false;
this.init();
}
init() {
console.log('🔄 Initializing Unified Write Mode...');
this.createInterface();
this.setupEventListeners();
this.initializeHandwritingRecognition();
}
createInterface() {
// Find or create the main container
this.container = document.getElementById('unifiedWriteContainer');
if (!this.container) {
this.container = document.createElement('div');
this.container.id = 'unifiedWriteContainer';
this.container.className = 'unified-write-mode';
// Insert into the game container or create new section
const gameContainer = document.getElementById('game-container');
if (gameContainer) {
gameContainer.appendChild(this.container);
} else {
document.body.appendChild(this.container);
}
}
this.container.innerHTML = `
<div class="input-mode-selector">
<button class="mode-button active" data-mode="keyboard">
<span class="icon">⌨️</span>
<span>Keyboard</span>
</button>
<button class="mode-button" data-mode="handwriting">
<span class="icon">✏️</span>
<span>Handwriting</span>
</button>
</div>
<div class="large-writing-canvas keyboard-mode">
<div class="input-method-indicator">Keyboard Mode</div>
<textarea
class="text-input-area"
placeholder="Type your answer here or switch to handwriting mode..."
spellcheck="false"
></textarea>
<canvas class="canvas-overlay"></canvas>
</div>
<div class="canvas-controls">
<button class="btn btn-recognize" id="recognizeBtn" style="display: none;">
<span>🔍</span>
<span>Recognize</span>
</button>
<button class="btn btn-clear" id="clearBtn">
<span>🧹</span>
<span>Clear</span>
</button>
<button class="btn btn-submit" id="submitBtn">
<span>✅</span>
<span>Submit Answer</span>
</button>
</div>
<div class="recognition-feedback" id="recognitionFeedback"></div>
`;
// Get references to key elements
this.textArea = this.container.querySelector('.text-input-area');
this.canvas = this.container.querySelector('.canvas-overlay');
this.canvasContainer = this.container.querySelector('.large-writing-canvas');
this.indicator = this.container.querySelector('.input-method-indicator');
// Setup canvas
this.setupCanvas();
console.log('✅ Interface created successfully');
}
setupCanvas() {
this.ctx = this.canvas.getContext('2d');
this.resizeCanvas();
// Configure canvas for drawing
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.lineWidth = 3;
this.ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim();
// Handle window resize
window.addEventListener('resize', () => this.resizeCanvas());
}
resizeCanvas() {
const rect = this.canvasContainer.getBoundingClientRect();
this.canvas.width = rect.width;
this.canvas.height = rect.height;
// Reconfigure context after resize
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.lineWidth = 3;
this.ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim();
}
setupEventListeners() {
// Mode switching
const modeButtons = this.container.querySelectorAll('.mode-button');
modeButtons.forEach(button => {
button.addEventListener('click', (e) => {
const mode = e.currentTarget.dataset.mode;
this.switchMode(mode);
});
});
// Canvas drawing events
this.setupDrawingEvents();
// Control buttons
const recognizeBtn = this.container.querySelector('#recognizeBtn');
const clearBtn = this.container.querySelector('#clearBtn');
const submitBtn = this.container.querySelector('#submitBtn');
if (recognizeBtn) {
recognizeBtn.addEventListener('click', () => this.recognizeHandwriting());
}
if (clearBtn) {
clearBtn.addEventListener('click', () => this.clearInput());
}
if (submitBtn) {
submitBtn.addEventListener('click', () => this.submitAnswer());
}
// Keyboard shortcuts
this.textArea.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.submitAnswer();
}
});
}
setupDrawingEvents() {
// Touch/Mouse events for handwriting
const startDrawing = (e) => {
if (this.currentMode !== 'handwriting') return;
this.isDrawing = true;
this.currentStroke = [];
const point = this.getCanvasPoint(e);
this.currentStroke.push(point);
this.ctx.beginPath();
this.ctx.moveTo(point.x, point.y);
};
const continueDrawing = (e) => {
if (!this.isDrawing || this.currentMode !== 'handwriting') return;
const point = this.getCanvasPoint(e);
this.currentStroke.push(point);
this.ctx.lineTo(point.x, point.y);
this.ctx.stroke();
};
const stopDrawing = () => {
if (!this.isDrawing || this.currentMode !== 'handwriting') return;
this.isDrawing = false;
if (this.currentStroke.length > 0) {
this.strokes.push([...this.currentStroke]);
console.log(`✏️ Stroke completed. Total strokes: ${this.strokes.length}`);
// Auto-recognize after a delay
this.scheduleAutoRecognition();
}
this.currentStroke = [];
};
// Mouse events
this.canvas.addEventListener('mousedown', startDrawing);
this.canvas.addEventListener('mousemove', continueDrawing);
this.canvas.addEventListener('mouseup', stopDrawing);
this.canvas.addEventListener('mouseout', stopDrawing);
// Touch events
this.canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
startDrawing(e.touches[0]);
});
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
continueDrawing(e.touches[0]);
});
this.canvas.addEventListener('touchend', (e) => {
e.preventDefault();
stopDrawing();
});
// Pointer events (for stylus)
if ('PointerEvent' in window) {
this.canvas.addEventListener('pointerdown', startDrawing);
this.canvas.addEventListener('pointermove', continueDrawing);
this.canvas.addEventListener('pointerup', stopDrawing);
}
}
getCanvasPoint(e) {
const rect = this.canvas.getBoundingClientRect();
return {
x: (e.clientX - rect.left) * (this.canvas.width / rect.width),
y: (e.clientY - rect.top) * (this.canvas.height / rect.height),
timestamp: Date.now(),
pressure: e.pressure || 0.5,
force: e.force || 0.5
};
}
switchMode(mode) {
console.log(`🔄 Switching to ${mode} mode`);
this.currentMode = mode;
// Update button states
const buttons = this.container.querySelectorAll('.mode-button');
buttons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
// Update container class
this.canvasContainer.className = `large-writing-canvas ${mode}-mode`;
// Update indicator
if (mode === 'keyboard') {
this.indicator.textContent = '⌨️ Keyboard Mode';
this.container.querySelector('#recognizeBtn').style.display = 'none';
} else {
this.indicator.textContent = '✏️ Handwriting Mode';
this.container.querySelector('#recognizeBtn').style.display = 'flex';
}
// Clear any existing content when switching modes
this.clearInput();
// Focus appropriate input
if (mode === 'keyboard') {
setTimeout(() => this.textArea.focus(), 100);
}
}
async initializeHandwritingRecognition() {
try {
// Use existing handwriting recognition if available
if (window.handwritingRecognition) {
this.handwritingRecognizer = window.handwritingRecognition;
console.log('✅ Using existing handwriting recognition');
} else if (window.HandwritingRecognition) {
this.handwritingRecognizer = new window.HandwritingRecognition();
console.log('✅ Created new handwriting recognition instance');
} else {
console.log('⚠️ No handwriting recognition available');
}
} catch (error) {
console.error('❌ Failed to initialize handwriting recognition:', error);
}
}
scheduleAutoRecognition() {
// Clear any existing timeout
if (this.autoRecognitionTimeout) {
clearTimeout(this.autoRecognitionTimeout);
}
// Schedule recognition after 2 seconds of no drawing
this.autoRecognitionTimeout = setTimeout(() => {
if (this.strokes.length > 0 && !this.isDrawing) {
this.recognizeHandwriting();
}
}, 2000);
}
async recognizeHandwriting() {
if (this.strokes.length === 0) {
this.showFeedback('error', 'Please draw something first!');
return;
}
if (this.isRecognizing) {
console.log('⚠️ Recognition already in progress');
return;
}
this.isRecognizing = true;
this.showFeedback('processing', 'Recognizing handwriting...');
try {
let recognizedText = null;
// Try different recognition methods
if (this.handwritingRecognizer) {
// Method 1: Use existing handwriting recognizer
if (typeof this.handwritingRecognizer.recognizeStrokes === 'function') {
recognizedText = await this.handwritingRecognizer.recognizeStrokes(this.strokes);
} else if (typeof this.handwritingRecognizer.recognizeText === 'function') {
// Temporarily set strokes and recognize
const oldStrokes = this.handwritingRecognizer.strokes;
this.handwritingRecognizer.strokes = this.strokes;
recognizedText = await this.handwritingRecognizer.recognizeText();
this.handwritingRecognizer.strokes = oldStrokes;
}
}
// Method 2: Simple pattern recognition fallback
if (!recognizedText) {
recognizedText = this.simplePatternRecognition();
}
if (recognizedText) {
this.recognizedText = recognizedText;
this.textArea.value = recognizedText;
this.showFeedback('success', `Recognized: "${recognizedText}"`);
console.log('✅ Handwriting recognition successful:', recognizedText);
} else {
this.showFeedback('error', 'Could not recognize handwriting. Please try writing more clearly.');
console.log('❌ Handwriting recognition failed');
}
} catch (error) {
console.error('❌ Recognition error:', error);
this.showFeedback('error', 'Recognition failed. Please try again.');
} finally {
this.isRecognizing = false;
}
}
simplePatternRecognition() {
// Simple fallback based on stroke patterns
const strokeCount = this.strokes.length;
console.log(`🔍 Simple pattern recognition: ${strokeCount} strokes`);
if (strokeCount === 1) {
return 'O';
} else if (strokeCount === 2) {
return 'H';
} else if (strokeCount === 3) {
return 'Boy';
} else if (strokeCount >= 4 && strokeCount <= 6) {
return 'Love';
} else if (strokeCount > 6) {
return 'Hello';
}
return null;
}
clearInput() {
// Clear text
this.textArea.value = '';
this.recognizedText = '';
// Clear canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.strokes = [];
this.currentStroke = [];
// Clear feedback
this.showFeedback('', '');
// Clear auto-recognition timeout
if (this.autoRecognitionTimeout) {
clearTimeout(this.autoRecognitionTimeout);
}
console.log('🧹 Input cleared');
}
getCurrentText() {
return this.textArea.value.trim();
}
submitAnswer() {
const answer = this.getCurrentText();
if (!answer) {
this.showFeedback('error', 'Please enter an answer first!');
return;
}
console.log('📝 Submitting answer:', answer);
// Use existing game submission logic if available
if (window.submitAnswer) {
window.submitAnswer(answer);
} else if (window.checkAnswer) {
window.checkAnswer(answer);
} else {
// Fallback - just show success
this.showFeedback('success', `Answer submitted: "${answer}"`);
// Trigger custom event for other systems to listen
const event = new CustomEvent('unifiedWriteSubmit', {
detail: { answer, mode: this.currentMode }
});
document.dispatchEvent(event);
}
// Clear after submission
setTimeout(() => this.clearInput(), 2000);
}
showFeedback(type, message) {
const feedback = this.container.querySelector('#recognitionFeedback');
feedback.className = `recognition-feedback ${type}`;
feedback.textContent = message;
// Auto-clear success/error messages
if (type === 'success' || type === 'error') {
setTimeout(() => {
if (feedback.textContent === message) {
feedback.textContent = '';
feedback.className = 'recognition-feedback';
}
}, 5000);
}
}
// Public methods for external integration
setText(text) {
this.textArea.value = text;
}
getText() {
return this.getCurrentText();
}
setMode(mode) {
this.switchMode(mode);
}
getMode() {
return this.currentMode;
}
setupGameIntegration() {
// Hook into existing submit button if it exists
const submitBtn = document.querySelector('button[onclick="submitAnswer()"]') ||
document.getElementById('submitBtn');
if (submitBtn) {
// Remove existing onclick and add new handler
submitBtn.removeAttribute('onclick');
submitBtn.addEventListener('click', () => {
if (window.submitAnswer) {
window.submitAnswer(this.getText());
}
});
}
// Add Enter key support for keyboard mode
this.canvas.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && this.currentMode === 'keyboard') {
if (window.submitAnswer) {
window.submitAnswer(this.getText());
}
}
});
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Wait a bit for other systems to initialize
setTimeout(() => {
window.unifiedWriteMode = new UnifiedWriteMode();
if (window.unifiedWriteMode.setupGameIntegration) {
window.unifiedWriteMode.setupGameIntegration();
}
console.log('✅ Unified Write Mode initialized and integrated');
}, 500);
});
// Export for use in other scripts
window.UnifiedWriteMode = UnifiedWriteMode;
+94
View File
@@ -0,0 +1,94 @@
/* Native Handwriting Recognition Status Styles */
.native-recognition-status {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.85rem;
margin: 8px 0;
text-align: center;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
animation: statusPulse 2s ease-in-out infinite;
}
.native-recognition-status.native-supported {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.native-recognition-status.fallback {
background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%);
color: #2d3436;
}
.native-recognition-status .status-icon {
margin-right: 6px;
}
.recognition-method-info {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 0.8rem;
color: #6c757d;
margin-top: 8px;
}
.recognition-method-info .method-badge {
background: #e9ecef;
padding: 2px 8px;
border-radius: 12px;
font-weight: 500;
}
.recognition-method-info .method-badge.native {
background: linear-gradient(135deg, #a8e6cf 0%, #7fcdcd 100%);
color: #2d3436;
}
.recognition-method-info .method-badge.enhanced {
background: linear-gradient(135deg, #ffd3a5 0%, #fd9853 100%);
color: #2d3436;
}
@keyframes statusPulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.02); opacity: 0.9; }
}
/* Tablet optimizations for recognition status */
@media (min-width: 768px) {
.native-recognition-status {
font-size: 1rem;
padding: 12px 20px;
margin: 12px 0;
}
.recognition-method-info {
font-size: 0.9rem;
margin-top: 10px;
}
.recognition-method-info .method-badge {
padding: 4px 12px;
font-size: 0.85rem;
}
}
/* Enhanced handwriting canvas for tablet use */
@media (min-width: 768px) {
#handwritingCanvas {
border: 3px solid #e1e8ed;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
background: #fafbfc;
cursor: crosshair;
touch-action: none; /* Prevent default touch behaviors */
}
#handwritingCanvas:active {
border-color: #1da1f2;
box-shadow: 0 4px 20px rgba(29, 161, 242, 0.2);
}
}
+184 -16
View File
@@ -35,6 +35,9 @@ body {
line-height: 1.6;
min-height: 100vh;
padding: 2rem 1rem;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0,0,0,0);
touch-action: manipulation;
}
/* Container and layout */
@@ -46,7 +49,7 @@ body {
/* Typography */
h1 {
font-size: 3rem;
font-size: clamp(2.5rem, 6vw, 4rem);
font-weight: 700;
text-align: center;
margin-bottom: 2rem;
@@ -74,27 +77,36 @@ h3 {
input, textarea, select {
width: 100%;
max-width: 320px;
padding: 0.75rem 1rem;
margin: 0.5rem;
padding: 1rem 1.25rem;
margin: 0.75rem;
border: 2px solid var(--border);
border-radius: var(--radius);
font-size: 1rem;
font-size: 1.125rem;
font-family: inherit;
background: var(--bg-primary);
color: var(--text-primary);
transition: var(--transition);
min-height: 48px; /* Tablet-friendly touch target */
touch-action: manipulation; /* Prevent zoom on focus */
}
/* Special styling for game input */
#userInput {
font-size: 1.2rem;
max-width: 400px;
font-size: 1.5rem;
max-width: 500px;
text-align: center;
background-color: white !important;
color: var(--text-primary) !important;
font-weight: 500;
letter-spacing: 0.5px;
caret-color: var(--primary);
min-height: 60px;
padding: 1.25rem !important;
border: 3px solid var(--primary) !important;
border-radius: 12px !important;
margin: 1.5rem auto !important;
display: block;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2) !important;
}
input:focus, textarea:focus, select:focus {
@@ -113,14 +125,15 @@ textarea {
button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
margin: 0.25rem;
padding: 1rem 2rem;
margin: 0.5rem;
background: var(--primary);
color: #ffffff !important;
border: none;
border-radius: var(--radius);
font-size: 1rem;
border-radius: 12px;
font-size: 1.125rem;
font-weight: 600;
font-family: inherit;
cursor: pointer;
@@ -128,7 +141,10 @@ button {
box-shadow: var(--shadow);
text-decoration: none;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
min-height: 44px;
min-height: 50px;
min-width: 120px;
touch-action: manipulation;
user-select: none;
}
button:hover {
@@ -460,16 +476,23 @@ button:disabled {
/* Game elements */
#wordBox, .word-box, #typingPrompt {
font-size: 3rem;
font-size: clamp(2.5rem, 8vw, 4rem);
font-weight: 700;
text-align: center;
padding: 3rem 2rem;
margin: 2rem 0;
margin: 2rem auto;
max-width: 700px;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--secondary) 100%);
border: 2px solid var(--border);
border-radius: var(--radius);
border: 3px solid var(--primary);
border-radius: 20px;
color: var(--primary);
box-shadow: var(--shadow-lg);
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 2px;
word-break: break-word;
}
/* Word feedback styles */
@@ -578,11 +601,47 @@ button:disabled {
/* Student controls specific styling */
.student-controls button {
min-height: 60px !important;
font-size: 1.1rem !important;
font-size: 1.25rem !important;
font-weight: 600 !important;
text-shadow: none !important;
border: 2px solid rgba(255, 255, 255, 0.2) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
padding: 1.5rem 2rem !important;
}
/* Activity options for tablet */
.activity-option {
background: var(--bg-primary) !important;
border-radius: 16px !important;
padding: 2rem !important;
text-align: center !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
transition: all 0.2s ease !important;
cursor: pointer !important;
border: 2px solid var(--border) !important;
min-height: 200px !important;
display: flex !important;
flex-direction: column !important;
justify-content: space-between !important;
}
.activity-option:hover, .activity-option:active {
transform: translateY(-4px) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important;
border-color: var(--primary) !important;
}
.activity-option h3 {
font-size: 1.5rem !important;
margin: 0.5rem 0 !important;
color: var(--text-primary) !important;
}
.activity-option p {
font-size: 1rem !important;
color: var(--text-secondary) !important;
margin-bottom: 1rem !important;
line-height: 1.4 !important;
}
.student-controls .btn-success {
@@ -688,3 +747,112 @@ button[id*="debug"],
[data-debug="true"] {
display: none !important;
}
/* Tablet and handwriting enhancements */
/* Input mode toggle styles */
.input-mode-toggle {
display: flex !important;
background: var(--secondary) !important;
border-radius: 12px !important;
padding: 0.5rem !important;
margin: 1.5rem auto !important;
max-width: 350px !important;
position: relative !important;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1) !important;
}
.input-mode-toggle button {
flex: 1 !important;
padding: 1rem 1.5rem !important;
border: none !important;
background: transparent !important;
color: var(--text-secondary) !important;
font-weight: 600 !important;
border-radius: 8px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
font-size: 1.125rem !important;
min-height: 50px !important;
margin: 0 !important;
box-shadow: none !important;
text-shadow: none !important;
}
.input-mode-toggle button.active {
background: var(--bg-primary) !important;
color: var(--primary) !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important;
transform: none !important;
}
.input-mode-toggle button:not(.active):hover {
color: var(--text-primary) !important;
background: rgba(255,255,255,0.5) !important;
}
/* Handwriting container styles */
.handwriting-container {
background: var(--bg-primary) !important;
border-radius: 16px !important;
padding: 2rem !important;
margin: 2rem auto !important;
box-shadow: 0 8px 25px rgba(0,0,0,0.1) !important;
border: 2px solid var(--border) !important;
max-width: 700px !important;
text-align: center !important;
}
.handwriting-container h4 {
color: var(--text-primary) !important;
font-size: 1.25rem !important;
margin-bottom: 1.5rem !important;
}
#handwritingCanvas {
border: 3px solid var(--primary) !important;
border-radius: 12px !important;
background: white !important;
cursor: crosshair !important;
display: block !important;
margin: 1rem auto !important;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1) !important;
touch-action: none !important;
max-width: 100% !important;
width: 100% !important;
max-width: 600px !important;
height: 250px !important;
}
/* Canvas controls */
.canvas-controls {
display: flex !important;
justify-content: center !important;
gap: 1rem !important;
margin: 1rem 0 !important;
flex-wrap: wrap !important;
}
.canvas-controls button {
padding: 1rem 2rem !important;
font-size: 1.125rem !important;
min-height: 50px !important;
min-width: 120px !important;
border-radius: 12px !important;
font-weight: 600 !important;
margin: 0.25rem !important;
}
.clear-btn {
background: var(--error) !important;
color: white !important;
}
.recognize-btn {
background: var(--success) !important;
color: white !important;
}
.undo-btn {
background: var(--warning) !important;
color: white !important;
}
File diff suppressed because it is too large. Load diff
+393
View File
@@ -0,0 +1,393 @@
/* Tablet-optimized styles for Spelling Practice App */
/* Tablet and touch device optimizations */
@media screen and (min-width: 600px), (pointer: coarse) {
/* Enhanced touch targets and spacing */
:root {
--touch-target: 48px; /* Minimum touch target size */
--tablet-padding: 2rem;
--tablet-margin: 1.5rem;
--tablet-font-large: 2.5rem;
--tablet-font-medium: 1.5rem;
--tablet-font-small: 1.125rem;
}
/* Body and container adjustments */
body {
padding: var(--tablet-padding);
font-size: var(--tablet-font-small);
}
.container {
max-width: 900px;
padding: 0 var(--tablet-padding);
}
/* Enhanced typography for tablet viewing */
h1 {
font-size: var(--tablet-font-large);
margin-bottom: 2.5rem;
text-align: center;
}
h2 {
font-size: 2.25rem;
margin-bottom: 2rem;
}
h3 {
font-size: 1.75rem;
margin-bottom: 1.5rem;
}
/* Enhanced buttons for touch interaction */
button {
min-height: var(--touch-target);
min-width: var(--touch-target);
padding: 1rem 2rem;
font-size: var(--tablet-font-small);
margin: 0.5rem;
border-radius: 12px;
font-weight: 600;
transition: all 0.2s ease;
cursor: pointer;
box-shadow: var(--shadow);
}
button:hover, button:focus {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
button:active {
transform: translateY(0);
}
/* Primary buttons get special treatment */
button.primary, #startGameBtn, #startTypingBtn, #submitAnswer {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
border: none;
font-size: 1.25rem;
padding: 1.25rem 2.5rem;
min-height: 60px;
}
/* Enhanced form controls for touch */
input, textarea, select {
min-height: var(--touch-target);
padding: 1rem 1.5rem;
font-size: var(--tablet-font-small);
border-radius: 12px;
border: 2px solid var(--border);
margin: 0.75rem;
max-width: 500px;
}
/* Special styling for game input */
#userInput {
font-size: 1.75rem;
min-height: 60px;
text-align: center;
font-weight: 600;
letter-spacing: 1px;
max-width: 600px;
border: 3px solid var(--primary);
box-shadow: var(--shadow-lg);
}
/* Word display optimization */
#wordBox {
font-size: 3.5rem;
padding: 2rem;
margin: 2rem auto;
border-radius: 20px;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--secondary) 100%);
box-shadow: var(--shadow-lg);
text-align: center;
font-weight: 700;
letter-spacing: 2px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
/* Navigation improvements */
#nav {
padding: 1.5rem;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
}
#nav button {
min-width: 120px;
font-size: 1.125rem;
}
/* Admin tabs for tablet */
#adminTabs {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
padding: 1.5rem;
justify-content: center;
}
#adminTabs button {
min-height: 50px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
border-radius: 25px;
}
/* Card layouts optimized for tablet */
.card, .adminTab {
padding: 2rem;
margin: 1.5rem 0;
border-radius: 16px;
box-shadow: var(--shadow-lg);
}
/* Game section enhancements */
#gameSection {
padding: 2rem;
text-align: center;
}
#inputSection {
margin-top: 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
/* Typing section improvements */
#typingSection {
padding: 2rem;
}
#bibleVerseDisplay {
font-size: 1.25rem;
line-height: 1.8;
padding: 2rem;
margin: 2rem 0;
border-radius: 16px;
min-height: 150px;
}
#bibleInput, #typingInput {
min-height: 200px;
font-size: 1.125rem;
line-height: 1.6;
padding: 1.5rem;
}
/* Activity options layout */
#activityOptions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin: 2rem 0;
}
.activity-option {
background: var(--bg-primary);
border-radius: 16px;
padding: 2rem;
text-align: center;
box-shadow: var(--shadow);
transition: var(--transition);
cursor: pointer;
border: 2px solid var(--border);
}
.activity-option:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--primary);
}
/* Summary and results improvements */
#summary {
background: var(--bg-primary);
border-radius: 20px;
padding: 3rem;
margin: 2rem auto;
box-shadow: var(--shadow-lg);
text-align: center;
}
#summary h2 {
font-size: 2.5rem;
margin-bottom: 2rem;
}
#summary p {
font-size: 1.25rem;
margin-bottom: 1rem;
}
/* Login panel optimizations */
#loginPanel {
max-width: 500px;
margin: 0 auto;
padding: 3rem;
background: var(--bg-primary);
border-radius: 20px;
box-shadow: var(--shadow-lg);
}
#loginPanel h2 {
text-align: center;
margin-bottom: 2.5rem;
}
#loginPanel input {
width: 100%;
margin: 1rem 0;
}
#loginPanel button {
width: 100%;
margin-top: 1.5rem;
}
/* User dropdown improvements */
#userDropdown select {
min-height: 50px;
font-size: 1.125rem;
padding: 1rem;
}
/* Progress indicators */
.progress-bar {
height: 12px;
border-radius: 6px;
margin: 1rem 0;
}
/* Feedback messages */
.feedback-correct, .feedback-incorrect {
font-size: 1.25rem;
font-weight: 600;
padding: 1rem;
border-radius: 12px;
margin: 1rem 0;
text-align: center;
}
.feedback-correct {
background-color: #f0fdf4;
color: var(--success);
border: 2px solid var(--success);
}
.feedback-incorrect {
background-color: #fef2f2;
color: var(--error);
border: 2px solid var(--error);
}
/* Badge display improvements */
.badge {
padding: 0.75rem 1.5rem;
font-size: 1rem;
border-radius: 25px;
margin: 0.5rem;
}
/* Charts and analytics */
.chart-container {
margin: 2rem 0;
padding: 2rem;
background: var(--bg-primary);
border-radius: 16px;
box-shadow: var(--shadow);
}
/* Responsive grid layouts */
.tablet-grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
margin: 1.5rem 0;
}
.tablet-grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin: 1.5rem 0;
}
/* Utility classes for tablet */
.tablet-center {
text-align: center;
margin: 0 auto;
}
.tablet-large-text {
font-size: var(--tablet-font-medium);
font-weight: 600;
}
.tablet-padding {
padding: var(--tablet-padding);
}
.tablet-margin {
margin: var(--tablet-margin);
}
}
/* Landscape tablet specific adjustments */
@media screen and (min-width: 768px) and (max-width: 1024px) and (orientation: landscape) {
body {
padding: 1rem 2rem;
}
#wordBox {
font-size: 3rem;
padding: 1.5rem;
margin: 1.5rem auto;
min-height: 100px;
}
#gameSection, #typingSection {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 70vh;
}
#nav {
padding: 1rem;
}
.tablet-grid-3 {
grid-template-columns: repeat(4, 1fr);
}
}
/* Portrait tablet specific adjustments */
@media screen and (min-width: 768px) and (max-width: 1024px) and (orientation: portrait) {
#activityOptions {
grid-template-columns: 1fr;
max-width: 400px;
margin: 2rem auto;
}
.tablet-grid-2 {
grid-template-columns: 1fr;
}
.tablet-grid-3 {
grid-template-columns: repeat(2, 1fr);
}
}
+297
View File
@@ -0,0 +1,297 @@
/* Unified Write Mode - Large Canvas + Keyboard Support */
.unified-write-mode {
width: 100%;
max-width: none;
padding: 20px;
}
.large-writing-canvas {
width: 100%;
min-height: 300px;
height: 40vh;
border: 3px solid #e1e8ed;
border-radius: 16px;
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
box-shadow:
inset 0 2px 4px rgba(0,0,0,0.06),
0 4px 20px rgba(0,0,0,0.1);
position: relative;
cursor: text;
touch-action: none;
font-family: 'Comic Sans MS', cursive, sans-serif;
font-size: 32px;
line-height: 1.4;
padding: 20px;
color: #2c3e50;
overflow: hidden;
}
.large-writing-canvas:focus-within {
border-color: #3b82f6;
box-shadow:
inset 0 2px 4px rgba(0,0,0,0.06),
0 4px 20px rgba(59, 130, 246, 0.2),
0 0 0 3px rgba(59, 130, 246, 0.1);
}
.canvas-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.canvas-overlay.active {
pointer-events: all;
}
.text-input-area {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
bottom: 20px;
background: transparent;
border: none;
outline: none;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: inherit;
resize: none;
z-index: 2;
}
.text-input-area::placeholder {
color: #94a3b8;
opacity: 0.8;
}
.input-mode-selector {
display: flex;
gap: 12px;
margin-bottom: 20px;
justify-content: center;
flex-wrap: wrap;
}
.mode-button {
padding: 12px 24px;
border: 2px solid #e1e8ed;
border-radius: 25px;
background: white;
color: #64748b;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
}
.mode-button:hover {
border-color: #3b82f6;
color: #3b82f6;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
.mode-button.active {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border-color: #1d4ed8;
color: white;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
}
.mode-button .icon {
font-size: 18px;
}
.canvas-controls {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: center;
flex-wrap: wrap;
}
.canvas-controls .btn {
padding: 12px 20px;
border: none;
border-radius: 20px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.btn-recognize {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
.btn-recognize:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.3);
}
.btn-clear {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
}
.btn-clear:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(239, 68, 68, 0.3);
}
.btn-submit {
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
font-size: 16px;
padding: 14px 28px;
}
.btn-submit:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
}
.recognition-feedback {
margin-top: 15px;
padding: 12px 20px;
border-radius: 12px;
font-weight: 500;
text-align: center;
min-height: 24px;
}
.recognition-feedback.success {
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
color: #065f46;
border: 1px solid #10b981;
}
.recognition-feedback.processing {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
color: #92400e;
border: 1px solid #f59e0b;
}
.recognition-feedback.error {
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
color: #991b1b;
border: 1px solid #ef4444;
}
/* Tablet optimizations */
@media (min-width: 768px) {
.large-writing-canvas {
min-height: 400px;
height: 50vh;
font-size: 36px;
padding: 30px;
}
.mode-button {
padding: 16px 32px;
font-size: 18px;
}
.canvas-controls .btn {
padding: 16px 28px;
font-size: 16px;
}
.btn-submit {
font-size: 18px;
padding: 18px 36px;
}
}
/* Large tablets and desktop */
@media (min-width: 1024px) {
.large-writing-canvas {
min-height: 450px;
height: 55vh;
font-size: 40px;
padding: 40px;
}
.unified-write-mode {
max-width: 1000px;
margin: 0 auto;
}
}
/* Stylus and fine pointer support */
@media (pointer: fine) {
.large-writing-canvas {
cursor: crosshair;
}
.text-input-area {
cursor: text;
}
}
/* Handwriting mode specific styles */
.handwriting-mode .text-input-area {
pointer-events: none;
opacity: 0.3;
}
.handwriting-mode .canvas-overlay {
pointer-events: all;
}
/* Keyboard mode specific styles */
.keyboard-mode .canvas-overlay {
pointer-events: none;
}
.keyboard-mode .text-input-area {
pointer-events: all;
opacity: 1;
}
/* Visual feedback for active mode */
.handwriting-mode .large-writing-canvas {
background: linear-gradient(145deg, #f0f9ff 0%, #e0f2fe 100%);
}
.keyboard-mode .large-writing-canvas {
background: linear-gradient(145deg, #f0fdf4 0%, #dcfce7 100%);
}
/* Input method indicator */
.input-method-indicator {
position: absolute;
top: 10px;
right: 20px;
background: rgba(0,0,0,0.1);
color: #64748b;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
z-index: 3;
}
.handwriting-mode .input-method-indicator {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.keyboard-mode .input-method-indicator {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}