@retray-dev/ui-kit 1.0.0 → 1.5.0

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.
@@ -1,5 +1,14 @@
1
- import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react'
2
- import { View, Text, TouchableOpacity, Animated, StyleSheet } from 'react-native'
1
+ import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'
2
+ import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'
3
+ import Animated, {
4
+ useSharedValue,
5
+ useAnimatedStyle,
6
+ withSpring,
7
+ withTiming,
8
+ runOnJS,
9
+ Easing,
10
+ } from 'react-native-reanimated'
11
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler'
3
12
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
4
13
  import * as Haptics from 'expo-haptics'
5
14
  import { useTheme } from '../../theme'
@@ -11,6 +20,7 @@ export interface ToastItem {
11
20
  title?: string
12
21
  description?: string
13
22
  variant?: ToastVariant
23
+ /** Auto-dismiss delay in milliseconds. Defaults to `3000`. */
14
24
  duration?: number
15
25
  }
16
26
 
@@ -28,57 +38,83 @@ export function useToast() {
28
38
  return useContext(ToastContext)
29
39
  }
30
40
 
41
+ const SWIPE_THRESHOLD = 80
42
+ const VELOCITY_THRESHOLD = 800
43
+
31
44
  function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: () => void }) {
32
45
  const { colors } = useTheme()
33
- const translateY = useRef(new Animated.Value(-80)).current
34
- const opacity = useRef(new Animated.Value(0)).current
46
+ const translateY = useSharedValue(-80)
47
+ const translateX = useSharedValue(0)
48
+ const opacity = useSharedValue(0)
35
49
 
36
50
  useEffect(() => {
37
- Animated.parallel([
38
- Animated.spring(translateY, { toValue: 0, useNativeDriver: true, bounciness: 2 }),
39
- Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true }),
40
- ]).start()
51
+ translateY.value = withTiming(0, { duration: 120, easing: Easing.out(Easing.exp) })
52
+ opacity.value = withTiming(1, { duration: 100 })
41
53
 
42
54
  const timer = setTimeout(() => {
43
- Animated.parallel([
44
- Animated.timing(translateY, { toValue: -80, duration: 200, useNativeDriver: true }),
45
- Animated.timing(opacity, { toValue: 0, duration: 200, useNativeDriver: true }),
46
- ]).start(onDismiss)
55
+ translateY.value = withTiming(-80, { duration: 200 })
56
+ opacity.value = withTiming(0, { duration: 200 }, (done) => {
57
+ if (done) runOnJS(onDismiss)()
58
+ })
47
59
  }, item.duration ?? 3000)
48
60
 
49
61
  return () => clearTimeout(timer)
50
62
  }, [])
51
63
 
64
+ const panGesture = Gesture.Pan()
65
+ .onUpdate((e) => {
66
+ translateX.value = e.translationX
67
+ })
68
+ .onEnd((e) => {
69
+ const shouldDismiss =
70
+ Math.abs(translateX.value) > SWIPE_THRESHOLD ||
71
+ Math.abs(e.velocityX) > VELOCITY_THRESHOLD
72
+ if (shouldDismiss) {
73
+ const direction = translateX.value > 0 ? 1 : -1
74
+ translateX.value = withTiming(direction * 500, { duration: 200 }, (done) => {
75
+ if (done) runOnJS(onDismiss)()
76
+ })
77
+ opacity.value = withTiming(0, { duration: 150 })
78
+ } else {
79
+ translateX.value = withSpring(0, { damping: 20, stiffness: 300 })
80
+ }
81
+ })
82
+
83
+ const animatedStyle = useAnimatedStyle(() => ({
84
+ opacity: opacity.value,
85
+ transform: [{ translateY: translateY.value }, { translateX: translateX.value }],
86
+ }))
87
+
52
88
  const bgColor = {
53
89
  default: colors.foreground,
54
90
  destructive: colors.destructive,
55
- success: '#16a34a',
91
+ success: colors.success,
56
92
  }[item.variant ?? 'default']
57
93
 
58
94
  const textColor = {
59
95
  default: colors.background,
60
96
  destructive: colors.destructiveForeground,
61
- success: '#ffffff',
97
+ success: colors.successForeground,
62
98
  }[item.variant ?? 'default']
63
99
 
64
100
  return (
65
- <Animated.View
66
- style={[styles.toast, { backgroundColor: bgColor, opacity, transform: [{ translateY }] }]}
67
- >
68
- <View style={styles.toastContent}>
69
- {item.title ? (
70
- <Text style={[styles.toastTitle, { color: textColor }]}>{item.title}</Text>
71
- ) : null}
72
- {item.description ? (
73
- <Text style={[styles.toastDescription, { color: textColor, opacity: 0.85 }]}>
74
- {item.description}
75
- </Text>
76
- ) : null}
77
- </View>
78
- <TouchableOpacity onPress={onDismiss} style={styles.dismissButton}>
79
- <Text style={[styles.dismissIcon, { color: textColor }]}>✕</Text>
80
- </TouchableOpacity>
81
- </Animated.View>
101
+ <GestureDetector gesture={panGesture}>
102
+ <Animated.View style={[styles.toast, { backgroundColor: bgColor }, animatedStyle]}>
103
+ <View style={styles.toastContent}>
104
+ {item.title ? (
105
+ <Text style={[styles.toastTitle, { color: textColor }]}>{item.title}</Text>
106
+ ) : null}
107
+ {item.description ? (
108
+ <Text style={[styles.toastDescription, { color: textColor, opacity: 0.85 }]}>
109
+ {item.description}
110
+ </Text>
111
+ ) : null}
112
+ </View>
113
+ <TouchableOpacity onPress={onDismiss} style={styles.dismissButton} touchSoundDisabled={true}>
114
+ <Text style={[styles.dismissIcon, { color: textColor }]}>✕</Text>
115
+ </TouchableOpacity>
116
+ </Animated.View>
117
+ </GestureDetector>
82
118
  )
83
119
  }
84
120
 
@@ -155,8 +191,8 @@ const styles = StyleSheet.create({
155
191
  fontSize: 13,
156
192
  },
157
193
  dismissButton: {
158
- padding: 4,
159
- marginLeft: 8,
194
+ padding: 12,
195
+ marginLeft: 4,
160
196
  },
161
197
  dismissIcon: {
162
198
  fontSize: 12,
@@ -1,5 +1,5 @@
1
- import React from 'react'
2
- import { TouchableOpacity, Text, StyleSheet, TouchableOpacityProps, ViewStyle } from 'react-native'
1
+ import React, { useRef } from 'react'
2
+ import { TouchableOpacity, Animated, Text, StyleSheet, TouchableOpacityProps, ViewStyle } from 'react-native'
3
3
  import * as Haptics from 'expo-haptics'
4
4
  import { useTheme } from '../../theme'
5
5
 
@@ -33,32 +33,44 @@ export function Toggle({
33
33
  ...props
34
34
  }: ToggleProps) {
35
35
  const { colors } = useTheme()
36
+ const scale = useRef(new Animated.Value(1)).current
37
+
38
+ const handlePressIn = () => {
39
+ if (disabled) return
40
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
41
+ }
42
+
43
+ const handlePressOut = () => {
44
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
45
+ }
36
46
 
37
47
  const containerStyle: ViewStyle = pressed
38
48
  ? { backgroundColor: colors.accent }
39
49
  : variant === 'outline'
40
- ? { backgroundColor: 'transparent', borderWidth: 1, borderColor: colors.border }
41
- : { backgroundColor: 'transparent' }
50
+ ? { backgroundColor: 'transparent', borderWidth: 1, borderColor: colors.border }
51
+ : { backgroundColor: 'transparent' }
42
52
 
43
53
  const textColor = pressed ? colors.accentForeground : colors.foreground
44
54
 
45
55
  return (
46
- <TouchableOpacity
47
- style={[
48
- styles.base,
49
- containerStyle,
50
- sizeStyles[size],
51
- disabled && styles.disabled,
52
- style,
53
- ]}
54
- onPress={() => { Haptics.selectionAsync(); onPressedChange?.(!pressed) }}
55
- disabled={disabled}
56
- activeOpacity={0.7}
57
- {...props}
58
- >
59
- {icon}
60
- {label ? <Text style={[styles.label, { color: textColor }]}>{label}</Text> : null}
61
- </TouchableOpacity>
56
+ <Animated.View style={{ transform: [{ scale }] }}>
57
+ <TouchableOpacity
58
+ style={[styles.base, containerStyle, sizeStyles[size], disabled && styles.disabled, style]}
59
+ onPress={() => {
60
+ Haptics.selectionAsync()
61
+ onPressedChange?.(!pressed)
62
+ }}
63
+ onPressIn={handlePressIn}
64
+ onPressOut={handlePressOut}
65
+ disabled={disabled}
66
+ activeOpacity={1}
67
+ touchSoundDisabled={true}
68
+ {...props}
69
+ >
70
+ {icon}
71
+ {label ? <Text style={[styles.label, { color: textColor }]}>{label}</Text> : null}
72
+ </TouchableOpacity>
73
+ </Animated.View>
62
74
  )
63
75
  }
64
76
 
@@ -18,6 +18,8 @@ export const defaultLight: ThemeColors = {
18
18
  border: '#e5e5e5',
19
19
  input: '#e5e5e5',
20
20
  ring: '#a3a3a3',
21
+ success: '#16a34a',
22
+ successForeground: '#ffffff',
21
23
  }
22
24
 
23
25
  export const defaultDark: ThemeColors = {
@@ -38,4 +40,6 @@ export const defaultDark: ThemeColors = {
38
40
  border: '#2a2a2a',
39
41
  input: '#2a2a2a',
40
42
  ring: '#d4d4d4',
43
+ success: '#22c55e',
44
+ successForeground: '#ffffff',
41
45
  }
@@ -16,6 +16,8 @@ export type ThemeColors = {
16
16
  border: string
17
17
  input: string
18
18
  ring: string
19
+ success: string
20
+ successForeground: string
19
21
  }
20
22
 
21
23
  export type Theme = {