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,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;
|
||||
}
|
||||
Reference in new issue
Block a user