sysrot-hub
Version:
CLI de nueva generación para proyectos Next.js 14+ con IA multi-modelo, Web3 integration, internacionalización completa y roadmap realista 2025-2026
515 lines (441 loc) • 15.2 kB
text/typescript
// Touch Gestures and Mobile Optimization Library
export interface TouchPoint {
x: number;
y: number;
timestamp: number;
}
export interface SwipeConfig {
minDistance: number;
maxTime: number;
threshold: number;
}
export interface GestureCallbacks {
onSwipeLeft?: () => void;
onSwipeRight?: () => void;
onSwipeUp?: () => void;
onSwipeDown?: () => void;
onPinchZoom?: (scale: number) => void;
onTap?: (point: TouchPoint) => void;
onDoubleTap?: (point: TouchPoint) => void;
onLongPress?: (point: TouchPoint) => void;
}
class TouchGestureManager {
private startTouches: TouchPoint[] = [];
private lastTap: TouchPoint | null = null;
private longPressTimer: ReturnType<typeof setTimeout> | null = null;
private element: HTMLElement;
private callbacks: GestureCallbacks;
private config: SwipeConfig;
constructor(
element: HTMLElement,
callbacks: GestureCallbacks,
config: Partial<SwipeConfig> = {}
) {
this.element = element;
this.callbacks = callbacks;
this.config = {
minDistance: 30,
maxTime: 1000,
threshold: 10,
...config
};
this.attachListeners();
}
private attachListeners() {
this.element.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: false });
this.element.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
this.element.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: false });
this.element.addEventListener('touchcancel', this.handleTouchCancel.bind(this));
}
private handleTouchStart(event: TouchEvent) {
const now = Date.now();
this.startTouches = Array.from(event.touches).map(touch => ({
x: touch.clientX,
y: touch.clientY,
timestamp: now
}));
// Handle long press
if (event.touches.length === 1) {
const touch = this.startTouches[0];
this.longPressTimer = setTimeout(() => {
this.callbacks.onLongPress?.(touch);
this.triggerHapticFeedback('medium');
}, 500);
}
// Prevent scrolling during gestures if needed
if (event.touches.length > 1) {
event.preventDefault();
}
}
private handleTouchMove(event: TouchEvent) {
// Cancel long press on movement
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
// Handle pinch zoom
if (event.touches.length === 2 && this.startTouches.length === 2) {
this.handlePinchZoom(event);
}
// Prevent default for multi-touch
if (event.touches.length > 1) {
event.preventDefault();
}
}
private handleTouchEnd(event: TouchEvent) {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
const now = Date.now();
const endTouch = event.changedTouches[0];
const endPoint: TouchPoint = {
x: endTouch.clientX,
y: endTouch.clientY,
timestamp: now
};
// Handle single touch gestures
if (this.startTouches.length === 1) {
const startTouch = this.startTouches[0];
const duration = now - startTouch.timestamp;
const distance = this.calculateDistance(startTouch, endPoint);
if (duration < this.config.maxTime) {
if (distance < this.config.threshold) {
// Tap or double tap
this.handleTap(endPoint);
} else if (distance > this.config.minDistance) {
// Swipe
this.handleSwipe(startTouch, endPoint);
}
}
}
this.startTouches = [];
}
private handleTouchCancel() {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
this.startTouches = [];
}
private handleTap(point: TouchPoint) {
const now = Date.now();
if (this.lastTap && now - this.lastTap.timestamp < 300) {
// Double tap
this.callbacks.onDoubleTap?.(point);
this.triggerHapticFeedback('light');
this.lastTap = null;
} else {
// Single tap
this.lastTap = point;
setTimeout(() => {
if (this.lastTap && this.lastTap.timestamp === point.timestamp) {
this.callbacks.onTap?.(point);
this.triggerHapticFeedback('light');
this.lastTap = null;
}
}, 300);
}
}
private handleSwipe(start: TouchPoint, end: TouchPoint) {
const deltaX = end.x - start.x;
const deltaY = end.y - start.y;
const absDeltaX = Math.abs(deltaX);
const absDeltaY = Math.abs(deltaY);
if (absDeltaX > absDeltaY) {
// Horizontal swipe
if (deltaX > 0) {
this.callbacks.onSwipeRight?.();
} else {
this.callbacks.onSwipeLeft?.();
}
} else {
// Vertical swipe
if (deltaY > 0) {
this.callbacks.onSwipeDown?.();
} else {
this.callbacks.onSwipeUp?.();
}
}
this.triggerHapticFeedback('medium');
}
private handlePinchZoom(event: TouchEvent) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const currentDistance = this.calculateDistance(
{ x: touch1.clientX, y: touch1.clientY, timestamp: 0 },
{ x: touch2.clientX, y: touch2.clientY, timestamp: 0 }
);
const startDistance = this.calculateDistance(
this.startTouches[0],
this.startTouches[1]
);
const scale = currentDistance / startDistance;
this.callbacks.onPinchZoom?.(scale);
}
private calculateDistance(point1: TouchPoint, point2: TouchPoint): number {
const deltaX = point2.x - point1.x;
const deltaY = point2.y - point1.y;
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}
private triggerHapticFeedback(type: 'light' | 'medium' | 'heavy') {
if ('vibrate' in navigator) {
const patterns = {
light: [10],
medium: [20],
heavy: [30]
};
navigator.vibrate(patterns[type]);
}
}
public destroy() {
this.element.removeEventListener('touchstart', this.handleTouchStart.bind(this));
this.element.removeEventListener('touchmove', this.handleTouchMove.bind(this));
this.element.removeEventListener('touchend', this.handleTouchEnd.bind(this));
this.element.removeEventListener('touchcancel', this.handleTouchCancel.bind(this));
}
}
// Pull to refresh functionality
export class PullToRefresh {
private element: HTMLElement;
private onRefresh: () => Promise<void>;
private threshold: number;
private startY: number = 0;
private currentY: number = 0;
private isRefreshing: boolean = false;
private isEnabled: boolean = true;
constructor(
element: HTMLElement,
onRefresh: () => Promise<void>,
threshold: number = 80
) {
this.element = element;
this.onRefresh = onRefresh;
this.threshold = threshold;
this.attachListeners();
}
private attachListeners() {
this.element.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: false });
this.element.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
this.element.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: false });
}
private handleTouchStart(event: TouchEvent) {
if (!this.isEnabled || this.isRefreshing) return;
this.startY = event.touches[0].clientY;
this.currentY = this.startY;
}
private handleTouchMove(event: TouchEvent) {
if (!this.isEnabled || this.isRefreshing) return;
this.currentY = event.touches[0].clientY;
const pullDistance = this.currentY - this.startY;
// Only allow pull down when at top of page
if (pullDistance > 0 && window.scrollY === 0) {
event.preventDefault();
// Visual feedback for pull distance
const opacity = Math.min(pullDistance / this.threshold, 1);
this.updateRefreshIndicator(opacity, pullDistance >= this.threshold);
}
}
private handleTouchEnd() {
if (!this.isEnabled || this.isRefreshing) return;
const pullDistance = this.currentY - this.startY;
if (pullDistance >= this.threshold && window.scrollY === 0) {
this.triggerRefresh();
} else {
this.resetRefreshIndicator();
}
}
private async triggerRefresh() {
this.isRefreshing = true;
this.showRefreshingState();
try {
await this.onRefresh();
navigator.vibrate?.([10, 50, 10]);
} catch (error) {
console.error('Refresh failed:', error);
} finally {
this.isRefreshing = false;
this.resetRefreshIndicator();
}
}
private updateRefreshIndicator(opacity: number, isReady: boolean) {
// Update visual indicator based on pull distance
this.element.style.setProperty('--pull-opacity', opacity.toString());
this.element.style.setProperty('--pull-ready', isReady ? '1' : '0');
}
private showRefreshingState() {
this.element.style.setProperty('--pull-refreshing', '1');
}
private resetRefreshIndicator() {
this.element.style.setProperty('--pull-opacity', '0');
this.element.style.setProperty('--pull-ready', '0');
this.element.style.setProperty('--pull-refreshing', '0');
}
public setEnabled(enabled: boolean) {
this.isEnabled = enabled;
}
public destroy() {
this.element.removeEventListener('touchstart', this.handleTouchStart.bind(this));
this.element.removeEventListener('touchmove', this.handleTouchMove.bind(this));
this.element.removeEventListener('touchend', this.handleTouchEnd.bind(this));
}
}
// Mobile performance utilities
export class MobilePerformance {
static optimizeScrolling(element: HTMLElement) {
// Enable momentum scrolling on iOS
(element.style as any).webkitOverflowScrolling = 'touch';
(element.style as any).overflowScrolling = 'touch';
}
static enableSmoothTransitions(element: HTMLElement) {
element.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
element.style.willChange = 'transform';
}
static preventZoom(element: HTMLElement) {
element.addEventListener('touchstart', (event) => {
if (event.touches.length > 1) {
event.preventDefault();
}
});
let lastTouchEnd = 0;
element.addEventListener('touchend', (event) => {
const now = Date.now();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}, false);
}
static optimizeImages() {
// Lazy load images that are not in viewport
const images = document.querySelectorAll('img[loading="lazy"]');
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
imageObserver.unobserve(img);
}
}
});
});
images.forEach(img => imageObserver.observe(img));
}
}
static monitorBatteryUsage(): Promise<any> {
return new Promise((resolve, reject) => {
if ('getBattery' in navigator) {
(navigator as any).getBattery().then((battery: any) => {
const batteryInfo = {
level: battery.level,
charging: battery.charging,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime
};
// Optimize performance based on battery level
if (battery.level < 0.2 && !battery.charging) {
// Low battery mode - reduce animations
document.body.classList.add('battery-save-mode');
}
resolve(batteryInfo);
});
} else {
reject(new Error('Battery API not supported'));
}
});
}
static enableOfflineMode() {
// Register service worker for offline functionality
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then((registration) => {
console.log('Service Worker registered:', registration);
}).catch((error) => {
console.error('Service Worker registration failed:', error);
});
}
// Handle online/offline events
window.addEventListener('online', () => {
document.body.classList.remove('offline-mode');
console.log('App is online');
});
window.addEventListener('offline', () => {
document.body.classList.add('offline-mode');
console.log('App is offline');
});
}
}
// Export utility functions
export function createTouchGestureManager(
element: HTMLElement,
callbacks: GestureCallbacks,
config?: Partial<SwipeConfig>
): TouchGestureManager {
return new TouchGestureManager(element, callbacks, config);
}
export function createPullToRefresh(
element: HTMLElement,
onRefresh: () => Promise<void>,
threshold?: number
): PullToRefresh {
return new PullToRefresh(element, onRefresh, threshold);
}
// PWA Installation utilities
export class PWAInstaller {
private deferredPrompt: any = null;
constructor() {
this.setupInstallPrompt();
}
private setupInstallPrompt() {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
this.deferredPrompt = e;
this.showInstallButton();
});
window.addEventListener('appinstalled', () => {
console.log('PWA was installed');
this.hideInstallButton();
});
}
public async promptInstall(): Promise<boolean> {
if (!this.deferredPrompt) {
return false;
}
this.deferredPrompt.prompt();
const { outcome } = await this.deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('User accepted PWA install');
return true;
} else {
console.log('User dismissed PWA install');
return false;
}
}
private showInstallButton() {
const installButton = document.getElementById('pwa-install-button');
if (installButton) {
installButton.style.display = 'block';
}
}
private hideInstallButton() {
const installButton = document.getElementById('pwa-install-button');
if (installButton) {
installButton.style.display = 'none';
}
}
public isInstalled(): boolean {
return window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone ||
document.referrer.includes('android-app://');
}
}
export default {
TouchGestureManager,
PullToRefresh,
MobilePerformance,
PWAInstaller,
createTouchGestureManager,
createPullToRefresh
};