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:
1 parent
0b9d46fed1
commit
8d37038177
24 files changed
+9506
-126
No files matched your search
@@ -0,0 +1,115 @@
|
|||||||
|
# 📱 Tablet Handwriting Recognition Test Deployment
|
||||||
|
|
||||||
|
## 🚀 Quick Deploy
|
||||||
|
|
||||||
|
### Pull and Run the Test Image:
|
||||||
|
```bash
|
||||||
|
# Pull the test image
|
||||||
|
docker pull ghcr.io/nmemmert/spelling:test
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -d -p 80:3000 --name spelling-tablet-test ghcr.io/nmemmert/spelling:test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternative with Docker Compose:
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
spelling-test:
|
||||||
|
image: ghcr.io/nmemmert/spelling:test
|
||||||
|
ports:
|
||||||
|
- "80:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "test-version=1.2.0-tablet"
|
||||||
|
- "features=handwriting-recognition"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🖊️ What's Included in This Test Build:
|
||||||
|
|
||||||
|
### ✅ Native Tablet Support:
|
||||||
|
- **Web Handwriting API** integration (Edge/Chrome experimental)
|
||||||
|
- **Windows Ink Platform** compatibility
|
||||||
|
- **Pointer Events API** with pressure sensitivity
|
||||||
|
- **Touch Events API** optimization
|
||||||
|
- **Force Touch** support (3D Touch devices)
|
||||||
|
|
||||||
|
### 🎯 Enhanced Recognition Features:
|
||||||
|
- **Multi-algorithm recognition** chain
|
||||||
|
- **Contextual word matching** for spelling practice
|
||||||
|
- **Pressure-sensitive** stroke analysis
|
||||||
|
- **Timing-based** pattern recognition
|
||||||
|
- **Dictionary-based** post-processing
|
||||||
|
|
||||||
|
### 📝 Tablet-Optimized UI:
|
||||||
|
- **Touch-friendly** interface elements
|
||||||
|
- **Stylus-optimized** canvas
|
||||||
|
- **Responsive** tablet layouts
|
||||||
|
- **Visual feedback** for recognition status
|
||||||
|
- **Auto-detection** of native capabilities
|
||||||
|
|
||||||
|
## 🧪 Testing Instructions:
|
||||||
|
|
||||||
|
### 1. **Access the Application:**
|
||||||
|
- Open your tablet browser
|
||||||
|
- Navigate to your server (e.g., `http://your-server/`)
|
||||||
|
- Look for "📱 Native Tablet Recognition" status
|
||||||
|
|
||||||
|
### 2. **Enable Browser Features (if needed):**
|
||||||
|
- **Edge:** `edge://flags/` → Enable "Experimental Web Platform features"
|
||||||
|
- **Chrome:** `chrome://flags/` → Enable "Experimental Web Platform features"
|
||||||
|
- **Safari:** Enable experimental features in settings
|
||||||
|
|
||||||
|
### 3. **Test Handwriting Recognition:**
|
||||||
|
- Switch to "✏️ Write" mode
|
||||||
|
- Draw letters/words on the canvas with stylus or finger
|
||||||
|
- Click "🔍 Recognize" button
|
||||||
|
- Test words: "Love", "Boy", "Cat", "Make", "Time"
|
||||||
|
|
||||||
|
### 4. **Check Native Features:**
|
||||||
|
- Look for pressure sensitivity (if using stylus)
|
||||||
|
- Test different drawing speeds
|
||||||
|
- Try both print and cursive writing
|
||||||
|
- Check recognition accuracy vs. desktop
|
||||||
|
|
||||||
|
## 📊 Expected Results:
|
||||||
|
|
||||||
|
### **On Modern Tablets:**
|
||||||
|
- ✅ Pointer Events API should work
|
||||||
|
- ✅ Touch Events should be detected
|
||||||
|
- 🔧 Web Handwriting API (needs experimental flags)
|
||||||
|
- ✅ Much better recognition accuracy than desktop
|
||||||
|
|
||||||
|
### **Performance Indicators:**
|
||||||
|
- **Recognition Status:** Should show "Native" or "Enhanced"
|
||||||
|
- **Pressure Data:** Available with compatible stylus
|
||||||
|
- **Touch Optimization:** Smooth drawing experience
|
||||||
|
- **Word Accuracy:** Significantly improved for spelling words
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting:
|
||||||
|
|
||||||
|
### If Recognition is Poor:
|
||||||
|
1. Enable experimental browser features
|
||||||
|
2. Use a compatible stylus (Surface Pen, Apple Pencil, S-Pen)
|
||||||
|
3. Write more slowly and clearly
|
||||||
|
4. Try print letters instead of cursive
|
||||||
|
|
||||||
|
### If No Native Support:
|
||||||
|
- Check browser compatibility
|
||||||
|
- Enable experimental flags
|
||||||
|
- Update to latest browser version
|
||||||
|
- Test on different tablet models
|
||||||
|
|
||||||
|
## 🔗 Image Details:
|
||||||
|
- **Image:** `ghcr.io/nmemmert/spelling:test`
|
||||||
|
- **Version:** `1.2.0-tablet`
|
||||||
|
- **Features:** Native handwriting + Enhanced fallback
|
||||||
|
- **Build Date:** October 2, 2025
|
||||||
|
|
||||||
|
## 📞 Feedback:
|
||||||
|
Test the handwriting recognition and report:
|
||||||
|
- Which device/browser combination
|
||||||
|
- Native API detection results
|
||||||
|
- Recognition accuracy for different words
|
||||||
|
- Any pressure sensitivity detected
|
||||||
|
- Overall user experience improvements
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
# Tablet UI and Handwriting Recognition Features
|
||||||
|
|
||||||
|
This document outlines the new tablet-optimized UI and handwriting recognition features added to the Spelling Practice App.
|
||||||
|
|
||||||
|
## 🎨 Tablet UI Overhaul
|
||||||
|
|
||||||
|
### Features Added
|
||||||
|
|
||||||
|
1. **Tablet-Optimized CSS** (`public/tablet.css`)
|
||||||
|
- Enhanced touch targets (minimum 48px)
|
||||||
|
- Improved typography for tablet viewing
|
||||||
|
- Better spacing and padding for tablet use
|
||||||
|
- Responsive grid layouts
|
||||||
|
- Landscape and portrait orientation support
|
||||||
|
|
||||||
|
2. **Enhanced Student Dashboard**
|
||||||
|
- Card-based activity options with large touch targets
|
||||||
|
- Visual icons and descriptions for each activity
|
||||||
|
- Improved button layouts with better spacing
|
||||||
|
- Better visual hierarchy
|
||||||
|
|
||||||
|
3. **Tablet Utilities** (`public/js/tablet-utils.js`)
|
||||||
|
- Automatic tablet detection
|
||||||
|
- Touch feedback and animations
|
||||||
|
- Swipe gesture support
|
||||||
|
- Orientation change handling
|
||||||
|
- Haptic feedback (where supported)
|
||||||
|
- Enhanced button interactions
|
||||||
|
|
||||||
|
### Tablet-Specific Optimizations
|
||||||
|
|
||||||
|
- **Touch Targets**: All interactive elements are at least 48px in size
|
||||||
|
- **Font Sizes**: Optimized for tablet viewing distances
|
||||||
|
- **Spacing**: Increased padding and margins for better touch interaction
|
||||||
|
- **Visual Feedback**: Touch ripples and scale animations
|
||||||
|
- **Keyboard Optimization**: Prevents zoom on input focus, auto-scroll to inputs
|
||||||
|
|
||||||
|
## ✏️ Handwriting Recognition
|
||||||
|
|
||||||
|
### Features Added
|
||||||
|
|
||||||
|
1. **Handwriting Canvas** (`public/js/handwriting.js`)
|
||||||
|
- HTML5 Canvas for drawing
|
||||||
|
- Multi-touch support
|
||||||
|
- Smooth drawing with configurable pen settings
|
||||||
|
- Stroke recording and playback
|
||||||
|
|
||||||
|
2. **Drawing Controls**
|
||||||
|
- Clear canvas button
|
||||||
|
- Undo last stroke
|
||||||
|
- Adjustable pen size (1-10px)
|
||||||
|
- Color selection
|
||||||
|
- Auto-recognition toggle
|
||||||
|
|
||||||
|
3. **Recognition Engine**
|
||||||
|
- Multiple recognition method support
|
||||||
|
- Basic pattern recognition (fallback)
|
||||||
|
- Integration with Google Vision API (configurable)
|
||||||
|
- MyScript API support (configurable)
|
||||||
|
|
||||||
|
### Input Mode Toggle
|
||||||
|
|
||||||
|
Students can switch between:
|
||||||
|
- **Typing Mode** (⌨️): Traditional keyboard input
|
||||||
|
- **Handwriting Mode** (✏️): Canvas-based writing recognition
|
||||||
|
|
||||||
|
### Handwriting CSS Styles (`public/handwriting.css`)
|
||||||
|
|
||||||
|
- Canvas styling with proper focus indicators
|
||||||
|
- Control buttons with touch-friendly sizing
|
||||||
|
- Recognition status display
|
||||||
|
- Tablet-optimized layouts
|
||||||
|
- Mobile responsive design
|
||||||
|
|
||||||
|
## 🛠️ Implementation Details
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
public/
|
||||||
|
├── tablet.css # Tablet-optimized styles
|
||||||
|
├── handwriting.css # Handwriting recognition styles
|
||||||
|
└── js/
|
||||||
|
├── handwriting.js # Handwriting recognition logic
|
||||||
|
└── tablet-utils.js # Tablet optimization utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Functions
|
||||||
|
|
||||||
|
#### Tablet Utilities
|
||||||
|
- `detectTablet()`: Automatic device detection
|
||||||
|
- `optimizeButtons()`: Enhanced button interactions
|
||||||
|
- `handleOrientationChange()`: Responsive orientation handling
|
||||||
|
- `addSwipeGestures()`: Navigation gestures
|
||||||
|
|
||||||
|
#### Handwriting Recognition
|
||||||
|
- `startDrawing()`: Begin stroke recording
|
||||||
|
- `draw()`: Track drawing movements
|
||||||
|
- `recognizeText()`: Convert drawing to text
|
||||||
|
- `clearCanvas()`: Reset drawing area
|
||||||
|
- `toggleInputMode()`: Switch between typing/writing
|
||||||
|
|
||||||
|
### Integration Points
|
||||||
|
|
||||||
|
1. **Game Integration**:
|
||||||
|
- `submitAnswer()` function updated to handle both input types
|
||||||
|
- `showWord()` function resets both input methods
|
||||||
|
- Recognition results auto-fill the game input
|
||||||
|
|
||||||
|
2. **UI Enhancement**:
|
||||||
|
- Activity cards with improved touch targets
|
||||||
|
- Input mode toggle in game section
|
||||||
|
- Canvas controls with tablet-friendly sizing
|
||||||
|
|
||||||
|
## 📱 Device Support
|
||||||
|
|
||||||
|
### Tablet Compatibility
|
||||||
|
- iPad (all generations)
|
||||||
|
- Android tablets
|
||||||
|
- Windows tablets
|
||||||
|
- Chrome OS tablets
|
||||||
|
|
||||||
|
### Browser Support
|
||||||
|
- Safari (iOS)
|
||||||
|
- Chrome (Android/Desktop)
|
||||||
|
- Firefox
|
||||||
|
- Edge
|
||||||
|
|
||||||
|
### Touch Features
|
||||||
|
- Multi-touch drawing
|
||||||
|
- Gesture navigation
|
||||||
|
- Haptic feedback (where available)
|
||||||
|
- Orientation detection
|
||||||
|
|
||||||
|
## 🚀 Usage Instructions
|
||||||
|
|
||||||
|
### For Students
|
||||||
|
|
||||||
|
1. **Starting the App**:
|
||||||
|
- Login and access the student dashboard
|
||||||
|
- Choose from improved activity cards
|
||||||
|
|
||||||
|
2. **Spelling Game with Handwriting**:
|
||||||
|
- Start a spelling game
|
||||||
|
- Use the toggle to switch between typing and handwriting
|
||||||
|
- Draw letters on the canvas for recognition
|
||||||
|
- Submit answers using either input method
|
||||||
|
|
||||||
|
3. **Drawing Tips**:
|
||||||
|
- Write clearly with consistent letter sizes
|
||||||
|
- Use print letters rather than cursive
|
||||||
|
- Allow space between letters
|
||||||
|
- Press firmly but write smoothly
|
||||||
|
|
||||||
|
### For Administrators
|
||||||
|
|
||||||
|
The tablet optimizations and handwriting features work seamlessly with existing admin functions:
|
||||||
|
- User management remains unchanged
|
||||||
|
- Word list management improved with better touch targets
|
||||||
|
- Analytics display optimized for tablet viewing
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Handwriting Recognition
|
||||||
|
|
||||||
|
To enable advanced recognition services, configure in `handwriting.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Google Vision API (requires API key)
|
||||||
|
async recognizeWithGoogleVision(imageData) {
|
||||||
|
// Add your Google Vision API implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyScript API (requires API credentials)
|
||||||
|
async recognizeWithMyScript() {
|
||||||
|
// Add your MyScript API implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tablet Detection
|
||||||
|
|
||||||
|
Tablet detection can be customized in `tablet-utils.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
detectTablet() {
|
||||||
|
// Customize detection logic based on your requirements
|
||||||
|
const isTouch = 'ontouchstart' in window;
|
||||||
|
const isTabletSize = window.innerWidth >= 768 && window.innerWidth <= 1024;
|
||||||
|
return isTouch && isTabletSize;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Performance Considerations
|
||||||
|
|
||||||
|
### Optimization Features
|
||||||
|
- Efficient canvas rendering
|
||||||
|
- Minimal DOM manipulation
|
||||||
|
- Event delegation for better performance
|
||||||
|
- Throttled recognition requests
|
||||||
|
- Optimized CSS animations
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
- Canvas cleanup on navigation
|
||||||
|
- Event listener removal
|
||||||
|
- Stroke data management
|
||||||
|
- Automatic cleanup of drawing history
|
||||||
|
|
||||||
|
## 🔄 Future Enhancements
|
||||||
|
|
||||||
|
### Planned Features
|
||||||
|
1. **Advanced Recognition**: ML-based handwriting models
|
||||||
|
2. **Multi-language Support**: Recognition for different languages
|
||||||
|
3. **Gesture Shortcuts**: Drawing gestures for common actions
|
||||||
|
4. **Offline Recognition**: Client-side recognition capabilities
|
||||||
|
5. **Collaborative Features**: Shared drawing sessions
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
- Voice commands integration
|
||||||
|
- High contrast mode support
|
||||||
|
- Screen reader compatibility
|
||||||
|
- Keyboard navigation fallbacks
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Canvas Not Responding**:
|
||||||
|
- Check if touch events are properly bound
|
||||||
|
- Verify canvas dimensions are set correctly
|
||||||
|
- Ensure no CSS is blocking pointer events
|
||||||
|
|
||||||
|
2. **Recognition Not Working**:
|
||||||
|
- Verify canvas has drawing data
|
||||||
|
- Check console for recognition errors
|
||||||
|
- Ensure fallback recognition is enabled
|
||||||
|
|
||||||
|
3. **Tablet Detection Issues**:
|
||||||
|
- Test on actual tablet devices
|
||||||
|
- Verify user agent detection
|
||||||
|
- Check window dimensions
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug logging by adding to console:
|
||||||
|
```javascript
|
||||||
|
localStorage.setItem('debugHandwriting', 'true');
|
||||||
|
localStorage.setItem('debugTablet', 'true');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Analytics Integration
|
||||||
|
|
||||||
|
The new features integrate with existing analytics:
|
||||||
|
- Track handwriting vs typing usage
|
||||||
|
- Monitor recognition accuracy
|
||||||
|
- Tablet interaction patterns
|
||||||
|
- Performance metrics
|
||||||
|
|
||||||
|
## 🔒 Privacy and Security
|
||||||
|
|
||||||
|
- Canvas drawings are processed locally when possible
|
||||||
|
- No drawing data is permanently stored
|
||||||
|
- Recognition requests can be configured for privacy compliance
|
||||||
|
- All touch interactions respect user privacy settings
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Tablet UI and Handwriting Issues - Troubleshooting Guide
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
### ✅ What's Been Implemented:
|
||||||
|
|
||||||
|
1. **Tablet-Optimized CSS**
|
||||||
|
- `tablet.css` - Comprehensive tablet styles
|
||||||
|
- `handwriting.css` - Canvas and handwriting-specific styles
|
||||||
|
- Enhanced button sizes (min 50px height, larger fonts)
|
||||||
|
- Touch-friendly spacing and padding
|
||||||
|
- Responsive design for tablet viewports
|
||||||
|
|
||||||
|
2. **Handwriting Recognition System**
|
||||||
|
- `handwriting.js` - Full canvas drawing implementation
|
||||||
|
- Touch and mouse event handling
|
||||||
|
- Basic pattern recognition (extendable)
|
||||||
|
- Canvas controls (clear, undo, recognize)
|
||||||
|
|
||||||
|
3. **Enhanced HTML Structure**
|
||||||
|
- Input mode toggle (Type ⌨️ / Write ✏️)
|
||||||
|
- Canvas element with proper sizing
|
||||||
|
- Tablet-friendly activity cards
|
||||||
|
- Improved button layouts
|
||||||
|
|
||||||
|
### ❌ Current Issues Identified:
|
||||||
|
|
||||||
|
1. **Tablet CSS Not Fully Applied**
|
||||||
|
- Media query might be too restrictive
|
||||||
|
- Some styles being overridden by existing CSS
|
||||||
|
- Need better tablet detection
|
||||||
|
|
||||||
|
2. **Handwriting Interface Not Visible**
|
||||||
|
- Canvas might not be properly initialized
|
||||||
|
- Toggle function might not be working
|
||||||
|
- CSS display properties conflicting
|
||||||
|
|
||||||
|
## Quick Fixes Needed:
|
||||||
|
|
||||||
|
### Fix 1: Improve Tablet CSS Application
|
||||||
|
```css
|
||||||
|
/* Change media query to be more inclusive */
|
||||||
|
@media screen and (min-width: 600px), (pointer: coarse) {
|
||||||
|
/* Tablet styles */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 2: Force Tablet Styles in Main CSS
|
||||||
|
Add `!important` declarations to critical tablet styles in `styles.css`
|
||||||
|
|
||||||
|
### Fix 3: Debug Canvas Initialization
|
||||||
|
Add console logging to track canvas setup process
|
||||||
|
|
||||||
|
### Fix 4: Simplify Toggle Function
|
||||||
|
Make input mode switching more reliable with explicit show/hide
|
||||||
|
|
||||||
|
## Immediate Action Plan:
|
||||||
|
|
||||||
|
1. **Test the separate tablet-test.html page** - This shows how it should work
|
||||||
|
2. **Fix CSS conflicts** in the main app
|
||||||
|
3. **Improve canvas initialization** with better error handling
|
||||||
|
4. **Add debug logging** to track what's happening
|
||||||
|
5. **Ensure student login works** to access handwriting features
|
||||||
|
|
||||||
|
## Files to Check:
|
||||||
|
|
||||||
|
- `public/index.html` - HTML structure
|
||||||
|
- `public/styles.css` - Main styles (may need tablet overrides)
|
||||||
|
- `public/tablet.css` - Tablet-specific styles
|
||||||
|
- `public/handwriting.css` - Canvas styles
|
||||||
|
- `public/js/handwriting.js` - Canvas functionality
|
||||||
|
- `public/js/tablet-utils.js` - Tablet optimizations
|
||||||
|
|
||||||
|
## Testing Steps:
|
||||||
|
|
||||||
|
1. Open `tablet-test.html` to see working example
|
||||||
|
2. Login as student (nate/nate123 or student1/123456)
|
||||||
|
3. Start spelling game
|
||||||
|
4. Check browser console for errors
|
||||||
|
5. Try toggling between type/write modes
|
||||||
|
6. Test canvas drawing if handwriting mode works
|
||||||
|
|
||||||
|
## Browser Console Commands for Testing:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Check if handwriting is initialized
|
||||||
|
console.log(window.handwritingRecognition);
|
||||||
|
|
||||||
|
// Check canvas element
|
||||||
|
console.log(document.getElementById('handwritingCanvas'));
|
||||||
|
|
||||||
|
// Test toggle function
|
||||||
|
window.toggleInputMode('handwriting');
|
||||||
|
|
||||||
|
// Check CSS loading
|
||||||
|
console.log(document.querySelector('link[href*="tablet.css"]'));
|
||||||
|
```
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# PowerShell script to build and push Docker image with "test" tag for tablet testing
|
||||||
|
|
||||||
|
# Set variables
|
||||||
|
$IMAGE_NAME = "ghcr.io/nmemmert/spelling"
|
||||||
|
$TEST_TAG = "test"
|
||||||
|
$VERSION = (Get-Content -Path "version.json" | ConvertFrom-Json).version
|
||||||
|
|
||||||
|
Write-Host "🖊️ Building Docker image for TABLET HANDWRITING TESTING..." -ForegroundColor Magenta
|
||||||
|
Write-Host "📦 Image: ${IMAGE_NAME}:${TEST_TAG}" -ForegroundColor Cyan
|
||||||
|
Write-Host "🔖 Base Version: ${VERSION}" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Add test-specific metadata to the build
|
||||||
|
$BUILD_DATE = Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ"
|
||||||
|
$GIT_COMMIT = git rev-parse --short HEAD 2>$null
|
||||||
|
if (-not $GIT_COMMIT) { $GIT_COMMIT = "unknown" }
|
||||||
|
|
||||||
|
Write-Host "📝 Git Commit: $GIT_COMMIT" -ForegroundColor Yellow
|
||||||
|
Write-Host "📅 Build Date: $BUILD_DATE" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Build the image with test tag and labels
|
||||||
|
Write-Host "🔨 Building Docker image..." -ForegroundColor Cyan
|
||||||
|
docker build `
|
||||||
|
--label "version=${VERSION}-test" `
|
||||||
|
--label "build-date=${BUILD_DATE}" `
|
||||||
|
--label "git-commit=${GIT_COMMIT}" `
|
||||||
|
--label "tablet-features=enabled" `
|
||||||
|
--label "handwriting-recognition=native+enhanced" `
|
||||||
|
-t ${IMAGE_NAME}:${TEST_TAG} `
|
||||||
|
-t ${IMAGE_NAME}:${VERSION}-test `
|
||||||
|
.
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "❌ Docker build failed!" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "✅ Build completed." -ForegroundColor Green
|
||||||
|
Write-Host "🧪 Testing the image..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Run container for testing
|
||||||
|
docker run --name spelling-test-tablet -d -p 3002:3000 ${IMAGE_NAME}:${TEST_TAG}
|
||||||
|
|
||||||
|
# Wait for container to start
|
||||||
|
Write-Host "⏱️ Waiting for container to start..." -ForegroundColor Yellow
|
||||||
|
Start-Sleep -Seconds 8
|
||||||
|
|
||||||
|
# Test health endpoint
|
||||||
|
try {
|
||||||
|
$response = Invoke-WebRequest -Uri "http://localhost:3002/health" -UseBasicParsing -TimeoutSec 10
|
||||||
|
if ($response.StatusCode -eq 200) {
|
||||||
|
Write-Host "✅ Health check passed!" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Test main page
|
||||||
|
$mainResponse = Invoke-WebRequest -Uri "http://localhost:3002/" -UseBasicParsing -TimeoutSec 10
|
||||||
|
if ($mainResponse.StatusCode -eq 200) {
|
||||||
|
Write-Host "✅ Main page accessible!" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Check for handwriting features in the HTML
|
||||||
|
if ($mainResponse.Content -like "*native-handwriting.js*") {
|
||||||
|
Write-Host "🖊️ Native handwriting support detected!" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
if ($mainResponse.Content -like "*handwriting-canvas*") {
|
||||||
|
Write-Host "🎨 Handwriting canvas found!" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ Health check failed with status $($response.StatusCode)" -ForegroundColor Red
|
||||||
|
docker logs spelling-test-tablet
|
||||||
|
docker stop spelling-test-tablet
|
||||||
|
docker rm spelling-test-tablet
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ Health check failed: $_" -ForegroundColor Red
|
||||||
|
Write-Host "📋 Container logs:" -ForegroundColor Yellow
|
||||||
|
docker logs spelling-test-tablet
|
||||||
|
docker stop spelling-test-tablet
|
||||||
|
docker rm spelling-test-tablet
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop and remove test container
|
||||||
|
docker stop spelling-test-tablet
|
||||||
|
docker rm spelling-test-tablet
|
||||||
|
|
||||||
|
Write-Host "📤 Pushing image to GitHub Container Registry..." -ForegroundColor Cyan
|
||||||
|
Write-Host "🏷️ Pushing tags: ${TEST_TAG} and ${VERSION}-test" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
docker push ${IMAGE_NAME}:${TEST_TAG}
|
||||||
|
docker push ${IMAGE_NAME}:${VERSION}-test
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "" -ForegroundColor Green
|
||||||
|
Write-Host "🚀 Successfully built and pushed TABLET TEST image!" -ForegroundColor Green
|
||||||
|
Write-Host "" -ForegroundColor White
|
||||||
|
Write-Host "📱 TABLET TESTING INSTRUCTIONS:" -ForegroundColor Magenta
|
||||||
|
Write-Host "=================================" -ForegroundColor Magenta
|
||||||
|
Write-Host "1. Deploy using: docker pull ${IMAGE_NAME}:${TEST_TAG}" -ForegroundColor White
|
||||||
|
Write-Host "2. Run on tablet-accessible server" -ForegroundColor White
|
||||||
|
Write-Host "3. Test handwriting recognition with stylus/touch" -ForegroundColor White
|
||||||
|
Write-Host "4. Features included:" -ForegroundColor White
|
||||||
|
Write-Host " • Native tablet handwriting APIs" -ForegroundColor Cyan
|
||||||
|
Write-Host " • Pressure-sensitive stylus support" -ForegroundColor Cyan
|
||||||
|
Write-Host " • Enhanced pattern recognition" -ForegroundColor Cyan
|
||||||
|
Write-Host " • Touch event optimization" -ForegroundColor Cyan
|
||||||
|
Write-Host " • Pointer Events API integration" -ForegroundColor Cyan
|
||||||
|
Write-Host "" -ForegroundColor White
|
||||||
|
Write-Host "🔗 Image: ${IMAGE_NAME}:${TEST_TAG}" -ForegroundColor Yellow
|
||||||
|
Write-Host "" -ForegroundColor White
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ Push failed!" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Press any key to exit..."
|
||||||
|
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||||
@@ -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
@@ -2,17 +2,27 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<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>
|
<title>✨ Modern Spelling Practice</title>
|
||||||
<link href="styles.css" rel="stylesheet"/>
|
<link href="styles.css" rel="stylesheet"/>
|
||||||
<link href="mobile.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="tab-contrast.css" rel="stylesheet"/>
|
||||||
<link href="badges.css" rel="stylesheet"/>
|
<link href="badges.css" rel="stylesheet"/>
|
||||||
<link href="analytics.css" rel="stylesheet"/>
|
<link href="analytics.css" rel="stylesheet"/>
|
||||||
<link href="gamification.css" rel="stylesheet"/>
|
<link href="gamification.css" rel="stylesheet"/>
|
||||||
<link href="spaced-repetition.css" rel="stylesheet"/>
|
<link href="spaced-repetition.css" rel="stylesheet"/>
|
||||||
<!-- Scripts -->
|
<!-- 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 -->
|
<!-- Fallback for jsSHA library if CDN fails -->
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener('DOMContentLoaded', function() {
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
@@ -40,6 +50,11 @@
|
|||||||
<script src="js/analytics.js"></script>
|
<script src="js/analytics.js"></script>
|
||||||
<script src="js/gamification.js"></script>
|
<script src="js/gamification.js"></script>
|
||||||
<script src="js/spaced-repetition.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>
|
<script src="js/main.js"></script>
|
||||||
<!-- Initialization script - loads last -->
|
<!-- Initialization script - loads last -->
|
||||||
<script src="js/chart-safety.js"></script>
|
<script src="js/chart-safety.js"></script>
|
||||||
@@ -70,50 +85,233 @@
|
|||||||
<p id="loginMessage" style="margin-top: 1rem; text-align: center;"></p>
|
<p id="loginMessage" style="margin-top: 1rem; text-align: center;"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Enhanced Navigation Menu -->
|
||||||
<div id="nav" class="hidden">
|
<div id="nav" class="hidden">
|
||||||
<p><strong id="userInfo"></strong></p>
|
<div class="nav-header">
|
||||||
<div class="flex gap-4 justify-center" style="margin-top: 1rem;">
|
<p><strong id="userInfo"></strong></p>
|
||||||
<button class="hidden" id="adminBtn" onclick="showAdmin()">👨💼 Admin Panel</button>
|
<div class="user-badge" id="userBadge"></div>
|
||||||
<button class="hidden" id="studentBtn" onclick="showStudent()">🎓 Student Panel</button>
|
</div>
|
||||||
<button id="logoutBtn" onclick="logoutUser()" class="btn-secondary">Sign Out</button>
|
|
||||||
|
<!-- 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Admin Panel -->
|
<!-- Admin Panel -->
|
||||||
<div class="hidden" id="adminPanel">
|
<div class="hidden" id="adminPanel">
|
||||||
<div class="card-header">
|
<!-- Admin Dashboard Overview -->
|
||||||
<h2>👨💼 Admin Dashboard</h2>
|
<div class="card">
|
||||||
<p style="color: var(--text-secondary);">Manage users, word lists, and view analytics</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Admin Tabs -->
|
<!-- Hidden Admin Tabs - Accessed via Menu -->
|
||||||
<div id="adminTabs">
|
<div style="display: none;">
|
||||||
<button class="active" data-tab="tabUsers" data-emoji="👤">Users</button>
|
<div id="adminTabs">
|
||||||
<button data-tab="tabWords" data-emoji="📝">Word Lists</button>
|
<button class="active" data-tab="tabUsers" data-emoji="👤">Users</button>
|
||||||
<button data-tab="tabAnalytics" data-emoji="📊">Analytics</button>
|
<button data-tab="tabWords" data-emoji="📝">Word Lists</button>
|
||||||
<button data-tab="tabResults" data-emoji="📋">Results</button>
|
<button data-tab="tabAnalytics" data-emoji="📊">Analytics</button>
|
||||||
<button data-tab="tabReports" data-emoji="📘">Reports</button>
|
<button data-tab="tabResults" data-emoji="📋">Results</button>
|
||||||
<button data-tab="tabSessions" data-emoji="📅">Sessions</button>
|
<button data-tab="tabReports" data-emoji="📘">Reports</button>
|
||||||
<button data-tab="tabBadges" data-emoji="🏅">Badges</button>
|
<button data-tab="tabSessions" data-emoji="📅">Sessions</button>
|
||||||
<button data-tab="tabPasswords" data-emoji="🔐">Passwords</button>
|
<button data-tab="tabBadges" data-emoji="🏅">Badges</button>
|
||||||
<button data-tab="tabTheme" data-emoji="🎨">Themes</button>
|
<button data-tab="tabPasswords" data-emoji="🔐">Passwords</button>
|
||||||
|
<button data-tab="tabTheme" data-emoji="🎨">Themes</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Users Tab -->
|
<!-- Users Tab -->
|
||||||
<div id="tabUsers" class="adminTab">
|
<div id="tabUsers" class="adminTab">
|
||||||
<h3>👤 User Management</h3>
|
<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">
|
<div class="card">
|
||||||
<h4>Add New User</h4>
|
<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="newUsername" placeholder="Username" required type="text"/>
|
||||||
<input id="newPassword" placeholder="Password" required type="password"/>
|
<input id="newPassword" placeholder="Password" required type="password"/>
|
||||||
<select id="newRole">
|
<select id="newRole">
|
||||||
<option value="student">🎓 Student</option>
|
<option value="student">🎓 Student</option>
|
||||||
<option value="admin">👨💼 Admin</option>
|
<option value="admin">👨💼 Admin</option>
|
||||||
</select>
|
</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>
|
</form>
|
||||||
<p id="userAddMessage" style="margin-top: 1rem;"></p>
|
<p id="userAddMessage" style="margin-top: 1rem;"></p>
|
||||||
<small id="hashPreview" style="color: var(--text-secondary);"></small>
|
<small id="hashPreview" style="color: var(--text-secondary);"></small>
|
||||||
@@ -129,7 +327,7 @@
|
|||||||
|
|
||||||
<!-- Word Lists Tab -->
|
<!-- Word Lists Tab -->
|
||||||
<div class="adminTab hidden" id="tabWords">
|
<div class="adminTab hidden" id="tabWords">
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
<div class="admin-grid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h4>Select User</h4>
|
<h4>Select User</h4>
|
||||||
<select class="word-user-dropdown" id="wordUserSelect" onchange="loadWordsForSelectedUser()" style="width: 100%;">
|
<select class="word-user-dropdown" id="wordUserSelect" onchange="loadWordsForSelectedUser()" style="width: 100%;">
|
||||||
@@ -481,7 +679,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>🎓 Student Dashboard</h2>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Streak & User Stats Section -->
|
<!-- Streak & User Stats Section -->
|
||||||
@@ -490,29 +688,66 @@
|
|||||||
<div id="userStatsContainer" class="card dashboard-card"></div>
|
<div id="userStatsContainer" class="card dashboard-card"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Theme Selection for Students -->
|
<!-- Quick Start Section -->
|
||||||
<div class="card" style="margin-bottom: 1.5rem;">
|
<div class="quick-start-section">
|
||||||
<h4>🎨 Choose Your Theme</h4>
|
<h3>🚀 Quick Start</h3>
|
||||||
<select id="studentThemeSelect" onchange="changeTheme(this.value)" style="width: 100%; max-width: 300px;">
|
<p>Jump right into your favorite activity:</p>
|
||||||
<option value="default">✨ Modern (Default)</option>
|
<div class="quick-actions-grid">
|
||||||
<option value="playground">🎪 Playground</option>
|
<button onclick="startGame()" class="quick-action-card">
|
||||||
<option value="chalkboard">🎓 Chalkboard</option>
|
<span class="quick-icon">🎮</span>
|
||||||
<option value="galaxy">🌌 Galaxy</option>
|
<span class="quick-title">Spelling Game</span>
|
||||||
<option value="nature">🌿 Nature</option>
|
</button>
|
||||||
</select>
|
<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>
|
||||||
|
|
||||||
<div class="student-controls">
|
<!-- Menu Instructions for Students -->
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; max-width: 900px; margin: 0 auto;">
|
<div class="menu-instructions student-instructions">
|
||||||
<button id="startGameBtn" onclick="startGame()" class="btn-success">
|
<h3>📱 Explore More Features</h3>
|
||||||
🎮 Spelling Game
|
<p>Use the menu button (<strong>☰</strong>) to access:</p>
|
||||||
</button>
|
<div class="instruction-grid">
|
||||||
<button id="startTypingBtn" onclick="startTypingPractice()" class="btn-warning">
|
<div class="instruction-item">
|
||||||
⌨️ Typing Practice
|
<span class="instruction-icon">🎯</span>
|
||||||
</button>
|
<span><strong>Challenges</strong> - Track your goals</span>
|
||||||
<button id="startBibleBtn" onclick="startBibleTyping()" class="btn-primary">
|
</div>
|
||||||
📖 Bible Typing
|
<div class="instruction-item">
|
||||||
</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -569,16 +804,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="word-box" id="wordBox"></div>
|
<div class="word-box" id="wordBox"></div>
|
||||||
<div class="hidden" id="inputSection" style="text-align: center;">
|
<div class="hidden" id="inputSection" style="text-align: center;">
|
||||||
<input id="userInput" placeholder="Type the word you saw..." type="text"
|
<!-- Unified Write Mode Container -->
|
||||||
style="max-width: 400px;
|
<div id="unifiedWriteContainer"></div>
|
||||||
color: black !important;
|
|
||||||
background-color: white !important;
|
<!-- Legacy input (hidden, for compatibility) -->
|
||||||
border: 2px solid #333 !important;
|
<input id="userInput" type="hidden"/>
|
||||||
font-size: 18px !important;
|
|
||||||
padding: 12px !important;
|
<!-- Recognized Text Display -->
|
||||||
visibility: visible !important;
|
<div id="recognizedText" class="recognized-text">Draw to see recognized text here</div>
|
||||||
opacity: 1 !important;
|
|
||||||
text-shadow: none !important;"/>
|
<!-- 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>
|
<button onclick="submitAnswer()" class="btn-primary">Submit Answer</button>
|
||||||
<div id="wordFeedback" style="margin-top: 10px; font-weight: bold; min-height: 24px;"></div>
|
<div id="wordFeedback" style="margin-top: 10px; font-weight: bold; min-height: 24px;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -628,8 +875,42 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="typingPrompt">Get ready to type...</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;">
|
<div style="text-align: center; margin: 2rem 0;">
|
||||||
|
<!-- Typing Input -->
|
||||||
<input id="typingInput" type="text" style="max-width: 500px;"/>
|
<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>
|
<button onclick="submitTyping()" class="btn-primary">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
<p id="typingFeedback" style="text-align: center; font-weight: 600;"></p>
|
<p id="typingFeedback" style="text-align: center; font-weight: 600;"></p>
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ async function verifyLogin() {
|
|||||||
function logoutUser() {
|
function logoutUser() {
|
||||||
localStorage.removeItem('loggedInUser');
|
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
|
// Hide all panels
|
||||||
document.getElementById('adminPanel')?.classList.add('hidden');
|
document.getElementById('adminPanel')?.classList.add('hidden');
|
||||||
document.getElementById('studentPanel')?.classList.add('hidden');
|
document.getElementById('studentPanel')?.classList.add('hidden');
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ const bibleVerses = [
|
|||||||
{
|
{
|
||||||
reference: "Matthew 6:33",
|
reference: "Matthew 6:33",
|
||||||
text: "But seek first the kingdom of God and his righteousness, and all these things will be added to you."
|
text: "But seek first the kingdom of God and his righteousness, and all these things will be added to you."
|
||||||
}
|
},
|
||||||
{
|
{
|
||||||
reference: "John 11:36",
|
reference: "John 11:36",
|
||||||
text: "Jesus wept."
|
text: "Jesus wept."
|
||||||
|
|||||||
+105
-19
@@ -7,13 +7,49 @@ let typingWords = [];
|
|||||||
let isSpacedRepetitionMode = false;
|
let isSpacedRepetitionMode = false;
|
||||||
|
|
||||||
// 📝 Check user's answer and move to next word regardless of correctness
|
// 📝 Check user's answer and move to next word regardless of correctness
|
||||||
window.submitAnswer = function() {
|
window.submitAnswer = function(providedAnswer = null) {
|
||||||
const input = document.getElementById('userInput').value.trim();
|
// 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
|
// Record the attempt
|
||||||
results.push({
|
results.push({
|
||||||
word: currentWord,
|
word: currentWord,
|
||||||
attempt: input
|
attempt: input,
|
||||||
|
inputMode: handwritingContainer && handwritingContainer.style.display !== 'none' ? 'handwriting' : 'typing'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show feedback and move to next word regardless of correctness
|
// Show feedback and move to next word regardless of correctness
|
||||||
@@ -21,7 +57,7 @@ window.submitAnswer = function() {
|
|||||||
// Correct answer - add visual feedback
|
// Correct answer - add visual feedback
|
||||||
const inputEl = document.getElementById('userInput');
|
const inputEl = document.getElementById('userInput');
|
||||||
inputEl.style.borderColor = 'var(--success)';
|
inputEl.style.borderColor = 'var(--success)';
|
||||||
inputEl.style.backgroundColor = '#f0fdf4';
|
inputEl.style.backgroundColor = 'var(--bg-secondary)';
|
||||||
|
|
||||||
// Add feedback text if element exists
|
// Add feedback text if element exists
|
||||||
const feedbackEl = document.getElementById('wordFeedback');
|
const feedbackEl = document.getElementById('wordFeedback');
|
||||||
@@ -34,7 +70,7 @@ window.submitAnswer = function() {
|
|||||||
// Incorrect answer - show correct spelling but continue
|
// Incorrect answer - show correct spelling but continue
|
||||||
const inputEl = document.getElementById('userInput');
|
const inputEl = document.getElementById('userInput');
|
||||||
inputEl.style.borderColor = 'var(--error)';
|
inputEl.style.borderColor = 'var(--error)';
|
||||||
inputEl.style.backgroundColor = '#fef2f2';
|
inputEl.style.backgroundColor = 'var(--bg-secondary)';
|
||||||
|
|
||||||
// Add feedback text if element exists
|
// Add feedback text if element exists
|
||||||
const feedbackEl = document.getElementById('wordFeedback');
|
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 = '';
|
let currentSentence = '';
|
||||||
@@ -205,23 +250,64 @@ function showWord() {
|
|||||||
inputSection.classList.remove('hidden');
|
inputSection.classList.remove('hidden');
|
||||||
inputSection.style.display = 'block';
|
inputSection.style.display = 'block';
|
||||||
|
|
||||||
// Reset and focus the input field with enhanced visibility
|
// Reset input (unified write mode or legacy)
|
||||||
const userInput = document.getElementById('userInput');
|
if (window.unifiedWriteMode) {
|
||||||
userInput.value = '';
|
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
|
// Focus appropriate input
|
||||||
userInput.classList.add('visible-input');
|
|
||||||
|
|
||||||
// Focus with a slight delay to ensure UI is ready
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
userInput.focus();
|
if (window.unifiedWriteMode) {
|
||||||
// Try to insert a cursor by setting a selection range
|
// Focus unified write mode
|
||||||
try {
|
window.unifiedWriteMode.focusCanvas();
|
||||||
userInput.setSelectionRange(0, 0);
|
console.log('🎯 Unified write mode focused, ready for input');
|
||||||
} catch (e) {
|
} else {
|
||||||
console.log('Could not set selection range:', e);
|
// 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);
|
}, 50);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,12 +167,12 @@ async function completeChallenge(challengeId, challengeType) {
|
|||||||
// Render streak indicator in student panel
|
// Render streak indicator in student panel
|
||||||
function renderStreakIndicator() {
|
function renderStreakIndicator() {
|
||||||
const streakContainer = document.getElementById('streakContainer');
|
const streakContainer = document.getElementById('streakContainer');
|
||||||
if (!streakContainer || !userProgress) return;
|
if (!streakContainer) return;
|
||||||
|
|
||||||
// Clear previous content
|
// Clear previous content
|
||||||
streakContainer.innerHTML = '';
|
streakContainer.innerHTML = '';
|
||||||
|
|
||||||
const currentStreak = userProgress.streaks.current || 0;
|
const currentStreak = userProgress?.streaks?.current || 0;
|
||||||
|
|
||||||
// Create streak element
|
// Create streak element
|
||||||
const streakEl = document.createElement('div');
|
const streakEl = document.createElement('div');
|
||||||
@@ -191,7 +191,7 @@ function renderStreakIndicator() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="streak-record">
|
<div class="streak-record">
|
||||||
<div class="streak-subtitle">Best</div>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ function renderStreakIndicator() {
|
|||||||
// Render user stats in student panel
|
// Render user stats in student panel
|
||||||
function renderUserStats() {
|
function renderUserStats() {
|
||||||
const statsContainer = document.getElementById('userStatsContainer');
|
const statsContainer = document.getElementById('userStatsContainer');
|
||||||
if (!statsContainer || !userProgress) return;
|
if (!statsContainer) return;
|
||||||
|
|
||||||
// Clear previous content
|
// Clear previous content
|
||||||
statsContainer.innerHTML = '';
|
statsContainer.innerHTML = '';
|
||||||
@@ -213,19 +213,19 @@ function renderUserStats() {
|
|||||||
// Add stat cards
|
// Add stat cards
|
||||||
statsEl.innerHTML = `
|
statsEl.innerHTML = `
|
||||||
<div class="stat-card">
|
<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 class="stat-label">Points</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<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 class="stat-label">Accuracy</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<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 class="stat-label">Sessions</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<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 class="stat-label">Words</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -233,6 +233,21 @@ function renderUserStats() {
|
|||||||
statsContainer.appendChild(statsEl);
|
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
|
// Render challenges in student panel
|
||||||
function renderChallenges() {
|
function renderChallenges() {
|
||||||
const challengesContainer = document.getElementById('challengesContainer');
|
const challengesContainer = document.getElementById('challengesContainer');
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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
@@ -1,37 +1,60 @@
|
|||||||
// 🎭 Role-Based UI Setup
|
// 🎭 Role-Based UI Setup
|
||||||
function setupSession(user) {
|
function setupSession(user) {
|
||||||
document.getElementById('loginPanel').classList.add('hidden');
|
// Safe element hiding/showing with null checks
|
||||||
document.getElementById('adminPanel').classList.add('hidden');
|
const loginPanel = document.getElementById('loginPanel');
|
||||||
document.getElementById('studentPanel').classList.add('hidden');
|
const adminPanel = document.getElementById('adminPanel');
|
||||||
document.getElementById('nav').classList.remove('hidden');
|
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
|
// 🎨 Banner Setup
|
||||||
const roleBanner = document.getElementById('roleBanner');
|
const roleBanner = document.getElementById('roleBanner');
|
||||||
roleBanner.classList.remove('hidden', 'admin', 'student');
|
if (roleBanner) {
|
||||||
roleBanner.classList.add(user.role);
|
roleBanner.classList.remove('hidden', 'admin', 'student');
|
||||||
roleBanner.textContent = user.role === "admin"
|
roleBanner.classList.add(user.role);
|
||||||
? "👨🏫 Admin Access Enabled"
|
roleBanner.textContent = user.role === "admin"
|
||||||
: "🎓 Student Mode Active";
|
? "👨🏫 Admin Access Enabled"
|
||||||
|
: "🎓 Student Mode Active";
|
||||||
|
}
|
||||||
|
|
||||||
// 🎚 Role-Based Navigation
|
// 🎚 Role-Based Navigation
|
||||||
const studentBtn = document.getElementById('studentBtn');
|
const studentBtn = document.getElementById('studentBtn');
|
||||||
const adminBtn = document.getElementById('adminBtn');
|
const adminBtn = document.getElementById('adminBtn');
|
||||||
|
|
||||||
if (user.role === "admin") {
|
if (user.role === "admin") {
|
||||||
studentBtn.classList.remove('hidden');
|
if (studentBtn) studentBtn.classList.remove('hidden');
|
||||||
adminBtn.classList.remove('hidden');
|
if (adminBtn) adminBtn.classList.remove('hidden');
|
||||||
showAdmin();
|
showAdmin();
|
||||||
} else if (user.role === "student") {
|
} else if (user.role === "student") {
|
||||||
studentBtn.classList.add('hidden');
|
if (studentBtn) studentBtn.classList.add('hidden');
|
||||||
adminBtn.classList.add('hidden');
|
if (adminBtn) adminBtn.classList.add('hidden');
|
||||||
showStudent();
|
showStudent();
|
||||||
} else {
|
} else {
|
||||||
alert("Unknown role. Logging out for safety.");
|
alert("Unknown role. Logging out for safety.");
|
||||||
logoutUser();
|
logoutUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update enhanced navigation menu
|
||||||
|
try {
|
||||||
|
if (typeof updateNavigationForUser === 'function') {
|
||||||
|
updateNavigationForUser();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Enhanced navigation update failed:', error);
|
||||||
|
}
|
||||||
|
|
||||||
showToast(`Welcome, ${user.role}!`);
|
showToast(`Welcome, ${user.role}!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +100,20 @@ function showStudent() {
|
|||||||
if (studentThemeSelect) {
|
if (studentThemeSelect) {
|
||||||
studentThemeSelect.value = savedTheme;
|
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) {
|
if (adminPanel) {
|
||||||
adminPanel.classList.remove("hidden");
|
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') {
|
if (typeof window.loadUsers === 'function') {
|
||||||
window.loadUsers().then(() => {
|
window.loadUsers().then(() => {
|
||||||
console.log("✅ Users loaded from server");
|
console.log("✅ Users loaded from server");
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
@@ -35,6 +35,9 @@ body {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Container and layout */
|
/* Container and layout */
|
||||||
@@ -46,7 +49,7 @@ body {
|
|||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3rem;
|
font-size: clamp(2.5rem, 6vw, 4rem);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
@@ -74,27 +77,36 @@ h3 {
|
|||||||
input, textarea, select {
|
input, textarea, select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
padding: 0.75rem 1rem;
|
padding: 1rem 1.25rem;
|
||||||
margin: 0.5rem;
|
margin: 0.75rem;
|
||||||
border: 2px solid var(--border);
|
border: 2px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
font-size: 1rem;
|
font-size: 1.125rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transition: var(--transition);
|
transition: var(--transition);
|
||||||
|
min-height: 48px; /* Tablet-friendly touch target */
|
||||||
|
touch-action: manipulation; /* Prevent zoom on focus */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Special styling for game input */
|
/* Special styling for game input */
|
||||||
#userInput {
|
#userInput {
|
||||||
font-size: 1.2rem;
|
font-size: 1.5rem;
|
||||||
max-width: 400px;
|
max-width: 500px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: white !important;
|
background-color: white !important;
|
||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
caret-color: var(--primary);
|
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 {
|
input:focus, textarea:focus, select:focus {
|
||||||
@@ -113,14 +125,15 @@ textarea {
|
|||||||
button {
|
button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 1rem 2rem;
|
||||||
margin: 0.25rem;
|
margin: 0.5rem;
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius);
|
border-radius: 12px;
|
||||||
font-size: 1rem;
|
font-size: 1.125rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -128,7 +141,10 @@ button {
|
|||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
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 {
|
button:hover {
|
||||||
@@ -460,16 +476,23 @@ button:disabled {
|
|||||||
|
|
||||||
/* Game elements */
|
/* Game elements */
|
||||||
#wordBox, .word-box, #typingPrompt {
|
#wordBox, .word-box, #typingPrompt {
|
||||||
font-size: 3rem;
|
font-size: clamp(2.5rem, 8vw, 4rem);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 3rem 2rem;
|
padding: 3rem 2rem;
|
||||||
margin: 2rem 0;
|
margin: 2rem auto;
|
||||||
|
max-width: 700px;
|
||||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--secondary) 100%);
|
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--secondary) 100%);
|
||||||
border: 2px solid var(--border);
|
border: 3px solid var(--primary);
|
||||||
border-radius: var(--radius);
|
border-radius: 20px;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
box-shadow: var(--shadow-lg);
|
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 */
|
/* Word feedback styles */
|
||||||
@@ -578,11 +601,47 @@ button:disabled {
|
|||||||
/* Student controls specific styling */
|
/* Student controls specific styling */
|
||||||
.student-controls button {
|
.student-controls button {
|
||||||
min-height: 60px !important;
|
min-height: 60px !important;
|
||||||
font-size: 1.1rem !important;
|
font-size: 1.25rem !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
text-shadow: none !important;
|
text-shadow: none !important;
|
||||||
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !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 {
|
.student-controls .btn-success {
|
||||||
@@ -688,3 +747,112 @@ button[id*="debug"],
|
|||||||
[data-debug="true"] {
|
[data-debug="true"] {
|
||||||
display: none !important;
|
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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>Tablet & Handwriting Test</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: #3b82f6;
|
||||||
|
--primary-dark: #2563eb;
|
||||||
|
--secondary: #f1f5f9;
|
||||||
|
--success: #10b981;
|
||||||
|
--error: #ef4444;
|
||||||
|
--text-primary: #1e293b;
|
||||||
|
--text-secondary: #64748b;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--bg-primary: #ffffff;
|
||||||
|
--bg-secondary: #f8fafc;
|
||||||
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-section {
|
||||||
|
margin: 2rem 0;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-section h2 {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button styles */
|
||||||
|
button {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 50px;
|
||||||
|
min-width: 120px;
|
||||||
|
margin: 0.5rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover, button:active {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input mode toggle */
|
||||||
|
.input-mode-toggle {
|
||||||
|
display: flex;
|
||||||
|
background: var(--secondary);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
max-width: 350px;
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-mode-toggle button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
min-height: 50px;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-mode-toggle button.active {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--primary);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text input */
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
background: white;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
min-height: 60px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border: 3px solid var(--primary);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
display: block;
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Canvas styles */
|
||||||
|
#handwritingCanvas {
|
||||||
|
border: 3px solid var(--primary);
|
||||||
|
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;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-controls button {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
background: var(--error) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognize-btn {
|
||||||
|
background: var(--success) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognition-status {
|
||||||
|
margin: 1rem auto;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognition-status.success {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: var(--success);
|
||||||
|
border: 2px solid var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognition-status.error {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: var(--error);
|
||||||
|
border: 2px solid var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognized-text {
|
||||||
|
background: var(--secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem auto;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
min-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognized-text.has-content {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handwriting-container {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin: 2rem auto;
|
||||||
|
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
max-width: 700px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>📱 Tablet & Handwriting Test</h1>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2>🎮 Spelling Game Interface</h2>
|
||||||
|
|
||||||
|
<!-- Word Display -->
|
||||||
|
<div style="font-size: 3rem; font-weight: 700; text-align: center; padding: 2rem; margin: 2rem auto; max-width: 600px; background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%); 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;">
|
||||||
|
EXAMPLE
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Mode Toggle -->
|
||||||
|
<div class="input-mode-toggle">
|
||||||
|
<button class="active" data-mode="typing" onclick="toggleMode('typing')">
|
||||||
|
⌨️ Type
|
||||||
|
</button>
|
||||||
|
<button data-mode="handwriting" onclick="toggleMode('handwriting')">
|
||||||
|
✏️ Write
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Typing Input -->
|
||||||
|
<input type="text" id="typingInput" placeholder="Type the word you saw..." style="display: block;">
|
||||||
|
|
||||||
|
<!-- Handwriting Container -->
|
||||||
|
<div id="handwritingContainer" class="handwriting-container" style="display: none;">
|
||||||
|
<h3>✏️ Write the word you saw:</h3>
|
||||||
|
|
||||||
|
<!-- Canvas -->
|
||||||
|
<canvas id="handwritingCanvas" width="600" height="250" style="width: 100%; max-width: 600px; height: 250px;"></canvas>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="canvas-controls">
|
||||||
|
<button onclick="clearCanvas()" class="clear-btn">🗑️ Clear</button>
|
||||||
|
<button onclick="recognizeText()" class="recognize-btn">🔍 Recognize</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div id="recognitionStatus" class="recognition-status" style="display: none;"></div>
|
||||||
|
|
||||||
|
<!-- Recognized Text -->
|
||||||
|
<div id="recognizedText" class="recognized-text">Draw to see recognized text here</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onclick="submitAnswer()" style="background: var(--success); font-size: 1.25rem; padding: 1.25rem 2.5rem; margin-top: 1rem;">Submit Answer</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2>📊 Test Results</h2>
|
||||||
|
<div id="testResults" style="padding: 1rem; background: var(--secondary); border-radius: 8px; font-family: monospace;">
|
||||||
|
Ready for testing...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Simple handwriting canvas implementation
|
||||||
|
let canvas, ctx, isDrawing = false, strokes = [];
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
canvas = document.getElementById('handwritingCanvas');
|
||||||
|
ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Set canvas properties
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
ctx.strokeStyle = '#1e293b';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
|
||||||
|
// Mouse events
|
||||||
|
canvas.addEventListener('mousedown', startDrawing);
|
||||||
|
canvas.addEventListener('mousemove', draw);
|
||||||
|
canvas.addEventListener('mouseup', stopDrawing);
|
||||||
|
|
||||||
|
// Touch events
|
||||||
|
canvas.addEventListener('touchstart', handleTouch);
|
||||||
|
canvas.addEventListener('touchmove', handleTouch);
|
||||||
|
canvas.addEventListener('touchend', stopDrawing);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleTouch(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
|
||||||
|
const touch = e.touches[0] || e.changedTouches[0];
|
||||||
|
const mouseEvent = new MouseEvent(
|
||||||
|
e.type === 'touchstart' ? 'mousedown' :
|
||||||
|
e.type === 'touchmove' ? 'mousemove' : 'mouseup',
|
||||||
|
{
|
||||||
|
clientX: touch.clientX,
|
||||||
|
clientY: touch.clientY
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (e.type === 'touchstart') startDrawing(mouseEvent);
|
||||||
|
else if (e.type === 'touchmove') draw(mouseEvent);
|
||||||
|
else stopDrawing();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrawing(e) {
|
||||||
|
isDrawing = true;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
|
||||||
|
const x = (e.clientX - rect.left) * scaleX;
|
||||||
|
const y = (e.clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(e) {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
|
||||||
|
const x = (e.clientX - rect.left) * scaleX;
|
||||||
|
const y = (e.clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrawing() {
|
||||||
|
isDrawing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCanvas() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
document.getElementById('recognizedText').textContent = 'Draw to see recognized text here';
|
||||||
|
document.getElementById('recognizedText').className = 'recognized-text';
|
||||||
|
updateTestResults('Canvas cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
function recognizeText() {
|
||||||
|
// Simple mock recognition
|
||||||
|
const mockWords = ['EXAMPLE', 'TEST', 'HELLO', 'WORLD', 'TABLET'];
|
||||||
|
const recognized = mockWords[Math.floor(Math.random() * mockWords.length)];
|
||||||
|
|
||||||
|
document.getElementById('recognizedText').textContent = recognized;
|
||||||
|
document.getElementById('recognizedText').className = 'recognized-text has-content';
|
||||||
|
|
||||||
|
updateTestResults(`Recognized: "${recognized}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMode(mode) {
|
||||||
|
const buttons = document.querySelectorAll('.input-mode-toggle button');
|
||||||
|
const typingInput = document.getElementById('typingInput');
|
||||||
|
const handwritingContainer = document.getElementById('handwritingContainer');
|
||||||
|
|
||||||
|
buttons.forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
if (btn.dataset.mode === mode) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mode === 'typing') {
|
||||||
|
typingInput.style.display = 'block';
|
||||||
|
handwritingContainer.style.display = 'none';
|
||||||
|
updateTestResults('Switched to typing mode');
|
||||||
|
} else {
|
||||||
|
typingInput.style.display = 'none';
|
||||||
|
handwritingContainer.style.display = 'block';
|
||||||
|
updateTestResults('Switched to handwriting mode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitAnswer() {
|
||||||
|
const typingInput = document.getElementById('typingInput');
|
||||||
|
const handwritingContainer = document.getElementById('handwritingContainer');
|
||||||
|
const recognizedText = document.getElementById('recognizedText');
|
||||||
|
|
||||||
|
let answer = '';
|
||||||
|
if (handwritingContainer.style.display !== 'none') {
|
||||||
|
answer = recognizedText.textContent;
|
||||||
|
if (answer === 'Draw to see recognized text here') answer = '';
|
||||||
|
} else {
|
||||||
|
answer = typingInput.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTestResults(`Submitted answer: "${answer}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTestResults(message) {
|
||||||
|
const results = document.getElementById('testResults');
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
results.innerHTML += `[${timestamp}] ${message}<br>`;
|
||||||
|
results.scrollTop = results.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial test
|
||||||
|
updateTestResults('Tablet and handwriting test interface loaded');
|
||||||
|
updateTestResults('Try switching between typing and handwriting modes');
|
||||||
|
updateTestResults('Test drawing on the canvas when in handwriting mode');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+13
-9
@@ -1,14 +1,18 @@
|
|||||||
{
|
{
|
||||||
"version": "1.1.0",
|
"version": "1.2.0-tablet",
|
||||||
"name": "spelling-practice-app",
|
"name": "spelling-practice-app",
|
||||||
"description": "A comprehensive educational application for spelling practice",
|
"description": "A comprehensive educational application for spelling practice with tablet handwriting support",
|
||||||
"lastUpdated": "August 13, 2025",
|
"lastUpdated": "October 2, 2025",
|
||||||
"releaseNotes": [
|
"releaseNotes": [
|
||||||
"Fixed analytics graph display issues",
|
"Added native tablet handwriting recognition support",
|
||||||
"Fixed results and sessions tabs data display",
|
"Integrated Web Handwriting API for modern browsers",
|
||||||
"Added robust error handling for file operations",
|
"Added Windows Ink Platform compatibility",
|
||||||
"Improved session data compatibility",
|
"Enhanced pointer events for pressure-sensitive stylus",
|
||||||
"Added API test page for easier debugging",
|
"Added touch events optimization for tablets",
|
||||||
"Enhanced documentation with troubleshooting guide"
|
"Improved UI for tablet-friendly interface",
|
||||||
|
"Added handwriting canvas with enhanced recognition",
|
||||||
|
"Created comprehensive pattern matching for spelling words",
|
||||||
|
"Added tablet detection and feature optimization",
|
||||||
|
"Enhanced recognition accuracy for educational content"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,593 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Native Windows Handwriting Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-container {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-warning {
|
||||||
|
background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%);
|
||||||
|
color: #2d3436;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
background: linear-gradient(135deg, #fd79a8 0%, #e84393 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-container {
|
||||||
|
border: 3px dashed #e0e6ed;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#testCanvas {
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: white;
|
||||||
|
cursor: crosshair;
|
||||||
|
touch-action: none;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%);
|
||||||
|
color: #2d3436;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-box {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 15px 0;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-text {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-info {
|
||||||
|
background: #e9ecef;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-test {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-test h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-results {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="test-container">
|
||||||
|
<h1>🖊️ Windows Native Handwriting Recognition Test</h1>
|
||||||
|
<p>This page tests various Windows handwriting recognition APIs available in Edge and Windows.</p>
|
||||||
|
|
||||||
|
<div id="overallStatus" class="status-bar status-warning">
|
||||||
|
🔍 Checking Windows handwriting support...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-results">
|
||||||
|
<div class="feature-test">
|
||||||
|
<h4>🌐 Web Handwriting API</h4>
|
||||||
|
<div id="webApiStatus">Testing...</div>
|
||||||
|
<div id="webApiDetails" class="api-info"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-test">
|
||||||
|
<h4>🖊️ Windows Ink Platform</h4>
|
||||||
|
<div id="inkApiStatus">Testing...</div>
|
||||||
|
<div id="inkApiDetails" class="api-info"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-test">
|
||||||
|
<h4>👆 Pointer Events API</h4>
|
||||||
|
<div id="pointerApiStatus">Testing...</div>
|
||||||
|
<div id="pointerApiDetails" class="api-info"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-test">
|
||||||
|
<h4>📱 Touch Events API</h4>
|
||||||
|
<div id="touchApiStatus">Testing...</div>
|
||||||
|
<div id="touchApiDetails" class="api-info"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="canvas-container">
|
||||||
|
<h3>✍️ Test Canvas</h3>
|
||||||
|
<p>Draw or write something below to test recognition:</p>
|
||||||
|
<canvas id="testCanvas" width="400" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-success" onclick="recognizeHandwriting()">🔍 Recognize</button>
|
||||||
|
<button class="btn btn-warning" onclick="clearCanvas()">🧹 Clear</button>
|
||||||
|
<button class="btn btn-primary" onclick="testPressure()">💪 Test Pressure</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-box">
|
||||||
|
<div class="result-text" id="recognitionResult">Write something and click Recognize!</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="debugInfo" class="api-info" style="max-height: 200px; overflow-y: auto;">
|
||||||
|
<strong>Debug Log:</strong><br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Global variables
|
||||||
|
let canvas, ctx;
|
||||||
|
let isDrawing = false;
|
||||||
|
let strokes = [];
|
||||||
|
let currentStroke = [];
|
||||||
|
let recognizer = null;
|
||||||
|
|
||||||
|
// Initialize when page loads
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initializeCanvas();
|
||||||
|
testAllAPIs();
|
||||||
|
});
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
const debugInfo = document.getElementById('debugInfo');
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
debugInfo.innerHTML += `[${timestamp}] ${message}<br>`;
|
||||||
|
debugInfo.scrollTop = debugInfo.scrollHeight;
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(elementId, status, message, details = '') {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
const detailsElement = document.getElementById(elementId.replace('Status', 'Details'));
|
||||||
|
|
||||||
|
element.textContent = message;
|
||||||
|
element.className = status;
|
||||||
|
|
||||||
|
if (detailsElement && details) {
|
||||||
|
detailsElement.textContent = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeCanvas() {
|
||||||
|
canvas = document.getElementById('testCanvas');
|
||||||
|
ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.strokeStyle = '#2c3e50';
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
setupCanvasEvents();
|
||||||
|
log('✅ Canvas initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupCanvasEvents() {
|
||||||
|
// Pointer events (best for stylus)
|
||||||
|
if ('PointerEvent' in window) {
|
||||||
|
canvas.addEventListener('pointerdown', startDrawing);
|
||||||
|
canvas.addEventListener('pointermove', draw);
|
||||||
|
canvas.addEventListener('pointerup', stopDrawing);
|
||||||
|
log('📱 Pointer events registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse events (fallback)
|
||||||
|
canvas.addEventListener('mousedown', startDrawing);
|
||||||
|
canvas.addEventListener('mousemove', draw);
|
||||||
|
canvas.addEventListener('mouseup', stopDrawing);
|
||||||
|
|
||||||
|
// Touch events
|
||||||
|
canvas.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
startDrawing(e.touches[0]);
|
||||||
|
});
|
||||||
|
canvas.addEventListener('touchmove', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
draw(e.touches[0]);
|
||||||
|
});
|
||||||
|
canvas.addEventListener('touchend', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
stopDrawing();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrawing(e) {
|
||||||
|
isDrawing = true;
|
||||||
|
currentStroke = [];
|
||||||
|
|
||||||
|
const point = getPoint(e);
|
||||||
|
currentStroke.push(point);
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(point.x, point.y);
|
||||||
|
|
||||||
|
log(`🖊️ Started stroke at (${point.x.toFixed(1)}, ${point.y.toFixed(1)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(e) {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
|
||||||
|
const point = getPoint(e);
|
||||||
|
currentStroke.push(point);
|
||||||
|
|
||||||
|
ctx.lineTo(point.x, point.y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrawing() {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
isDrawing = false;
|
||||||
|
|
||||||
|
if (currentStroke.length > 0) {
|
||||||
|
strokes.push([...currentStroke]);
|
||||||
|
log(`✅ Stroke completed: ${currentStroke.length} points`);
|
||||||
|
}
|
||||||
|
currentStroke = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPoint(e) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const point = {
|
||||||
|
x: e.clientX - rect.left,
|
||||||
|
y: e.clientY - rect.top,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add extra data if available
|
||||||
|
if (e.pressure !== undefined) {
|
||||||
|
point.pressure = e.pressure;
|
||||||
|
}
|
||||||
|
if (e.tiltX !== undefined) {
|
||||||
|
point.tiltX = e.tiltX;
|
||||||
|
}
|
||||||
|
if (e.tiltY !== undefined) {
|
||||||
|
point.tiltY = e.tiltY;
|
||||||
|
}
|
||||||
|
if (e.force !== undefined) {
|
||||||
|
point.force = e.force;
|
||||||
|
}
|
||||||
|
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAllAPIs() {
|
||||||
|
log('🔍 Starting comprehensive API testing...');
|
||||||
|
|
||||||
|
// Test Web Handwriting API
|
||||||
|
await testWebHandwritingAPI();
|
||||||
|
|
||||||
|
// Test Windows Ink API
|
||||||
|
testWindowsInkAPI();
|
||||||
|
|
||||||
|
// Test Pointer Events
|
||||||
|
testPointerEventsAPI();
|
||||||
|
|
||||||
|
// Test Touch Events
|
||||||
|
testTouchEventsAPI();
|
||||||
|
|
||||||
|
// Update overall status
|
||||||
|
updateOverallStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testWebHandwritingAPI() {
|
||||||
|
try {
|
||||||
|
if ('createHandwritingRecognizer' in navigator) {
|
||||||
|
log('🌐 Web Handwriting API detected!');
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
languages: ['en'],
|
||||||
|
alternatives: 3,
|
||||||
|
inputType: 'text'
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryResult = await navigator.queryHandwritingRecognizer(query);
|
||||||
|
|
||||||
|
if (queryResult) {
|
||||||
|
recognizer = await navigator.createHandwritingRecognizer(query);
|
||||||
|
updateStatus('webApiStatus', 'status-success', '✅ Available and Ready');
|
||||||
|
updateStatus('webApiDetails', '', '', 'Web Handwriting API successfully initialized');
|
||||||
|
log('✅ Web Handwriting recognizer created successfully');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
updateStatus('webApiStatus', 'status-warning', '⚠️ Query Failed');
|
||||||
|
updateStatus('webApiDetails', '', '', 'API exists but query returned false');
|
||||||
|
log('⚠️ Web Handwriting API query returned false');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateStatus('webApiStatus', 'status-error', '❌ Not Available');
|
||||||
|
updateStatus('webApiDetails', '', '', 'navigator.createHandwritingRecognizer not found');
|
||||||
|
log('❌ Web Handwriting API not available');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateStatus('webApiStatus', 'status-error', '❌ Error');
|
||||||
|
updateStatus('webApiDetails', '', '', `Error: ${error.message}`);
|
||||||
|
log(`❌ Web Handwriting API error: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWindowsInkAPI() {
|
||||||
|
try {
|
||||||
|
if (typeof Windows !== 'undefined' && Windows.UI && Windows.UI.Input && Windows.UI.Input.Inking) {
|
||||||
|
updateStatus('inkApiStatus', 'status-success', '✅ Available');
|
||||||
|
updateStatus('inkApiDetails', '', '', 'Windows.UI.Input.Inking namespace detected');
|
||||||
|
log('✅ Windows Ink API available');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
updateStatus('inkApiStatus', 'status-warning', '⚠️ Not Available');
|
||||||
|
updateStatus('inkApiDetails', '', '', 'Windows Runtime APIs not accessible (requires UWP context)');
|
||||||
|
log('⚠️ Windows Ink API not available (not in UWP context)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateStatus('inkApiStatus', 'status-error', '❌ Error');
|
||||||
|
updateStatus('inkApiDetails', '', '', `Error: ${error.message}`);
|
||||||
|
log(`❌ Windows Ink API error: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPointerEventsAPI() {
|
||||||
|
if ('PointerEvent' in window) {
|
||||||
|
updateStatus('pointerApiStatus', 'status-success', '✅ Available');
|
||||||
|
updateStatus('pointerApiDetails', '', '', 'Pointer Events Level 2 support detected');
|
||||||
|
log('✅ Pointer Events API available');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
updateStatus('pointerApiStatus', 'status-error', '❌ Not Available');
|
||||||
|
updateStatus('pointerApiDetails', '', '', 'PointerEvent constructor not found');
|
||||||
|
log('❌ Pointer Events API not available');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testTouchEventsAPI() {
|
||||||
|
if ('TouchEvent' in window || 'ontouchstart' in window) {
|
||||||
|
updateStatus('touchApiStatus', 'status-success', '✅ Available');
|
||||||
|
updateStatus('touchApiDetails', '', '', 'Touch Events support detected');
|
||||||
|
log('✅ Touch Events API available');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
updateStatus('touchApiStatus', 'status-warning', '⚠️ Not Available');
|
||||||
|
updateStatus('touchApiDetails', '', '', 'No touch event support (desktop only?)');
|
||||||
|
log('⚠️ Touch Events API not available');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOverallStatus() {
|
||||||
|
const overallStatus = document.getElementById('overallStatus');
|
||||||
|
|
||||||
|
if (recognizer) {
|
||||||
|
overallStatus.className = 'status-bar status-success';
|
||||||
|
overallStatus.textContent = '✅ Native handwriting recognition is available!';
|
||||||
|
log('🎉 Native handwriting recognition ready for use');
|
||||||
|
} else {
|
||||||
|
overallStatus.className = 'status-bar status-warning';
|
||||||
|
overallStatus.textContent = '⚠️ Native handwriting not available, using enhanced fallback';
|
||||||
|
log('⚠️ Falling back to enhanced pattern recognition');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recognizeHandwriting() {
|
||||||
|
const resultDiv = document.getElementById('recognitionResult');
|
||||||
|
|
||||||
|
if (strokes.length === 0) {
|
||||||
|
resultDiv.textContent = 'Please draw something first!';
|
||||||
|
log('⚠️ No strokes to recognize');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultDiv.textContent = 'Recognizing...';
|
||||||
|
log(`🔍 Starting recognition of ${strokes.length} strokes`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try Web Handwriting API first
|
||||||
|
if (recognizer && recognizer.startDrawing) {
|
||||||
|
const result = await recognizeWithWebAPI();
|
||||||
|
if (result) {
|
||||||
|
resultDiv.textContent = `"${result}"`;
|
||||||
|
log(`✅ Web API recognition: "${result}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to simple pattern recognition
|
||||||
|
const fallbackResult = recognizeWithPatterns();
|
||||||
|
resultDiv.textContent = fallbackResult ? `"${fallbackResult}"` : 'Could not recognize';
|
||||||
|
log(`🔄 Fallback recognition: "${fallbackResult || 'failed'}"`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
resultDiv.textContent = `Error: ${error.message}`;
|
||||||
|
log(`❌ Recognition error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recognizeWithWebAPI() {
|
||||||
|
try {
|
||||||
|
const drawing = recognizer.startDrawing();
|
||||||
|
|
||||||
|
for (const stroke of strokes) {
|
||||||
|
const apiStroke = drawing.addStroke();
|
||||||
|
for (const point of stroke) {
|
||||||
|
apiStroke.addPoint({
|
||||||
|
x: point.x,
|
||||||
|
y: point.y,
|
||||||
|
t: point.timestamp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const predictions = await recognizer.getPrediction();
|
||||||
|
|
||||||
|
if (predictions && predictions.length > 0) {
|
||||||
|
return predictions[0].text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
log(`❌ Web API recognition failed: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recognizeWithPatterns() {
|
||||||
|
// Simple pattern recognition based on stroke count and shape
|
||||||
|
const strokeCount = strokes.length;
|
||||||
|
|
||||||
|
if (strokeCount === 1) {
|
||||||
|
return 'O'; // Single stroke likely a circle
|
||||||
|
} else if (strokeCount === 2) {
|
||||||
|
return 'H'; // Two strokes likely H or similar
|
||||||
|
} else if (strokeCount === 3) {
|
||||||
|
return 'Boy'; // Three strokes, guess Boy
|
||||||
|
} else if (strokeCount >= 4) {
|
||||||
|
return 'Love'; // Four+ strokes, guess Love
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCanvas() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
strokes = [];
|
||||||
|
currentStroke = [];
|
||||||
|
document.getElementById('recognitionResult').textContent = 'Write something and click Recognize!';
|
||||||
|
log('🧹 Canvas cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPressure() {
|
||||||
|
log('💪 Testing pressure sensitivity...');
|
||||||
|
const resultDiv = document.getElementById('recognitionResult');
|
||||||
|
|
||||||
|
if (strokes.length === 0) {
|
||||||
|
resultDiv.textContent = 'Draw something to test pressure!';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pressureCount = 0;
|
||||||
|
let totalPressure = 0;
|
||||||
|
|
||||||
|
for (const stroke of strokes) {
|
||||||
|
for (const point of stroke) {
|
||||||
|
if (point.pressure !== undefined) {
|
||||||
|
pressureCount++;
|
||||||
|
totalPressure += point.pressure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pressureCount > 0) {
|
||||||
|
const avgPressure = (totalPressure / pressureCount).toFixed(3);
|
||||||
|
resultDiv.textContent = `Pressure detected! Avg: ${avgPressure}`;
|
||||||
|
log(`✅ Pressure data: ${pressureCount} points, average ${avgPressure}`);
|
||||||
|
} else {
|
||||||
|
resultDiv.textContent = 'No pressure data detected';
|
||||||
|
log('⚠️ No pressure sensitivity detected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable experimental features message
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!recognizer) {
|
||||||
|
log('💡 To enable Web Handwriting API in Edge:');
|
||||||
|
log(' 1. Go to edge://flags/');
|
||||||
|
log(' 2. Search for "Experimental Web Platform features"');
|
||||||
|
log(' 3. Enable it and restart Edge');
|
||||||
|
log(' 4. Or try Chrome Canary with --enable-experimental-web-platform-features');
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in new issue
Block a user