@rakeyshgidwani/roger-ui-bank-theme-stan-design 0.1.3 → 0.1.5
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/CHANGELOG.md +1 -1
- package/dist/index.d.ts +131 -131
- package/dist/index.esm.js +148 -148
- package/dist/index.js +148 -148
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/ui/accessibility-demo.tsx +271 -0
- package/src/components/ui/advanced-component-architecture-demo.tsx +916 -0
- package/src/components/ui/advanced-transition-system-demo.tsx +670 -0
- package/src/components/ui/advanced-transition-system.tsx +395 -0
- package/src/components/ui/animation/animated-container.tsx +166 -0
- package/src/components/ui/animation/index.ts +19 -0
- package/src/components/ui/animation/staggered-container.tsx +68 -0
- package/src/components/ui/animation-demo.tsx +250 -0
- package/src/components/ui/badge.tsx +33 -0
- package/src/components/ui/battery-conscious-animation-demo.tsx +568 -0
- package/src/components/ui/border-radius-shadow-demo.tsx +187 -0
- package/src/components/ui/button.tsx +36 -0
- package/src/components/ui/card.tsx +207 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/color-preview.tsx +411 -0
- package/src/components/ui/data-display/chart.tsx +653 -0
- package/src/components/ui/data-display/data-grid-simple.tsx +76 -0
- package/src/components/ui/data-display/data-grid.tsx +680 -0
- package/src/components/ui/data-display/list.tsx +456 -0
- package/src/components/ui/data-display/table.tsx +482 -0
- package/src/components/ui/data-display/timeline.tsx +441 -0
- package/src/components/ui/data-display/tree.tsx +602 -0
- package/src/components/ui/data-display/types.ts +536 -0
- package/src/components/ui/enterprise-mobile-experience-demo.tsx +749 -0
- package/src/components/ui/enterprise-mobile-experience.tsx +464 -0
- package/src/components/ui/feedback/alert.tsx +157 -0
- package/src/components/ui/feedback/progress.tsx +292 -0
- package/src/components/ui/feedback/skeleton.tsx +185 -0
- package/src/components/ui/feedback/toast.tsx +280 -0
- package/src/components/ui/feedback/types.ts +125 -0
- package/src/components/ui/font-preview.tsx +288 -0
- package/src/components/ui/form-demo.tsx +553 -0
- package/src/components/ui/hardware-acceleration-demo.tsx +547 -0
- package/src/components/ui/input.tsx +35 -0
- package/src/components/ui/label.tsx +16 -0
- package/src/components/ui/layout-demo.tsx +367 -0
- package/src/components/ui/layouts/adaptive-layout.tsx +139 -0
- package/src/components/ui/layouts/desktop-layout.tsx +224 -0
- package/src/components/ui/layouts/index.ts +10 -0
- package/src/components/ui/layouts/mobile-layout.tsx +162 -0
- package/src/components/ui/layouts/tablet-layout.tsx +197 -0
- package/src/components/ui/mobile-form-validation.tsx +451 -0
- package/src/components/ui/mobile-input-demo.tsx +201 -0
- package/src/components/ui/mobile-input.tsx +281 -0
- package/src/components/ui/mobile-skeleton-loading-demo.tsx +638 -0
- package/src/components/ui/navigation/breadcrumb.tsx +158 -0
- package/src/components/ui/navigation/index.ts +36 -0
- package/src/components/ui/navigation/menu.tsx +374 -0
- package/src/components/ui/navigation/navigation-demo.tsx +324 -0
- package/src/components/ui/navigation/pagination.tsx +272 -0
- package/src/components/ui/navigation/sidebar.tsx +383 -0
- package/src/components/ui/navigation/stepper.tsx +303 -0
- package/src/components/ui/navigation/tabs.tsx +205 -0
- package/src/components/ui/navigation/types.ts +299 -0
- package/src/components/ui/overlay/backdrop.tsx +81 -0
- package/src/components/ui/overlay/focus-manager.tsx +143 -0
- package/src/components/ui/overlay/index.ts +36 -0
- package/src/components/ui/overlay/modal.tsx +270 -0
- package/src/components/ui/overlay/overlay-manager.tsx +110 -0
- package/src/components/ui/overlay/popover.tsx +462 -0
- package/src/components/ui/overlay/portal.tsx +79 -0
- package/src/components/ui/overlay/tooltip.tsx +303 -0
- package/src/components/ui/overlay/types.ts +196 -0
- package/src/components/ui/performance-demo.tsx +596 -0
- package/src/components/ui/semantic-input-system-demo.tsx +502 -0
- package/src/components/ui/semantic-input-system-demo.tsx.disabled +873 -0
- package/src/components/ui/tablet-layout.tsx +192 -0
- package/src/components/ui/theme-customizer.tsx +386 -0
- package/src/components/ui/theme-preview.tsx +310 -0
- package/src/components/ui/theme-switcher.tsx +264 -0
- package/src/components/ui/theme-toggle.tsx +38 -0
- package/src/components/ui/token-demo.tsx +195 -0
- package/src/components/ui/touch-demo.tsx +462 -0
- package/src/components/ui/touch-friendly-interface-demo.tsx +519 -0
- package/src/components/ui/touch-friendly-interface.tsx +296 -0
- package/src/hooks/index.ts +190 -0
- package/src/hooks/use-accessibility-support.ts +518 -0
- package/src/hooks/use-adaptive-layout.ts +289 -0
- package/src/hooks/use-advanced-patterns.ts +294 -0
- package/src/hooks/use-advanced-transition-system.ts +393 -0
- package/src/hooks/use-animation-profile.ts +288 -0
- package/src/hooks/use-battery-animations.ts +384 -0
- package/src/hooks/use-battery-conscious-loading.ts +475 -0
- package/src/hooks/use-battery-optimization.ts +330 -0
- package/src/hooks/use-battery-status.ts +299 -0
- package/src/hooks/use-component-performance.ts +344 -0
- package/src/hooks/use-device-loading-states.ts +459 -0
- package/src/hooks/use-device.tsx +110 -0
- package/src/hooks/use-enterprise-mobile-experience.ts +488 -0
- package/src/hooks/use-form-feedback.ts +403 -0
- package/src/hooks/use-form-performance.ts +513 -0
- package/src/hooks/use-frame-rate.ts +251 -0
- package/src/hooks/use-gestures.ts +338 -0
- package/src/hooks/use-hardware-acceleration.ts +341 -0
- package/src/hooks/use-input-accessibility.ts +455 -0
- package/src/hooks/use-input-performance.ts +506 -0
- package/src/hooks/use-layout-performance.ts +319 -0
- package/src/hooks/use-loading-accessibility.ts +535 -0
- package/src/hooks/use-loading-performance.ts +473 -0
- package/src/hooks/use-memory-usage.ts +287 -0
- package/src/hooks/use-mobile-form-layout.ts +464 -0
- package/src/hooks/use-mobile-form-validation.ts +518 -0
- package/src/hooks/use-mobile-keyboard-optimization.ts +472 -0
- package/src/hooks/use-mobile-layout.ts +302 -0
- package/src/hooks/use-mobile-optimization.ts +406 -0
- package/src/hooks/use-mobile-skeleton.ts +402 -0
- package/src/hooks/use-mobile-touch.ts +414 -0
- package/src/hooks/use-performance-throttling.ts +348 -0
- package/src/hooks/use-performance.ts +316 -0
- package/src/hooks/use-reusable-architecture.ts +414 -0
- package/src/hooks/use-semantic-input-types.ts +357 -0
- package/src/hooks/use-semantic-input.ts +565 -0
- package/src/hooks/use-tablet-layout.ts +384 -0
- package/src/hooks/use-touch-friendly-input.ts +524 -0
- package/src/hooks/use-touch-friendly-interface.ts +331 -0
- package/src/hooks/use-touch-optimization.ts +375 -0
- package/src/index.ts +279 -279
- package/src/lib/utils.ts +6 -0
- package/src/themes/README.md +272 -0
- package/src/themes/ThemeContext.tsx +31 -0
- package/src/themes/ThemeProvider.tsx +232 -0
- package/src/themes/accessibility/index.ts +27 -0
- package/src/themes/accessibility.ts +259 -0
- package/src/themes/aria-patterns.ts +420 -0
- package/src/themes/base-themes.ts +55 -0
- package/src/themes/colorManager.ts +380 -0
- package/src/themes/examples/dark-theme.ts +154 -0
- package/src/themes/examples/minimal-theme.ts +108 -0
- package/src/themes/focus-management.ts +701 -0
- package/src/themes/fontLoader.ts +201 -0
- package/src/themes/high-contrast.ts +621 -0
- package/src/themes/index.ts +19 -0
- package/src/themes/inheritance.ts +227 -0
- package/src/themes/keyboard-navigation.ts +550 -0
- package/src/themes/motion-reduction.ts +662 -0
- package/src/themes/navigation.ts +238 -0
- package/src/themes/screen-reader.ts +645 -0
- package/src/themes/systemThemeDetector.ts +182 -0
- package/src/themes/themeCSSUpdater.ts +262 -0
- package/src/themes/themePersistence.ts +238 -0
- package/src/themes/themes/default.ts +586 -0
- package/src/themes/themes/harvey.ts +554 -0
- package/src/themes/themes/stan-design.ts +683 -0
- package/src/themes/types.ts +460 -0
- package/src/themes/useSystemTheme.ts +48 -0
- package/src/themes/useTheme.ts +87 -0
- package/src/themes/validation.ts +462 -0
- package/src/tokens/index.ts +34 -0
- package/src/tokens/tokenExporter.ts +397 -0
- package/src/tokens/tokenGenerator.ts +276 -0
- package/src/tokens/tokenManager.ts +248 -0
- package/src/tokens/tokenValidator.ts +543 -0
- package/src/tokens/types.ts +78 -0
- package/src/utils/bundle-analyzer.ts +260 -0
- package/src/utils/bundle-splitting.ts +483 -0
- package/src/utils/lazy-loading.ts +441 -0
- package/src/utils/performance-monitor.ts +513 -0
- package/src/utils/tree-shaking.ts +274 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface FrameRateMetrics {
|
|
4
|
+
fps: number
|
|
5
|
+
frameTime: number
|
|
6
|
+
frameCount: number
|
|
7
|
+
averageFPS: number
|
|
8
|
+
minFPS: number
|
|
9
|
+
maxFPS: number
|
|
10
|
+
droppedFrames: number
|
|
11
|
+
performanceScore: 'excellent' | 'good' | 'fair' | 'poor'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FrameRateThresholds {
|
|
15
|
+
warning: number
|
|
16
|
+
critical: number
|
|
17
|
+
excellent: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FrameRateCallbacks {
|
|
21
|
+
onFPSWarning?: (fps: number) => void
|
|
22
|
+
onFPSDrop?: (fps: number, previousFPS: number) => void
|
|
23
|
+
onPerformanceScoreChange?: (score: FrameRateMetrics['performanceScore']) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FrameRateOptions {
|
|
27
|
+
updateInterval?: number
|
|
28
|
+
sampleSize?: number
|
|
29
|
+
thresholds?: Partial<FrameRateThresholds>
|
|
30
|
+
callbacks?: FrameRateCallbacks
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_THRESHOLDS: FrameRateThresholds = {
|
|
34
|
+
warning: 45,
|
|
35
|
+
critical: 30,
|
|
36
|
+
excellent: 55
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_SAMPLE_SIZE = 60 // 1 second at 60fps
|
|
40
|
+
|
|
41
|
+
export const useFrameRate = (options: FrameRateOptions = {}) => {
|
|
42
|
+
const {
|
|
43
|
+
updateInterval = 1000,
|
|
44
|
+
sampleSize = DEFAULT_SAMPLE_SIZE,
|
|
45
|
+
thresholds = {},
|
|
46
|
+
callbacks = {}
|
|
47
|
+
} = options
|
|
48
|
+
|
|
49
|
+
const [metrics, setMetrics] = useState<FrameRateMetrics>({
|
|
50
|
+
fps: 60,
|
|
51
|
+
frameTime: 16.67,
|
|
52
|
+
frameCount: 0,
|
|
53
|
+
averageFPS: 60,
|
|
54
|
+
minFPS: 60,
|
|
55
|
+
maxFPS: 60,
|
|
56
|
+
droppedFrames: 0,
|
|
57
|
+
performanceScore: 'excellent'
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const [isMonitoring, setIsMonitoring] = useState(false)
|
|
61
|
+
const frameCountRef = useRef(0)
|
|
62
|
+
const lastTimeRef = useRef(performance.now())
|
|
63
|
+
const animationIdRef = useRef<number>()
|
|
64
|
+
const intervalIdRef = useRef<NodeJS.Timeout>()
|
|
65
|
+
const fpsHistoryRef = useRef<number[]>([])
|
|
66
|
+
const previousFPSRef = useRef(60)
|
|
67
|
+
|
|
68
|
+
// Merge default thresholds with custom ones
|
|
69
|
+
const finalThresholds = useMemo(() => ({
|
|
70
|
+
...DEFAULT_THRESHOLDS,
|
|
71
|
+
...thresholds
|
|
72
|
+
}), [thresholds])
|
|
73
|
+
|
|
74
|
+
// Calculate performance score based on FPS
|
|
75
|
+
const calculatePerformanceScore = useCallback((fps: number): FrameRateMetrics['performanceScore'] => {
|
|
76
|
+
if (fps >= finalThresholds.excellent) return 'excellent'
|
|
77
|
+
if (fps >= finalThresholds.warning) return 'good'
|
|
78
|
+
if (fps >= finalThresholds.critical) return 'fair'
|
|
79
|
+
return 'poor'
|
|
80
|
+
}, [finalThresholds])
|
|
81
|
+
|
|
82
|
+
// Update FPS history and calculate statistics
|
|
83
|
+
const updateFPSHistory = useCallback((fps: number) => {
|
|
84
|
+
fpsHistoryRef.current.push(fps)
|
|
85
|
+
|
|
86
|
+
// Keep only the last sampleSize frames
|
|
87
|
+
if (fpsHistoryRef.current.length > sampleSize) {
|
|
88
|
+
fpsHistoryRef.current.shift()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Calculate statistics
|
|
92
|
+
const averageFPS = Math.round(
|
|
93
|
+
fpsHistoryRef.current.reduce((sum, f) => sum + f, 0) / fpsHistoryRef.current.length
|
|
94
|
+
)
|
|
95
|
+
const minFPS = Math.min(...fpsHistoryRef.current)
|
|
96
|
+
const maxFPS = Math.max(...fpsHistoryRef.current)
|
|
97
|
+
|
|
98
|
+
return { averageFPS, minFPS, maxFPS }
|
|
99
|
+
}, [sampleSize])
|
|
100
|
+
|
|
101
|
+
// Detect dropped frames
|
|
102
|
+
const detectDroppedFrames = useCallback((fps: number, previousFPS: number) => {
|
|
103
|
+
if (fps < previousFPS && previousFPS >= finalThresholds.warning) {
|
|
104
|
+
const drop = previousFPS - fps
|
|
105
|
+
if (drop > 10) { // Significant drop
|
|
106
|
+
return drop
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return 0
|
|
110
|
+
}, [finalThresholds.warning])
|
|
111
|
+
|
|
112
|
+
// Frame rate monitoring
|
|
113
|
+
const measureFrameRate = useCallback(() => {
|
|
114
|
+
frameCountRef.current++
|
|
115
|
+
const currentTime = performance.now()
|
|
116
|
+
|
|
117
|
+
if (currentTime - lastTimeRef.current >= updateInterval) {
|
|
118
|
+
const fps = Math.round((frameCountRef.current * 1000) / (currentTime - lastTimeRef.current))
|
|
119
|
+
const frameTime = 1000 / fps
|
|
120
|
+
|
|
121
|
+
// Update FPS history and get statistics
|
|
122
|
+
const { averageFPS, minFPS, maxFPS } = updateFPSHistory(fps)
|
|
123
|
+
|
|
124
|
+
// Detect dropped frames
|
|
125
|
+
const droppedFrames = detectDroppedFrames(fps, previousFPSRef.current)
|
|
126
|
+
|
|
127
|
+
// Calculate performance score
|
|
128
|
+
const performanceScore = calculatePerformanceScore(fps)
|
|
129
|
+
|
|
130
|
+
const newMetrics: FrameRateMetrics = {
|
|
131
|
+
fps,
|
|
132
|
+
frameTime,
|
|
133
|
+
frameCount: frameCountRef.current,
|
|
134
|
+
averageFPS,
|
|
135
|
+
minFPS,
|
|
136
|
+
maxFPS,
|
|
137
|
+
droppedFrames,
|
|
138
|
+
performanceScore
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setMetrics(newMetrics)
|
|
142
|
+
|
|
143
|
+
// Check thresholds and call callbacks
|
|
144
|
+
if (fps <= finalThresholds.critical && callbacks.onFPSWarning) {
|
|
145
|
+
callbacks.onFPSWarning(fps)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (droppedFrames > 0 && callbacks.onFPSDrop) {
|
|
149
|
+
callbacks.onFPSDrop(fps, previousFPSRef.current)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if performance score changed
|
|
153
|
+
if (performanceScore !== metrics.performanceScore && callbacks.onPerformanceScoreChange) {
|
|
154
|
+
callbacks.onPerformanceScoreChange(performanceScore)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Reset counters
|
|
158
|
+
frameCountRef.current = 0
|
|
159
|
+
lastTimeRef.current = currentTime
|
|
160
|
+
previousFPSRef.current = fps
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
animationIdRef.current = requestAnimationFrame(measureFrameRate)
|
|
164
|
+
}, [updateInterval, finalThresholds.critical, callbacks, metrics.performanceScore, updateFPSHistory, detectDroppedFrames, calculatePerformanceScore])
|
|
165
|
+
|
|
166
|
+
// Start monitoring
|
|
167
|
+
const startMonitoring = useCallback(() => {
|
|
168
|
+
if (isMonitoring) return
|
|
169
|
+
|
|
170
|
+
setIsMonitoring(true)
|
|
171
|
+
measureFrameRate()
|
|
172
|
+
}, [isMonitoring, measureFrameRate])
|
|
173
|
+
|
|
174
|
+
// Stop monitoring
|
|
175
|
+
const stopMonitoring = useCallback(() => {
|
|
176
|
+
setIsMonitoring(false)
|
|
177
|
+
|
|
178
|
+
if (animationIdRef.current) {
|
|
179
|
+
cancelAnimationFrame(animationIdRef.current)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (intervalIdRef.current) {
|
|
183
|
+
clearInterval(intervalIdRef.current)
|
|
184
|
+
}
|
|
185
|
+
}, [])
|
|
186
|
+
|
|
187
|
+
// Reset statistics
|
|
188
|
+
const resetStats = useCallback(() => {
|
|
189
|
+
fpsHistoryRef.current = []
|
|
190
|
+
setMetrics(prev => ({
|
|
191
|
+
...prev,
|
|
192
|
+
averageFPS: 60,
|
|
193
|
+
minFPS: 60,
|
|
194
|
+
maxFPS: 60,
|
|
195
|
+
droppedFrames: 0
|
|
196
|
+
}))
|
|
197
|
+
}, [])
|
|
198
|
+
|
|
199
|
+
// Auto-start monitoring on mount
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
startMonitoring()
|
|
202
|
+
return () => stopMonitoring()
|
|
203
|
+
}, [startMonitoring, stopMonitoring])
|
|
204
|
+
|
|
205
|
+
// Performance insights
|
|
206
|
+
const getPerformanceInsights = useCallback(() => {
|
|
207
|
+
const insights: string[] = []
|
|
208
|
+
|
|
209
|
+
if (metrics.fps < finalThresholds.critical) {
|
|
210
|
+
insights.push('Critical FPS drop detected - consider reducing animation complexity')
|
|
211
|
+
} else if (metrics.fps < finalThresholds.warning) {
|
|
212
|
+
insights.push('FPS below optimal range - monitor for performance issues')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (metrics.droppedFrames > 0) {
|
|
216
|
+
insights.push(`${metrics.droppedFrames} frames dropped - check for heavy operations`)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (metrics.averageFPS < 50) {
|
|
220
|
+
insights.push('Average FPS is low - consider performance optimizations')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return insights
|
|
224
|
+
}, [metrics, finalThresholds])
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
// Metrics
|
|
228
|
+
metrics,
|
|
229
|
+
|
|
230
|
+
// Controls
|
|
231
|
+
startMonitoring,
|
|
232
|
+
stopMonitoring,
|
|
233
|
+
isMonitoring,
|
|
234
|
+
resetStats,
|
|
235
|
+
|
|
236
|
+
// Performance analysis
|
|
237
|
+
performanceScore: metrics.performanceScore,
|
|
238
|
+
getPerformanceInsights,
|
|
239
|
+
|
|
240
|
+
// Raw values
|
|
241
|
+
fps: metrics.fps,
|
|
242
|
+
frameTime: metrics.frameTime,
|
|
243
|
+
averageFPS: metrics.averageFPS,
|
|
244
|
+
minFPS: metrics.minFPS,
|
|
245
|
+
maxFPS: metrics.maxFPS,
|
|
246
|
+
droppedFrames: metrics.droppedFrames,
|
|
247
|
+
|
|
248
|
+
// Thresholds
|
|
249
|
+
thresholds: finalThresholds
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface GestureConfig {
|
|
4
|
+
minSwipeDistance: number
|
|
5
|
+
maxSwipeTime: number
|
|
6
|
+
minPinchDistance: number
|
|
7
|
+
enableHapticFeedback: boolean
|
|
8
|
+
enableSoundFeedback: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SwipeGesture {
|
|
12
|
+
direction: 'up' | 'down' | 'left' | 'right'
|
|
13
|
+
distance: number
|
|
14
|
+
velocity: number
|
|
15
|
+
duration: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PinchGesture {
|
|
19
|
+
scale: number
|
|
20
|
+
center: { x: number; y: number }
|
|
21
|
+
distance: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GestureCallbacks {
|
|
25
|
+
onSwipe?: (gesture: SwipeGesture) => void
|
|
26
|
+
onPinch?: (gesture: PinchGesture) => void
|
|
27
|
+
onTap?: (position: { x: number; y: number }) => void
|
|
28
|
+
onLongPress?: (position: { x: number; y: number }) => void
|
|
29
|
+
onTouchStart?: (position: { x: number; y: number }) => void
|
|
30
|
+
onTouchEnd?: (position: { x: number; y: number }) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GestureState {
|
|
34
|
+
isTracking: boolean
|
|
35
|
+
currentGesture: 'none' | 'swipe' | 'pinch' | 'tap' | 'longPress'
|
|
36
|
+
lastGesture: SwipeGesture | PinchGesture | null
|
|
37
|
+
touchCount: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const useGestures = (
|
|
41
|
+
elementRef: React.RefObject<HTMLElement>,
|
|
42
|
+
callbacks: GestureCallbacks = {},
|
|
43
|
+
config: Partial<GestureConfig> = {}
|
|
44
|
+
) => {
|
|
45
|
+
const defaultConfig: GestureConfig = {
|
|
46
|
+
minSwipeDistance: 50,
|
|
47
|
+
maxSwipeTime: 300,
|
|
48
|
+
minPinchDistance: 20,
|
|
49
|
+
enableHapticFeedback: true,
|
|
50
|
+
enableSoundFeedback: false,
|
|
51
|
+
...config
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const [gestureState, setGestureState] = useState<GestureState>({
|
|
55
|
+
isTracking: false,
|
|
56
|
+
currentGesture: 'none',
|
|
57
|
+
lastGesture: null,
|
|
58
|
+
touchCount: 0
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null)
|
|
62
|
+
const touchStartDistanceRef = useRef<number>(0)
|
|
63
|
+
const touchStartCenterRef = useRef<{ x: number; y: number } | null>(null)
|
|
64
|
+
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null)
|
|
65
|
+
const lastTapTimeRef = useRef<number>(0)
|
|
66
|
+
|
|
67
|
+
// Haptic feedback function
|
|
68
|
+
const triggerHapticFeedback = useCallback(() => {
|
|
69
|
+
if (defaultConfig.enableHapticFeedback && 'vibrate' in navigator) {
|
|
70
|
+
try {
|
|
71
|
+
navigator.vibrate(50)
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Fallback for older browsers
|
|
74
|
+
console.warn('Haptic feedback not supported')
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}, [defaultConfig.enableHapticFeedback])
|
|
78
|
+
|
|
79
|
+
// Sound feedback function
|
|
80
|
+
const triggerSoundFeedback = useCallback(() => {
|
|
81
|
+
if (defaultConfig.enableSoundFeedback) {
|
|
82
|
+
// Create a simple beep sound
|
|
83
|
+
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
|
|
84
|
+
const oscillator = audioContext.createOscillator()
|
|
85
|
+
const gainNode = audioContext.createGain()
|
|
86
|
+
|
|
87
|
+
oscillator.connect(gainNode)
|
|
88
|
+
gainNode.connect(audioContext.destination)
|
|
89
|
+
|
|
90
|
+
oscillator.frequency.setValueAtTime(800, audioContext.currentTime)
|
|
91
|
+
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime)
|
|
92
|
+
|
|
93
|
+
oscillator.start(audioContext.currentTime)
|
|
94
|
+
oscillator.stop(audioContext.currentTime + 0.1)
|
|
95
|
+
}
|
|
96
|
+
}, [defaultConfig.enableSoundFeedback])
|
|
97
|
+
|
|
98
|
+
// Calculate distance between two points
|
|
99
|
+
const calculateDistance = useCallback((p1: { x: number; y: number }, p2: { x: number; y: number }) => {
|
|
100
|
+
const dx = p2.x - p1.x
|
|
101
|
+
const dy = p2.y - p1.y
|
|
102
|
+
return Math.sqrt(dx * dx + dy * dy)
|
|
103
|
+
}, [])
|
|
104
|
+
|
|
105
|
+
// Calculate center point between two touches
|
|
106
|
+
const calculateCenter = useCallback((touches: TouchList) => {
|
|
107
|
+
let x = 0
|
|
108
|
+
let y = 0
|
|
109
|
+
for (let i = 0; i < touches.length; i++) {
|
|
110
|
+
x += touches[i].clientX
|
|
111
|
+
y += touches[i].clientY
|
|
112
|
+
}
|
|
113
|
+
return { x: x / touches.length, y: y / touches.length }
|
|
114
|
+
}, [])
|
|
115
|
+
|
|
116
|
+
// Handle touch start
|
|
117
|
+
const handleTouchStart = useCallback((event: TouchEvent) => {
|
|
118
|
+
event.preventDefault()
|
|
119
|
+
|
|
120
|
+
const touches = event.touches
|
|
121
|
+
const touchCount = touches.length
|
|
122
|
+
|
|
123
|
+
if (touchCount === 1) {
|
|
124
|
+
const touch = touches[0]
|
|
125
|
+
touchStartRef.current = {
|
|
126
|
+
x: touch.clientX,
|
|
127
|
+
y: touch.clientY,
|
|
128
|
+
time: Date.now()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Start long press timer
|
|
132
|
+
longPressTimerRef.current = setTimeout(() => {
|
|
133
|
+
if (touchStartRef.current) {
|
|
134
|
+
const position = { x: touch.clientX, y: touch.clientY }
|
|
135
|
+
setGestureState(prev => ({ ...prev, currentGesture: 'longPress' }))
|
|
136
|
+
callbacks.onLongPress?.(position)
|
|
137
|
+
triggerHapticFeedback()
|
|
138
|
+
}
|
|
139
|
+
}, 500)
|
|
140
|
+
|
|
141
|
+
callbacks.onTouchStart?.({ x: touch.clientX, y: touch.clientY })
|
|
142
|
+
} else if (touchCount === 2) {
|
|
143
|
+
// Pinch gesture start
|
|
144
|
+
const center = calculateCenter(touches)
|
|
145
|
+
const distance = calculateDistance(
|
|
146
|
+
{ x: touches[0].clientX, y: touches[0].clientY },
|
|
147
|
+
{ x: touches[1].clientX, y: touches[1].clientY }
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
touchStartCenterRef.current = center
|
|
151
|
+
touchStartDistanceRef.current = distance
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
setGestureState(prev => ({ ...prev, isTracking: true, touchCount }))
|
|
155
|
+
}, [callbacks, calculateCenter, calculateDistance, triggerHapticFeedback])
|
|
156
|
+
|
|
157
|
+
// Handle touch move
|
|
158
|
+
const handleTouchMove = useCallback((event: TouchEvent) => {
|
|
159
|
+
event.preventDefault()
|
|
160
|
+
|
|
161
|
+
const touches = event.touches
|
|
162
|
+
const touchCount = touches.length
|
|
163
|
+
|
|
164
|
+
if (touchCount === 1 && touchStartRef.current) {
|
|
165
|
+
// Cancel long press timer on movement
|
|
166
|
+
if (longPressTimerRef.current) {
|
|
167
|
+
clearTimeout(longPressTimerRef.current)
|
|
168
|
+
longPressTimerRef.current = null
|
|
169
|
+
}
|
|
170
|
+
} else if (touchCount === 2 && touchStartCenterRef.current && touchStartDistanceRef.current) {
|
|
171
|
+
// Handle pinch gesture
|
|
172
|
+
const center = calculateCenter(touches)
|
|
173
|
+
const distance = calculateDistance(
|
|
174
|
+
{ x: touches[0].clientX, y: touches[0].clientY },
|
|
175
|
+
{ x: touches[1].clientX, y: touches[1].clientY }
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
const scale = distance / touchStartDistanceRef.current
|
|
179
|
+
|
|
180
|
+
if (Math.abs(scale - 1) * 100 > defaultConfig.minPinchDistance) {
|
|
181
|
+
const pinchGesture: PinchGesture = {
|
|
182
|
+
scale,
|
|
183
|
+
center,
|
|
184
|
+
distance
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
setGestureState(prev => ({ ...prev, currentGesture: 'pinch', lastGesture: pinchGesture }))
|
|
188
|
+
callbacks.onPinch?.(pinchGesture)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}, [callbacks, calculateCenter, calculateDistance, defaultConfig.minPinchDistance])
|
|
192
|
+
|
|
193
|
+
// Handle touch end
|
|
194
|
+
const handleTouchEnd = useCallback((event: TouchEvent) => {
|
|
195
|
+
event.preventDefault()
|
|
196
|
+
|
|
197
|
+
const touches = event.changedTouches
|
|
198
|
+
const touchCount = touches.length
|
|
199
|
+
|
|
200
|
+
if (touchCount === 1 && touchStartRef.current) {
|
|
201
|
+
const touch = touches[0]
|
|
202
|
+
const endTime = Date.now()
|
|
203
|
+
const duration = endTime - touchStartRef.current.time
|
|
204
|
+
|
|
205
|
+
// Calculate swipe gesture
|
|
206
|
+
const dx = touch.clientX - touchStartRef.current.x
|
|
207
|
+
const dy = touch.clientY - touchStartRef.current.y
|
|
208
|
+
const distance = Math.sqrt(dx * dx + dy * dy)
|
|
209
|
+
|
|
210
|
+
if (distance >= defaultConfig.minSwipeDistance && duration <= defaultConfig.maxSwipeTime) {
|
|
211
|
+
// Determine swipe direction
|
|
212
|
+
let direction: 'up' | 'down' | 'left' | 'right'
|
|
213
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
214
|
+
direction = dx > 0 ? 'right' : 'left'
|
|
215
|
+
} else {
|
|
216
|
+
direction = dy > 0 ? 'down' : 'up'
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const velocity = distance / duration
|
|
220
|
+
const swipeGesture: SwipeGesture = {
|
|
221
|
+
direction,
|
|
222
|
+
distance,
|
|
223
|
+
velocity,
|
|
224
|
+
duration
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setGestureState(prev => ({ ...prev, currentGesture: 'swipe', lastGesture: swipeGesture }))
|
|
228
|
+
callbacks.onSwipe?.(swipeGesture)
|
|
229
|
+
triggerHapticFeedback()
|
|
230
|
+
triggerSoundFeedback()
|
|
231
|
+
} else if (distance < 10 && duration < 200) {
|
|
232
|
+
// Handle tap gesture
|
|
233
|
+
const currentTime = Date.now()
|
|
234
|
+
const timeSinceLastTap = currentTime - lastTapTimeRef.current
|
|
235
|
+
|
|
236
|
+
if (timeSinceLastTap < 300) {
|
|
237
|
+
// Double tap detected
|
|
238
|
+
const position = { x: touch.clientX, y: touch.clientY }
|
|
239
|
+
callbacks.onTap?.(position)
|
|
240
|
+
triggerHapticFeedback()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
lastTapTimeRef.current = currentTime
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Clear long press timer
|
|
247
|
+
if (longPressTimerRef.current) {
|
|
248
|
+
clearTimeout(longPressTimerRef.current)
|
|
249
|
+
longPressTimerRef.current = null
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
touchStartRef.current = null
|
|
253
|
+
callbacks.onTouchEnd?.({ x: touch.clientX, y: touch.clientY })
|
|
254
|
+
} else if (touchCount === 0) {
|
|
255
|
+
// All touches ended
|
|
256
|
+
touchStartCenterRef.current = null
|
|
257
|
+
touchStartDistanceRef.current = 0
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
setGestureState(prev => ({ ...prev, isTracking: false, currentGesture: 'none' }))
|
|
261
|
+
}, [callbacks, defaultConfig.minSwipeDistance, defaultConfig.maxSwipeTime, triggerHapticFeedback, triggerSoundFeedback])
|
|
262
|
+
|
|
263
|
+
// Set up event listeners
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
const element = elementRef.current
|
|
266
|
+
if (!element) return
|
|
267
|
+
|
|
268
|
+
element.addEventListener('touchstart', handleTouchStart, { passive: false })
|
|
269
|
+
element.addEventListener('touchmove', handleTouchMove, { passive: false })
|
|
270
|
+
element.addEventListener('touchend', handleTouchEnd, { passive: false })
|
|
271
|
+
|
|
272
|
+
return () => {
|
|
273
|
+
element.removeEventListener('touchstart', handleTouchStart)
|
|
274
|
+
element.removeEventListener('touchmove', handleTouchMove)
|
|
275
|
+
element.removeEventListener('touchend', handleTouchEnd)
|
|
276
|
+
|
|
277
|
+
// Clean up timers
|
|
278
|
+
if (longPressTimerRef.current) {
|
|
279
|
+
clearTimeout(longPressTimerRef.current)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}, [elementRef, handleTouchStart, handleTouchMove, handleTouchEnd])
|
|
283
|
+
|
|
284
|
+
// Utility functions
|
|
285
|
+
const resetGestureState = useCallback(() => {
|
|
286
|
+
setGestureState({
|
|
287
|
+
isTracking: false,
|
|
288
|
+
currentGesture: 'none',
|
|
289
|
+
lastGesture: null,
|
|
290
|
+
touchCount: 0
|
|
291
|
+
})
|
|
292
|
+
}, [])
|
|
293
|
+
|
|
294
|
+
const isGestureActive = useMemo(() => gestureState.isTracking, [gestureState.isTracking])
|
|
295
|
+
const currentGestureType = useMemo(() => gestureState.currentGesture, [gestureState.currentGesture])
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
gestureState,
|
|
299
|
+
isGestureActive,
|
|
300
|
+
currentGestureType,
|
|
301
|
+
resetGestureState,
|
|
302
|
+
triggerHapticFeedback,
|
|
303
|
+
triggerSoundFeedback
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Convenience hooks for specific gestures
|
|
308
|
+
export const useSwipeGesture = (
|
|
309
|
+
elementRef: React.RefObject<HTMLElement>,
|
|
310
|
+
onSwipe: (gesture: SwipeGesture) => void,
|
|
311
|
+
config?: Partial<GestureConfig>
|
|
312
|
+
) => {
|
|
313
|
+
return useGestures(elementRef, { onSwipe }, config)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export const usePinchGesture = (
|
|
317
|
+
elementRef: React.RefObject<HTMLElement>,
|
|
318
|
+
onPinch: (gesture: PinchGesture) => void,
|
|
319
|
+
config?: Partial<GestureConfig>
|
|
320
|
+
) => {
|
|
321
|
+
return useGestures(elementRef, { onPinch }, config)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export const useTapGesture = (
|
|
325
|
+
elementRef: React.RefObject<HTMLElement>,
|
|
326
|
+
onTap: (position: { x: number; y: number }) => void,
|
|
327
|
+
config?: Partial<GestureConfig>
|
|
328
|
+
) => {
|
|
329
|
+
return useGestures(elementRef, { onTap }, config)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export const useLongPressGesture = (
|
|
333
|
+
elementRef: React.RefObject<HTMLElement>,
|
|
334
|
+
onLongPress: (position: { x: number; y: number }) => void,
|
|
335
|
+
config?: Partial<GestureConfig>
|
|
336
|
+
) => {
|
|
337
|
+
return useGestures(elementRef, { onLongPress }, config)
|
|
338
|
+
}
|