UNPKG

@datametria/vue-components

Version:

DATAMETRIA Vue.js 3 Component Library with Multi-Brand Theming - 51 components + 10 composables with theming support, WCAG 2.2 AA, dark mode, responsive system

440 lines (364 loc) 10.9 kB
import { ref, computed } from 'vue' type HapticType = 'light' | 'medium' | 'heavy' | 'selection' | 'impact' | 'notification' type NotificationType = 'success' | 'warning' | 'error' interface HapticOptions { enabled?: boolean fallbackToVisual?: boolean visualDuration?: number } /** * Composable para feedback háptico e visual * Suporta dispositivos iOS/Android com fallback visual */ export function useHapticFeedback(options: HapticOptions = {}) { const { enabled = true, fallbackToVisual = true, visualDuration = 150 } = options const isSupported = ref(false) const isEnabled = ref(enabled) const activeVisualFeedback = ref<string | null>(null) // Detectar suporte a haptic feedback const detectHapticSupport = () => { // Haptic Feedback API (iOS/Android) if ('hapticFeedback' in navigator && (navigator as any).hapticFeedback) { return true } // Web Vibration API if ('vibrate' in navigator && typeof (navigator as any).vibrate === 'function') { return true } return false } // Executar vibração nativa const executeNativeVibration = (pattern: number | number[]) => { if (!('vibrate' in navigator) || typeof (navigator as any).vibrate !== 'function') return false try { return (navigator as any).vibrate(pattern) } catch (error) { console.warn('Haptic feedback failed:', error) return false } } // Feedback visual como fallback const executeVisualFeedback = (type: HapticType, element?: HTMLElement) => { if (!fallbackToVisual) return const feedbackId = `visual-${Date.now()}` activeVisualFeedback.value = feedbackId // Aplicar efeito visual ao elemento if (element) { const originalTransform = element.style.transform const originalTransition = element.style.transition element.style.transition = 'transform 75ms ease-out' switch (type) { case 'light': element.style.transform = 'scale(0.98)' break case 'medium': element.style.transform = 'scale(0.95)' break case 'heavy': element.style.transform = 'scale(0.92)' break case 'selection': element.style.transform = 'scale(1.02)' break default: element.style.transform = 'scale(0.97)' } setTimeout(() => { element.style.transform = originalTransform setTimeout(() => { element.style.transition = originalTransition if (activeVisualFeedback.value === feedbackId) { activeVisualFeedback.value = null } }, 75) }, 75) } // Limpar feedback visual após duração setTimeout(() => { if (activeVisualFeedback.value === feedbackId) { activeVisualFeedback.value = null } }, visualDuration) } // Feedback leve (toque suave) const light = (element?: HTMLElement) => { if (!isEnabled.value) return let success = false if (isSupported.value) { success = executeNativeVibration(10) } if (!success && fallbackToVisual) { executeVisualFeedback('light', element) } } // Feedback médio (toque moderado) const medium = (element?: HTMLElement) => { if (!isEnabled.value) return let success = false if (isSupported.value) { success = executeNativeVibration(20) } if (!success && fallbackToVisual) { executeVisualFeedback('medium', element) } } // Feedback pesado (toque forte) const heavy = (element?: HTMLElement) => { if (!isEnabled.value) return let success = false if (isSupported.value) { success = executeNativeVibration(40) } if (!success && fallbackToVisual) { executeVisualFeedback('heavy', element) } } // Feedback de seleção const selection = (element?: HTMLElement) => { if (!isEnabled.value) return let success = false if (isSupported.value) { success = executeNativeVibration(5) } if (!success && fallbackToVisual) { executeVisualFeedback('selection', element) } } // Feedback de impacto const impact = (intensity: 'light' | 'medium' | 'heavy' = 'medium', element?: HTMLElement) => { switch (intensity) { case 'light': light(element) break case 'medium': medium(element) break case 'heavy': heavy(element) break } } // Feedback de notificação const notification = (type: NotificationType, element?: HTMLElement) => { if (!isEnabled.value) return let pattern: number[] switch (type) { case 'success': pattern = [10, 50, 10] break case 'warning': pattern = [20, 100, 20, 100, 20] break case 'error': pattern = [50, 100, 50, 100, 50] break } let success = false if (isSupported.value) { success = executeNativeVibration(pattern) } if (!success && fallbackToVisual) { executeVisualFeedback('notification', element) } } // Padrão customizado const custom = (pattern: number | number[], element?: HTMLElement) => { if (!isEnabled.value) return let success = false if (isSupported.value) { success = executeNativeVibration(pattern) } if (!success && fallbackToVisual) { executeVisualFeedback('medium', element) } } // Habilitar/desabilitar feedback const enable = () => { isEnabled.value = true } const disable = () => { isEnabled.value = false } const toggle = () => { isEnabled.value = !isEnabled.value } // Computed const canVibrate = computed(() => isSupported.value && isEnabled.value) const hasVisualFeedback = computed(() => activeVisualFeedback.value !== null) // Método principal para feedback háptico const triggerHaptic = (type: HapticType | NotificationType = 'light') => { if (!isEnabled.value) return false // Tentar haptic feedback nativo primeiro if ('hapticFeedback' in navigator && (navigator as any).hapticFeedback) { try { return (navigator as any).hapticFeedback.impact(type) } catch (error) { console.warn('Haptic feedback failed:', error) } } // Fallback para vibration API if ('vibrate' in navigator && typeof (navigator as any).vibrate === 'function') { let pattern: number | number[] switch (type) { case 'light': pattern = 10 break case 'medium': pattern = 20 break case 'heavy': pattern = 30 break case 'success': pattern = [10, 50, 10] break case 'warning': pattern = [20, 100, 20] break case 'error': pattern = [50, 100, 50, 100, 50] break default: pattern = 10 } try { return (navigator as any).vibrate(pattern) } catch (error) { console.warn('Vibration failed:', error) return false } } return false } // Injetar CSS para animações visuais const injectVisualCSS = () => { const styleId = 'haptic-keyframes' if (document.getElementById(styleId)) return const style = document.createElement('style') style.id = styleId style.textContent = ` @keyframes haptic-pulse-light { 0% { transform: scale(1); } 50% { transform: scale(0.98); } 100% { transform: scale(1); } } @keyframes haptic-pulse-medium { 0% { transform: scale(1); } 50% { transform: scale(0.95); } 100% { transform: scale(1); } } @keyframes haptic-pulse-heavy { 0% { transform: scale(1); } 50% { transform: scale(0.92); } 100% { transform: scale(1); } } @keyframes haptic-success { 0% { transform: scale(1); filter: brightness(1); } 50% { transform: scale(1.02); filter: brightness(1.1); } 100% { transform: scale(1); filter: brightness(1); } } @keyframes haptic-error { 0% { transform: translateX(0); } 25% { transform: translateX(-2px); } 75% { transform: translateX(2px); } 100% { transform: translateX(0); } } @media (prefers-reduced-motion: reduce) { @keyframes haptic-pulse-light, @keyframes haptic-pulse-medium, @keyframes haptic-pulse-heavy, @keyframes haptic-success, @keyframes haptic-error { 0%, 100% { transform: none; filter: none; } } } ` document.head.appendChild(style) } // Feedback visual const triggerVisualFeedback = (element: HTMLElement | null, type: HapticType | NotificationType) => { if (!element) return injectVisualCSS() const originalAnimation = element.style.animation let animation: string let duration: number switch (type) { case 'light': animation = 'haptic-pulse-light 0.15s ease-out' duration = 150 break case 'medium': animation = 'haptic-pulse-medium 0.2s ease-out' duration = 200 break case 'heavy': animation = 'haptic-pulse-heavy 0.25s ease-out' duration = 250 break case 'success': animation = 'haptic-success 0.3s ease-out' duration = 300 break case 'warning': animation = 'haptic-pulse-medium 0.2s ease-out' duration = 200 break case 'error': animation = 'haptic-error 0.4s ease-out' duration = 400 break default: animation = 'haptic-pulse-light 0.15s ease-out' duration = 150 } element.style.animation = animation setTimeout(() => { element.style.animation = originalAnimation }, duration) } // Feedback combinado (háptico + visual) const feedback = (element: HTMLElement | null, type: HapticType | NotificationType) => { const hapticResult = triggerHaptic(type) triggerVisualFeedback(element, type) return hapticResult } // Verificar suporte (método público) const checkSupport = () => { const hasSupport = detectHapticSupport() isSupported.value = hasSupport return hasSupport } // Inicializar detecção de suporte checkSupport() return { // Estado isSupported, isEnabled, canVibrate, hasVisualFeedback, activeVisualFeedback, // Métodos principais (compatíveis com testes) triggerHaptic, triggerVisualFeedback, feedback, checkSupport, // Métodos de feedback específicos light, medium, heavy, selection, impact, notification, custom, // Controles enable, disable, toggle, // Utilitários detectHapticSupport } }