@datametria/vue-components 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +548 -657
- package/dist/index.es.js +2353 -1364
- package/dist/index.umd.js +10 -10
- package/dist/vue-components.css +1 -1
- package/package.json +102 -98
- package/src/components/DatametriaAlert.vue +137 -137
- package/src/components/DatametriaAutocomplete.vue +184 -138
- package/src/components/DatametriaAvatar.vue +177 -33
- package/src/components/DatametriaBadge.vue +98 -98
- package/src/components/DatametriaBreadcrumb.vue +21 -21
- package/src/components/DatametriaButton.vue +177 -165
- package/src/components/DatametriaCard.vue +12 -12
- package/src/components/DatametriaCheckbox.vue +8 -8
- package/src/components/DatametriaChip.vue +145 -149
- package/src/components/DatametriaContainer.vue +4 -4
- package/src/components/DatametriaDatePicker.vue +686 -68
- package/src/components/DatametriaDivider.vue +13 -13
- package/src/components/DatametriaFileUpload.vue +272 -140
- package/src/components/DatametriaGrid.vue +3 -3
- package/src/components/DatametriaInput.vue +15 -15
- package/src/components/DatametriaMenu.vue +604 -619
- package/src/components/DatametriaModal.vue +16 -16
- package/src/components/DatametriaNavbar.vue +230 -252
- package/src/components/DatametriaPasswordInput.vue +430 -0
- package/src/components/DatametriaProgress.vue +18 -18
- package/src/components/DatametriaRadio.vue +20 -20
- package/src/components/DatametriaSelect.vue +15 -15
- package/src/components/DatametriaSkeleton.vue +243 -239
- package/src/components/DatametriaSlider.vue +395 -407
- package/src/components/DatametriaSortableTable.vue +585 -0
- package/src/components/DatametriaSpinner.vue +7 -7
- package/src/components/DatametriaSwitch.vue +16 -16
- package/src/components/DatametriaTable.vue +14 -14
- package/src/components/DatametriaTextarea.vue +28 -28
- package/src/components/DatametriaTimePicker.vue +285 -285
- package/src/components/DatametriaToast.vue +176 -176
- package/src/components/DatametriaTooltip.vue +408 -408
- package/src/components/__tests__/DatametriaAlert.test.js +35 -35
- package/src/components/__tests__/DatametriaAlert.test.ts +190 -0
- package/src/components/__tests__/DatametriaAutocomplete.test.ts +180 -0
- package/src/components/__tests__/DatametriaAvatar.test.ts +152 -0
- package/src/components/__tests__/DatametriaBadge.test.js +29 -29
- package/src/components/__tests__/DatametriaBadge.test.ts +167 -0
- package/src/components/__tests__/DatametriaBreadcrumb.test.ts +75 -0
- package/src/components/__tests__/DatametriaButton.test.js +30 -30
- package/src/components/__tests__/DatametriaButton.test.ts +283 -0
- package/src/components/__tests__/DatametriaCard.test.ts +201 -0
- package/src/components/__tests__/DatametriaCheckbox.test.ts +47 -0
- package/src/components/__tests__/DatametriaChip.test.js +38 -38
- package/src/components/__tests__/DatametriaContainer.test.ts +52 -0
- package/src/components/__tests__/DatametriaDatePicker.test.ts +234 -0
- package/src/components/__tests__/DatametriaDivider.test.ts +54 -0
- package/src/components/__tests__/DatametriaFileUpload.test.ts +291 -0
- package/src/components/__tests__/DatametriaGrid.test.ts +31 -0
- package/src/components/__tests__/DatametriaInput.test.ts +72 -0
- package/src/components/__tests__/DatametriaMenu.test.ts +366 -0
- package/src/components/__tests__/DatametriaModal.test.ts +86 -0
- package/src/components/__tests__/DatametriaNavbar.test.js +48 -48
- package/src/components/__tests__/DatametriaNavbar.test.ts +203 -0
- package/src/components/__tests__/DatametriaPasswordInput.test.js +305 -0
- package/src/components/__tests__/DatametriaProgress.test.ts +90 -0
- package/src/components/__tests__/DatametriaRadio.test.ts +77 -0
- package/src/components/__tests__/DatametriaSelect.test.ts +77 -0
- package/src/components/__tests__/DatametriaSlider.test.ts +261 -0
- package/src/components/__tests__/DatametriaSortableTable.test.js +168 -0
- package/src/components/__tests__/DatametriaSpinner.test.ts +156 -0
- package/src/components/__tests__/DatametriaSwitch.test.ts +64 -0
- package/src/components/__tests__/DatametriaTable.test.ts +97 -0
- package/src/components/__tests__/DatametriaTextarea.test.ts +66 -0
- package/src/components/__tests__/DatametriaToast.test.js +48 -48
- package/src/components/__tests__/DatametriaToast.test.ts +99 -0
- package/src/composables/useAccessibilityScale.ts +94 -94
- package/src/composables/useBreakpoints.ts +82 -82
- package/src/composables/useHapticFeedback.ts +439 -439
- package/src/composables/useRipple.ts +218 -218
- package/src/index.ts +68 -61
- package/src/stories/Variants.stories.js +95 -95
- package/src/styles/design-tokens.css +623 -623
- package/src/theme/ThemeProvider.vue +96 -0
- package/src/theme/__tests__/ThemeProvider.test.ts +208 -0
- package/src/theme/__tests__/constants.test.ts +31 -0
- package/src/theme/__tests__/presets.test.ts +166 -0
- package/src/theme/__tests__/tokens.test.ts +155 -0
- package/src/theme/__tests__/types.test.ts +153 -0
- package/src/theme/__tests__/useTheme.test.ts +146 -0
- package/src/theme/constants.ts +14 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/presets/datametria.ts +94 -0
- package/src/theme/presets/default.ts +94 -0
- package/src/theme/presets/index.ts +8 -0
- package/src/theme/tokens/colors.ts +28 -0
- package/src/theme/tokens/index.ts +47 -0
- package/src/theme/tokens/spacing.ts +21 -0
- package/src/theme/tokens/typography.ts +35 -0
- package/src/theme/types.ts +111 -0
- package/src/theme/useTheme.ts +28 -0
- package/src/types/index.ts +19 -0
|
@@ -1,440 +1,440 @@
|
|
|
1
|
-
import { ref, computed } from 'vue'
|
|
2
|
-
|
|
3
|
-
type HapticType = 'light' | 'medium' | 'heavy' | 'selection' | 'impact' | 'notification'
|
|
4
|
-
type NotificationType = 'success' | 'warning' | 'error'
|
|
5
|
-
|
|
6
|
-
interface HapticOptions {
|
|
7
|
-
enabled?: boolean
|
|
8
|
-
fallbackToVisual?: boolean
|
|
9
|
-
visualDuration?: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Composable para feedback háptico e visual
|
|
14
|
-
* Suporta dispositivos iOS/Android com fallback visual
|
|
15
|
-
*/
|
|
16
|
-
export function useHapticFeedback(options: HapticOptions = {}) {
|
|
17
|
-
const {
|
|
18
|
-
enabled = true,
|
|
19
|
-
fallbackToVisual = true,
|
|
20
|
-
visualDuration = 150
|
|
21
|
-
} = options
|
|
22
|
-
|
|
23
|
-
const isSupported = ref(false)
|
|
24
|
-
const isEnabled = ref(enabled)
|
|
25
|
-
const activeVisualFeedback = ref<string | null>(null)
|
|
26
|
-
|
|
27
|
-
// Detectar suporte a haptic feedback
|
|
28
|
-
const detectHapticSupport = () => {
|
|
29
|
-
// Haptic Feedback API (iOS/Android)
|
|
30
|
-
if ('hapticFeedback' in navigator && (navigator as any).hapticFeedback) {
|
|
31
|
-
return true
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Web Vibration API
|
|
35
|
-
if ('vibrate' in navigator && typeof (navigator as any).vibrate === 'function') {
|
|
36
|
-
return true
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return false
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Executar vibração nativa
|
|
43
|
-
const executeNativeVibration = (pattern: number | number[]) => {
|
|
44
|
-
if (!('vibrate' in navigator) || typeof (navigator as any).vibrate !== 'function') return false
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
return (navigator as any).vibrate(pattern)
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.warn('Haptic feedback failed:', error)
|
|
50
|
-
return false
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Feedback visual como fallback
|
|
55
|
-
const executeVisualFeedback = (type: HapticType, element?: HTMLElement) => {
|
|
56
|
-
if (!fallbackToVisual) return
|
|
57
|
-
|
|
58
|
-
const feedbackId = `visual-${Date.now()}`
|
|
59
|
-
activeVisualFeedback.value = feedbackId
|
|
60
|
-
|
|
61
|
-
// Aplicar efeito visual ao elemento
|
|
62
|
-
if (element) {
|
|
63
|
-
const originalTransform = element.style.transform
|
|
64
|
-
const originalTransition = element.style.transition
|
|
65
|
-
|
|
66
|
-
element.style.transition = 'transform 75ms ease-out'
|
|
67
|
-
|
|
68
|
-
switch (type) {
|
|
69
|
-
case 'light':
|
|
70
|
-
element.style.transform = 'scale(0.98)'
|
|
71
|
-
break
|
|
72
|
-
case 'medium':
|
|
73
|
-
element.style.transform = 'scale(0.95)'
|
|
74
|
-
break
|
|
75
|
-
case 'heavy':
|
|
76
|
-
element.style.transform = 'scale(0.92)'
|
|
77
|
-
break
|
|
78
|
-
case 'selection':
|
|
79
|
-
element.style.transform = 'scale(1.02)'
|
|
80
|
-
break
|
|
81
|
-
default:
|
|
82
|
-
element.style.transform = 'scale(0.97)'
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
setTimeout(() => {
|
|
86
|
-
element.style.transform = originalTransform
|
|
87
|
-
setTimeout(() => {
|
|
88
|
-
element.style.transition = originalTransition
|
|
89
|
-
if (activeVisualFeedback.value === feedbackId) {
|
|
90
|
-
activeVisualFeedback.value = null
|
|
91
|
-
}
|
|
92
|
-
}, 75)
|
|
93
|
-
}, 75)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Limpar feedback visual após duração
|
|
97
|
-
setTimeout(() => {
|
|
98
|
-
if (activeVisualFeedback.value === feedbackId) {
|
|
99
|
-
activeVisualFeedback.value = null
|
|
100
|
-
}
|
|
101
|
-
}, visualDuration)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Feedback leve (toque suave)
|
|
105
|
-
const light = (element?: HTMLElement) => {
|
|
106
|
-
if (!isEnabled.value) return
|
|
107
|
-
|
|
108
|
-
let success = false
|
|
109
|
-
|
|
110
|
-
if (isSupported.value) {
|
|
111
|
-
success = executeNativeVibration(10)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (!success && fallbackToVisual) {
|
|
115
|
-
executeVisualFeedback('light', element)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Feedback médio (toque moderado)
|
|
120
|
-
const medium = (element?: HTMLElement) => {
|
|
121
|
-
if (!isEnabled.value) return
|
|
122
|
-
|
|
123
|
-
let success = false
|
|
124
|
-
|
|
125
|
-
if (isSupported.value) {
|
|
126
|
-
success = executeNativeVibration(20)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (!success && fallbackToVisual) {
|
|
130
|
-
executeVisualFeedback('medium', element)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Feedback pesado (toque forte)
|
|
135
|
-
const heavy = (element?: HTMLElement) => {
|
|
136
|
-
if (!isEnabled.value) return
|
|
137
|
-
|
|
138
|
-
let success = false
|
|
139
|
-
|
|
140
|
-
if (isSupported.value) {
|
|
141
|
-
success = executeNativeVibration(40)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!success && fallbackToVisual) {
|
|
145
|
-
executeVisualFeedback('heavy', element)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Feedback de seleção
|
|
150
|
-
const selection = (element?: HTMLElement) => {
|
|
151
|
-
if (!isEnabled.value) return
|
|
152
|
-
|
|
153
|
-
let success = false
|
|
154
|
-
|
|
155
|
-
if (isSupported.value) {
|
|
156
|
-
success = executeNativeVibration(5)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (!success && fallbackToVisual) {
|
|
160
|
-
executeVisualFeedback('selection', element)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Feedback de impacto
|
|
165
|
-
const impact = (intensity: 'light' | 'medium' | 'heavy' = 'medium', element?: HTMLElement) => {
|
|
166
|
-
switch (intensity) {
|
|
167
|
-
case 'light':
|
|
168
|
-
light(element)
|
|
169
|
-
break
|
|
170
|
-
case 'medium':
|
|
171
|
-
medium(element)
|
|
172
|
-
break
|
|
173
|
-
case 'heavy':
|
|
174
|
-
heavy(element)
|
|
175
|
-
break
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Feedback de notificação
|
|
180
|
-
const notification = (type: NotificationType, element?: HTMLElement) => {
|
|
181
|
-
if (!isEnabled.value) return
|
|
182
|
-
|
|
183
|
-
let pattern: number[]
|
|
184
|
-
|
|
185
|
-
switch (type) {
|
|
186
|
-
case 'success':
|
|
187
|
-
pattern = [10, 50, 10]
|
|
188
|
-
break
|
|
189
|
-
case 'warning':
|
|
190
|
-
pattern = [20, 100, 20, 100, 20]
|
|
191
|
-
break
|
|
192
|
-
case 'error':
|
|
193
|
-
pattern = [50, 100, 50, 100, 50]
|
|
194
|
-
break
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let success = false
|
|
198
|
-
|
|
199
|
-
if (isSupported.value) {
|
|
200
|
-
success = executeNativeVibration(pattern)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (!success && fallbackToVisual) {
|
|
204
|
-
executeVisualFeedback('notification', element)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Padrão customizado
|
|
209
|
-
const custom = (pattern: number | number[], element?: HTMLElement) => {
|
|
210
|
-
if (!isEnabled.value) return
|
|
211
|
-
|
|
212
|
-
let success = false
|
|
213
|
-
|
|
214
|
-
if (isSupported.value) {
|
|
215
|
-
success = executeNativeVibration(pattern)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (!success && fallbackToVisual) {
|
|
219
|
-
executeVisualFeedback('medium', element)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Habilitar/desabilitar feedback
|
|
224
|
-
const enable = () => {
|
|
225
|
-
isEnabled.value = true
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const disable = () => {
|
|
229
|
-
isEnabled.value = false
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const toggle = () => {
|
|
233
|
-
isEnabled.value = !isEnabled.value
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Computed
|
|
237
|
-
const canVibrate = computed(() => isSupported.value && isEnabled.value)
|
|
238
|
-
const hasVisualFeedback = computed(() => activeVisualFeedback.value !== null)
|
|
239
|
-
|
|
240
|
-
// Método principal para feedback háptico
|
|
241
|
-
const triggerHaptic = (type: HapticType | NotificationType = 'light') => {
|
|
242
|
-
if (!isEnabled.value) return false
|
|
243
|
-
|
|
244
|
-
// Tentar haptic feedback nativo primeiro
|
|
245
|
-
if ('hapticFeedback' in navigator && (navigator as any).hapticFeedback) {
|
|
246
|
-
try {
|
|
247
|
-
return (navigator as any).hapticFeedback.impact(type)
|
|
248
|
-
} catch (error) {
|
|
249
|
-
console.warn('Haptic feedback failed:', error)
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Fallback para vibration API
|
|
254
|
-
if ('vibrate' in navigator && typeof (navigator as any).vibrate === 'function') {
|
|
255
|
-
let pattern: number | number[]
|
|
256
|
-
|
|
257
|
-
switch (type) {
|
|
258
|
-
case 'light':
|
|
259
|
-
pattern = 10
|
|
260
|
-
break
|
|
261
|
-
case 'medium':
|
|
262
|
-
pattern = 20
|
|
263
|
-
break
|
|
264
|
-
case 'heavy':
|
|
265
|
-
pattern = 30
|
|
266
|
-
break
|
|
267
|
-
case 'success':
|
|
268
|
-
pattern = [10, 50, 10]
|
|
269
|
-
break
|
|
270
|
-
case 'warning':
|
|
271
|
-
pattern = [20, 100, 20]
|
|
272
|
-
break
|
|
273
|
-
case 'error':
|
|
274
|
-
pattern = [50, 100, 50, 100, 50]
|
|
275
|
-
break
|
|
276
|
-
default:
|
|
277
|
-
pattern = 10
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
return (navigator as any).vibrate(pattern)
|
|
282
|
-
} catch (error) {
|
|
283
|
-
console.warn('Vibration failed:', error)
|
|
284
|
-
return false
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return false
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Injetar CSS para animações visuais
|
|
292
|
-
const injectVisualCSS = () => {
|
|
293
|
-
const styleId = 'haptic-keyframes'
|
|
294
|
-
|
|
295
|
-
if (document.getElementById(styleId)) return
|
|
296
|
-
|
|
297
|
-
const style = document.createElement('style')
|
|
298
|
-
style.id = styleId
|
|
299
|
-
style.textContent = `
|
|
300
|
-
@keyframes haptic-pulse-light {
|
|
301
|
-
0% { transform: scale(1); }
|
|
302
|
-
50% { transform: scale(0.98); }
|
|
303
|
-
100% { transform: scale(1); }
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
@keyframes haptic-pulse-medium {
|
|
307
|
-
0% { transform: scale(1); }
|
|
308
|
-
50% { transform: scale(0.95); }
|
|
309
|
-
100% { transform: scale(1); }
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
@keyframes haptic-pulse-heavy {
|
|
313
|
-
0% { transform: scale(1); }
|
|
314
|
-
50% { transform: scale(0.92); }
|
|
315
|
-
100% { transform: scale(1); }
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
@keyframes haptic-success {
|
|
319
|
-
0% { transform: scale(1); filter: brightness(1); }
|
|
320
|
-
50% { transform: scale(1.02); filter: brightness(1.1); }
|
|
321
|
-
100% { transform: scale(1); filter: brightness(1); }
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
@keyframes haptic-error {
|
|
325
|
-
0% { transform: translateX(0); }
|
|
326
|
-
25% { transform: translateX(-2px); }
|
|
327
|
-
75% { transform: translateX(2px); }
|
|
328
|
-
100% { transform: translateX(0); }
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
@media (prefers-reduced-motion: reduce) {
|
|
332
|
-
@keyframes haptic-pulse-light,
|
|
333
|
-
@keyframes haptic-pulse-medium,
|
|
334
|
-
@keyframes haptic-pulse-heavy,
|
|
335
|
-
@keyframes haptic-success,
|
|
336
|
-
@keyframes haptic-error {
|
|
337
|
-
0%, 100% { transform: none; filter: none; }
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
`
|
|
341
|
-
|
|
342
|
-
document.head.appendChild(style)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Feedback visual
|
|
346
|
-
const triggerVisualFeedback = (element: HTMLElement | null, type: HapticType | NotificationType) => {
|
|
347
|
-
if (!element) return
|
|
348
|
-
|
|
349
|
-
injectVisualCSS()
|
|
350
|
-
|
|
351
|
-
const originalAnimation = element.style.animation
|
|
352
|
-
let animation: string
|
|
353
|
-
let duration: number
|
|
354
|
-
|
|
355
|
-
switch (type) {
|
|
356
|
-
case 'light':
|
|
357
|
-
animation = 'haptic-pulse-light 0.15s ease-out'
|
|
358
|
-
duration = 150
|
|
359
|
-
break
|
|
360
|
-
case 'medium':
|
|
361
|
-
animation = 'haptic-pulse-medium 0.2s ease-out'
|
|
362
|
-
duration = 200
|
|
363
|
-
break
|
|
364
|
-
case 'heavy':
|
|
365
|
-
animation = 'haptic-pulse-heavy 0.25s ease-out'
|
|
366
|
-
duration = 250
|
|
367
|
-
break
|
|
368
|
-
case 'success':
|
|
369
|
-
animation = 'haptic-success 0.3s ease-out'
|
|
370
|
-
duration = 300
|
|
371
|
-
break
|
|
372
|
-
case 'warning':
|
|
373
|
-
animation = 'haptic-pulse-medium 0.2s ease-out'
|
|
374
|
-
duration = 200
|
|
375
|
-
break
|
|
376
|
-
case 'error':
|
|
377
|
-
animation = 'haptic-error 0.4s ease-out'
|
|
378
|
-
duration = 400
|
|
379
|
-
break
|
|
380
|
-
default:
|
|
381
|
-
animation = 'haptic-pulse-light 0.15s ease-out'
|
|
382
|
-
duration = 150
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
element.style.animation = animation
|
|
386
|
-
|
|
387
|
-
setTimeout(() => {
|
|
388
|
-
element.style.animation = originalAnimation
|
|
389
|
-
}, duration)
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Feedback combinado (háptico + visual)
|
|
393
|
-
const feedback = (element: HTMLElement | null, type: HapticType | NotificationType) => {
|
|
394
|
-
const hapticResult = triggerHaptic(type)
|
|
395
|
-
triggerVisualFeedback(element, type)
|
|
396
|
-
return hapticResult
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Verificar suporte (método público)
|
|
400
|
-
const checkSupport = () => {
|
|
401
|
-
const hasSupport = detectHapticSupport()
|
|
402
|
-
isSupported.value = hasSupport
|
|
403
|
-
return hasSupport
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Inicializar detecção de suporte
|
|
407
|
-
checkSupport()
|
|
408
|
-
|
|
409
|
-
return {
|
|
410
|
-
// Estado
|
|
411
|
-
isSupported,
|
|
412
|
-
isEnabled,
|
|
413
|
-
canVibrate,
|
|
414
|
-
hasVisualFeedback,
|
|
415
|
-
activeVisualFeedback,
|
|
416
|
-
|
|
417
|
-
// Métodos principais (compatíveis com testes)
|
|
418
|
-
triggerHaptic,
|
|
419
|
-
triggerVisualFeedback,
|
|
420
|
-
feedback,
|
|
421
|
-
checkSupport,
|
|
422
|
-
|
|
423
|
-
// Métodos de feedback específicos
|
|
424
|
-
light,
|
|
425
|
-
medium,
|
|
426
|
-
heavy,
|
|
427
|
-
selection,
|
|
428
|
-
impact,
|
|
429
|
-
notification,
|
|
430
|
-
custom,
|
|
431
|
-
|
|
432
|
-
// Controles
|
|
433
|
-
enable,
|
|
434
|
-
disable,
|
|
435
|
-
toggle,
|
|
436
|
-
|
|
437
|
-
// Utilitários
|
|
438
|
-
detectHapticSupport
|
|
439
|
-
}
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
|
+
|
|
3
|
+
type HapticType = 'light' | 'medium' | 'heavy' | 'selection' | 'impact' | 'notification'
|
|
4
|
+
type NotificationType = 'success' | 'warning' | 'error'
|
|
5
|
+
|
|
6
|
+
interface HapticOptions {
|
|
7
|
+
enabled?: boolean
|
|
8
|
+
fallbackToVisual?: boolean
|
|
9
|
+
visualDuration?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Composable para feedback háptico e visual
|
|
14
|
+
* Suporta dispositivos iOS/Android com fallback visual
|
|
15
|
+
*/
|
|
16
|
+
export function useHapticFeedback(options: HapticOptions = {}) {
|
|
17
|
+
const {
|
|
18
|
+
enabled = true,
|
|
19
|
+
fallbackToVisual = true,
|
|
20
|
+
visualDuration = 150
|
|
21
|
+
} = options
|
|
22
|
+
|
|
23
|
+
const isSupported = ref(false)
|
|
24
|
+
const isEnabled = ref(enabled)
|
|
25
|
+
const activeVisualFeedback = ref<string | null>(null)
|
|
26
|
+
|
|
27
|
+
// Detectar suporte a haptic feedback
|
|
28
|
+
const detectHapticSupport = () => {
|
|
29
|
+
// Haptic Feedback API (iOS/Android)
|
|
30
|
+
if ('hapticFeedback' in navigator && (navigator as any).hapticFeedback) {
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Web Vibration API
|
|
35
|
+
if ('vibrate' in navigator && typeof (navigator as any).vibrate === 'function') {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Executar vibração nativa
|
|
43
|
+
const executeNativeVibration = (pattern: number | number[]) => {
|
|
44
|
+
if (!('vibrate' in navigator) || typeof (navigator as any).vibrate !== 'function') return false
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return (navigator as any).vibrate(pattern)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn('Haptic feedback failed:', error)
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Feedback visual como fallback
|
|
55
|
+
const executeVisualFeedback = (type: HapticType, element?: HTMLElement) => {
|
|
56
|
+
if (!fallbackToVisual) return
|
|
57
|
+
|
|
58
|
+
const feedbackId = `visual-${Date.now()}`
|
|
59
|
+
activeVisualFeedback.value = feedbackId
|
|
60
|
+
|
|
61
|
+
// Aplicar efeito visual ao elemento
|
|
62
|
+
if (element) {
|
|
63
|
+
const originalTransform = element.style.transform
|
|
64
|
+
const originalTransition = element.style.transition
|
|
65
|
+
|
|
66
|
+
element.style.transition = 'transform 75ms ease-out'
|
|
67
|
+
|
|
68
|
+
switch (type) {
|
|
69
|
+
case 'light':
|
|
70
|
+
element.style.transform = 'scale(0.98)'
|
|
71
|
+
break
|
|
72
|
+
case 'medium':
|
|
73
|
+
element.style.transform = 'scale(0.95)'
|
|
74
|
+
break
|
|
75
|
+
case 'heavy':
|
|
76
|
+
element.style.transform = 'scale(0.92)'
|
|
77
|
+
break
|
|
78
|
+
case 'selection':
|
|
79
|
+
element.style.transform = 'scale(1.02)'
|
|
80
|
+
break
|
|
81
|
+
default:
|
|
82
|
+
element.style.transform = 'scale(0.97)'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
element.style.transform = originalTransform
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
element.style.transition = originalTransition
|
|
89
|
+
if (activeVisualFeedback.value === feedbackId) {
|
|
90
|
+
activeVisualFeedback.value = null
|
|
91
|
+
}
|
|
92
|
+
}, 75)
|
|
93
|
+
}, 75)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Limpar feedback visual após duração
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
if (activeVisualFeedback.value === feedbackId) {
|
|
99
|
+
activeVisualFeedback.value = null
|
|
100
|
+
}
|
|
101
|
+
}, visualDuration)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Feedback leve (toque suave)
|
|
105
|
+
const light = (element?: HTMLElement) => {
|
|
106
|
+
if (!isEnabled.value) return
|
|
107
|
+
|
|
108
|
+
let success = false
|
|
109
|
+
|
|
110
|
+
if (isSupported.value) {
|
|
111
|
+
success = executeNativeVibration(10)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!success && fallbackToVisual) {
|
|
115
|
+
executeVisualFeedback('light', element)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Feedback médio (toque moderado)
|
|
120
|
+
const medium = (element?: HTMLElement) => {
|
|
121
|
+
if (!isEnabled.value) return
|
|
122
|
+
|
|
123
|
+
let success = false
|
|
124
|
+
|
|
125
|
+
if (isSupported.value) {
|
|
126
|
+
success = executeNativeVibration(20)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!success && fallbackToVisual) {
|
|
130
|
+
executeVisualFeedback('medium', element)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Feedback pesado (toque forte)
|
|
135
|
+
const heavy = (element?: HTMLElement) => {
|
|
136
|
+
if (!isEnabled.value) return
|
|
137
|
+
|
|
138
|
+
let success = false
|
|
139
|
+
|
|
140
|
+
if (isSupported.value) {
|
|
141
|
+
success = executeNativeVibration(40)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!success && fallbackToVisual) {
|
|
145
|
+
executeVisualFeedback('heavy', element)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Feedback de seleção
|
|
150
|
+
const selection = (element?: HTMLElement) => {
|
|
151
|
+
if (!isEnabled.value) return
|
|
152
|
+
|
|
153
|
+
let success = false
|
|
154
|
+
|
|
155
|
+
if (isSupported.value) {
|
|
156
|
+
success = executeNativeVibration(5)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!success && fallbackToVisual) {
|
|
160
|
+
executeVisualFeedback('selection', element)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Feedback de impacto
|
|
165
|
+
const impact = (intensity: 'light' | 'medium' | 'heavy' = 'medium', element?: HTMLElement) => {
|
|
166
|
+
switch (intensity) {
|
|
167
|
+
case 'light':
|
|
168
|
+
light(element)
|
|
169
|
+
break
|
|
170
|
+
case 'medium':
|
|
171
|
+
medium(element)
|
|
172
|
+
break
|
|
173
|
+
case 'heavy':
|
|
174
|
+
heavy(element)
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Feedback de notificação
|
|
180
|
+
const notification = (type: NotificationType, element?: HTMLElement) => {
|
|
181
|
+
if (!isEnabled.value) return
|
|
182
|
+
|
|
183
|
+
let pattern: number[]
|
|
184
|
+
|
|
185
|
+
switch (type) {
|
|
186
|
+
case 'success':
|
|
187
|
+
pattern = [10, 50, 10]
|
|
188
|
+
break
|
|
189
|
+
case 'warning':
|
|
190
|
+
pattern = [20, 100, 20, 100, 20]
|
|
191
|
+
break
|
|
192
|
+
case 'error':
|
|
193
|
+
pattern = [50, 100, 50, 100, 50]
|
|
194
|
+
break
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let success = false
|
|
198
|
+
|
|
199
|
+
if (isSupported.value) {
|
|
200
|
+
success = executeNativeVibration(pattern)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!success && fallbackToVisual) {
|
|
204
|
+
executeVisualFeedback('notification', element)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Padrão customizado
|
|
209
|
+
const custom = (pattern: number | number[], element?: HTMLElement) => {
|
|
210
|
+
if (!isEnabled.value) return
|
|
211
|
+
|
|
212
|
+
let success = false
|
|
213
|
+
|
|
214
|
+
if (isSupported.value) {
|
|
215
|
+
success = executeNativeVibration(pattern)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!success && fallbackToVisual) {
|
|
219
|
+
executeVisualFeedback('medium', element)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Habilitar/desabilitar feedback
|
|
224
|
+
const enable = () => {
|
|
225
|
+
isEnabled.value = true
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const disable = () => {
|
|
229
|
+
isEnabled.value = false
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const toggle = () => {
|
|
233
|
+
isEnabled.value = !isEnabled.value
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Computed
|
|
237
|
+
const canVibrate = computed(() => isSupported.value && isEnabled.value)
|
|
238
|
+
const hasVisualFeedback = computed(() => activeVisualFeedback.value !== null)
|
|
239
|
+
|
|
240
|
+
// Método principal para feedback háptico
|
|
241
|
+
const triggerHaptic = (type: HapticType | NotificationType = 'light') => {
|
|
242
|
+
if (!isEnabled.value) return false
|
|
243
|
+
|
|
244
|
+
// Tentar haptic feedback nativo primeiro
|
|
245
|
+
if ('hapticFeedback' in navigator && (navigator as any).hapticFeedback) {
|
|
246
|
+
try {
|
|
247
|
+
return (navigator as any).hapticFeedback.impact(type)
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.warn('Haptic feedback failed:', error)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Fallback para vibration API
|
|
254
|
+
if ('vibrate' in navigator && typeof (navigator as any).vibrate === 'function') {
|
|
255
|
+
let pattern: number | number[]
|
|
256
|
+
|
|
257
|
+
switch (type) {
|
|
258
|
+
case 'light':
|
|
259
|
+
pattern = 10
|
|
260
|
+
break
|
|
261
|
+
case 'medium':
|
|
262
|
+
pattern = 20
|
|
263
|
+
break
|
|
264
|
+
case 'heavy':
|
|
265
|
+
pattern = 30
|
|
266
|
+
break
|
|
267
|
+
case 'success':
|
|
268
|
+
pattern = [10, 50, 10]
|
|
269
|
+
break
|
|
270
|
+
case 'warning':
|
|
271
|
+
pattern = [20, 100, 20]
|
|
272
|
+
break
|
|
273
|
+
case 'error':
|
|
274
|
+
pattern = [50, 100, 50, 100, 50]
|
|
275
|
+
break
|
|
276
|
+
default:
|
|
277
|
+
pattern = 10
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
return (navigator as any).vibrate(pattern)
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.warn('Vibration failed:', error)
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Injetar CSS para animações visuais
|
|
292
|
+
const injectVisualCSS = () => {
|
|
293
|
+
const styleId = 'haptic-keyframes'
|
|
294
|
+
|
|
295
|
+
if (document.getElementById(styleId)) return
|
|
296
|
+
|
|
297
|
+
const style = document.createElement('style')
|
|
298
|
+
style.id = styleId
|
|
299
|
+
style.textContent = `
|
|
300
|
+
@keyframes haptic-pulse-light {
|
|
301
|
+
0% { transform: scale(1); }
|
|
302
|
+
50% { transform: scale(0.98); }
|
|
303
|
+
100% { transform: scale(1); }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@keyframes haptic-pulse-medium {
|
|
307
|
+
0% { transform: scale(1); }
|
|
308
|
+
50% { transform: scale(0.95); }
|
|
309
|
+
100% { transform: scale(1); }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@keyframes haptic-pulse-heavy {
|
|
313
|
+
0% { transform: scale(1); }
|
|
314
|
+
50% { transform: scale(0.92); }
|
|
315
|
+
100% { transform: scale(1); }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@keyframes haptic-success {
|
|
319
|
+
0% { transform: scale(1); filter: brightness(1); }
|
|
320
|
+
50% { transform: scale(1.02); filter: brightness(1.1); }
|
|
321
|
+
100% { transform: scale(1); filter: brightness(1); }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
@keyframes haptic-error {
|
|
325
|
+
0% { transform: translateX(0); }
|
|
326
|
+
25% { transform: translateX(-2px); }
|
|
327
|
+
75% { transform: translateX(2px); }
|
|
328
|
+
100% { transform: translateX(0); }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@media (prefers-reduced-motion: reduce) {
|
|
332
|
+
@keyframes haptic-pulse-light,
|
|
333
|
+
@keyframes haptic-pulse-medium,
|
|
334
|
+
@keyframes haptic-pulse-heavy,
|
|
335
|
+
@keyframes haptic-success,
|
|
336
|
+
@keyframes haptic-error {
|
|
337
|
+
0%, 100% { transform: none; filter: none; }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
`
|
|
341
|
+
|
|
342
|
+
document.head.appendChild(style)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Feedback visual
|
|
346
|
+
const triggerVisualFeedback = (element: HTMLElement | null, type: HapticType | NotificationType) => {
|
|
347
|
+
if (!element) return
|
|
348
|
+
|
|
349
|
+
injectVisualCSS()
|
|
350
|
+
|
|
351
|
+
const originalAnimation = element.style.animation
|
|
352
|
+
let animation: string
|
|
353
|
+
let duration: number
|
|
354
|
+
|
|
355
|
+
switch (type) {
|
|
356
|
+
case 'light':
|
|
357
|
+
animation = 'haptic-pulse-light 0.15s ease-out'
|
|
358
|
+
duration = 150
|
|
359
|
+
break
|
|
360
|
+
case 'medium':
|
|
361
|
+
animation = 'haptic-pulse-medium 0.2s ease-out'
|
|
362
|
+
duration = 200
|
|
363
|
+
break
|
|
364
|
+
case 'heavy':
|
|
365
|
+
animation = 'haptic-pulse-heavy 0.25s ease-out'
|
|
366
|
+
duration = 250
|
|
367
|
+
break
|
|
368
|
+
case 'success':
|
|
369
|
+
animation = 'haptic-success 0.3s ease-out'
|
|
370
|
+
duration = 300
|
|
371
|
+
break
|
|
372
|
+
case 'warning':
|
|
373
|
+
animation = 'haptic-pulse-medium 0.2s ease-out'
|
|
374
|
+
duration = 200
|
|
375
|
+
break
|
|
376
|
+
case 'error':
|
|
377
|
+
animation = 'haptic-error 0.4s ease-out'
|
|
378
|
+
duration = 400
|
|
379
|
+
break
|
|
380
|
+
default:
|
|
381
|
+
animation = 'haptic-pulse-light 0.15s ease-out'
|
|
382
|
+
duration = 150
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
element.style.animation = animation
|
|
386
|
+
|
|
387
|
+
setTimeout(() => {
|
|
388
|
+
element.style.animation = originalAnimation
|
|
389
|
+
}, duration)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Feedback combinado (háptico + visual)
|
|
393
|
+
const feedback = (element: HTMLElement | null, type: HapticType | NotificationType) => {
|
|
394
|
+
const hapticResult = triggerHaptic(type)
|
|
395
|
+
triggerVisualFeedback(element, type)
|
|
396
|
+
return hapticResult
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Verificar suporte (método público)
|
|
400
|
+
const checkSupport = () => {
|
|
401
|
+
const hasSupport = detectHapticSupport()
|
|
402
|
+
isSupported.value = hasSupport
|
|
403
|
+
return hasSupport
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Inicializar detecção de suporte
|
|
407
|
+
checkSupport()
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
// Estado
|
|
411
|
+
isSupported,
|
|
412
|
+
isEnabled,
|
|
413
|
+
canVibrate,
|
|
414
|
+
hasVisualFeedback,
|
|
415
|
+
activeVisualFeedback,
|
|
416
|
+
|
|
417
|
+
// Métodos principais (compatíveis com testes)
|
|
418
|
+
triggerHaptic,
|
|
419
|
+
triggerVisualFeedback,
|
|
420
|
+
feedback,
|
|
421
|
+
checkSupport,
|
|
422
|
+
|
|
423
|
+
// Métodos de feedback específicos
|
|
424
|
+
light,
|
|
425
|
+
medium,
|
|
426
|
+
heavy,
|
|
427
|
+
selection,
|
|
428
|
+
impact,
|
|
429
|
+
notification,
|
|
430
|
+
custom,
|
|
431
|
+
|
|
432
|
+
// Controles
|
|
433
|
+
enable,
|
|
434
|
+
disable,
|
|
435
|
+
toggle,
|
|
436
|
+
|
|
437
|
+
// Utilitários
|
|
438
|
+
detectHapticSupport
|
|
439
|
+
}
|
|
440
440
|
}
|