@retray-dev/ui-kit 5.4.0 → 6.0.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,281 +1,55 @@
1
- import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'
2
- import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native'
3
- import { FontAwesome5, MaterialIcons, Entypo, AntDesign } from '@expo/vector-icons'
4
- import Animated, {
5
- useSharedValue,
6
- useAnimatedStyle,
7
- withSpring,
8
- withTiming,
9
- Easing,
10
- } from 'react-native-reanimated'
11
- import { scheduleOnRN } from 'react-native-worklets'
12
- import { Gesture, GestureDetector } from 'react-native-gesture-handler'
1
+ import React from 'react'
2
+ import { Toaster, toast as sonnerToast } from 'sonner-native'
13
3
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
14
- import { notificationSuccess, notificationError, impactLight } from '../../utils/haptics'
15
4
  import { useTheme } from '../../theme'
16
5
  import { s, vs, ms } from '../../utils/scaling'
17
- import { renderIcon } from '../../utils/icons'
18
6
 
19
- export type ToastVariant = 'default' | 'destructive' | 'success' | 'warning'
20
-
21
- export interface ToastAction {
22
- label: string
23
- onPress: () => void
24
- }
25
-
26
- export interface ToastItem {
27
- id: string
28
- title?: string
29
- description?: string
30
- variant?: ToastVariant
31
- icon?: React.ReactNode
32
- iconName?: string
33
- iconColor?: string
34
- /** Auto-dismiss delay in milliseconds. Defaults to `3000`. */
35
- duration?: number
36
- /** Optional inline action button rendered at the end of the toast. */
37
- action?: ToastAction
38
- }
39
-
40
- interface ToastContextValue {
41
- toast: (item: Omit<ToastItem, 'id'>) => void
42
- dismiss: (id: string) => void
43
- }
44
-
45
- const ToastContext = createContext<ToastContextValue>({
46
- toast: () => {},
47
- dismiss: () => {},
48
- })
7
+ // Direct function API no hook required
8
+ export { sonnerToast as toast }
49
9
 
10
+ // useToast — backward-compat wrapper
50
11
  export function useToast() {
51
- return useContext(ToastContext)
52
- }
53
-
54
- const SWIPE_THRESHOLD = 80
55
- const VELOCITY_THRESHOLD = 800
56
-
57
- function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: () => void }) {
58
- const { colors } = useTheme()
59
- const translateY = useSharedValue(-80)
60
- const translateX = useSharedValue(0)
61
- const opacity = useSharedValue(0)
62
-
63
- useEffect(() => {
64
- translateY.value = withTiming(0, { duration: 120, easing: Easing.out(Easing.exp) })
65
- opacity.value = withTiming(1, { duration: 100 })
66
-
67
- const timer = setTimeout(() => {
68
- translateY.value = withTiming(-80, { duration: 200 })
69
- opacity.value = withTiming(0, { duration: 200 }, (done) => {
70
- if (done) scheduleOnRN(onDismiss)
71
- })
72
- }, item.duration ?? 3000)
73
-
74
- return () => clearTimeout(timer)
75
- }, [])
76
-
77
- const panGesture = Gesture.Pan()
78
- .onUpdate((e) => {
79
- translateX.value = e.translationX
80
- })
81
- .onEnd((e) => {
82
- const shouldDismiss =
83
- Math.abs(translateX.value) > SWIPE_THRESHOLD ||
84
- Math.abs(e.velocityX) > VELOCITY_THRESHOLD
85
- if (shouldDismiss) {
86
- const direction = translateX.value > 0 ? 1 : -1
87
- translateX.value = withTiming(direction * 500, { duration: 200 }, (done) => {
88
- if (done) scheduleOnRN(onDismiss)
89
- })
90
- opacity.value = withTiming(0, { duration: 150 })
91
- } else {
92
- translateX.value = withSpring(0, { damping: 20, stiffness: 300 })
93
- }
94
- })
95
-
96
- const animatedStyle = useAnimatedStyle(() => ({
97
- opacity: opacity.value,
98
- transform: [{ translateY: translateY.value }, { translateX: translateX.value }],
99
- }))
100
-
101
- const variant = item.variant ?? 'default'
102
-
103
- const bgColor = {
104
- default: colors.card,
105
- destructive: colors.destructiveTint,
106
- success: colors.successTint,
107
- warning: colors.warningTint,
108
- }[variant]
109
-
110
- const borderColor = {
111
- default: colors.border,
112
- destructive: colors.destructiveBorder,
113
- success: colors.successBorder,
114
- warning: colors.warningBorder,
115
- }[variant]
116
-
117
- const accentColor = {
118
- default: colors.primary,
119
- destructive: colors.destructive,
120
- success: colors.success,
121
- warning: colors.warning,
122
- }[variant]
123
-
124
- const titleColor = variant === 'default' ? colors.foreground : accentColor
125
- const descColor = variant === 'default' ? colors.foregroundMuted : accentColor
126
-
127
- const defaultIcon =
128
- variant === 'success' ? (
129
- <FontAwesome5 name="check-circle" size={16} color={accentColor} />
130
- ) : variant === 'destructive' ? (
131
- <AntDesign name="exclamation-circle" size={16} color={accentColor} />
132
- ) : variant === 'warning' ? (
133
- <MaterialIcons name="warning-amber" size={17} color={accentColor} />
134
- ) : (
135
- <Entypo name="info-with-circle" size={16} color={accentColor} />
136
- )
137
-
138
- const leftIcon: React.ReactNode = item.iconName
139
- ? renderIcon(item.iconName, 16, item.iconColor ?? accentColor)
140
- : item.icon ?? defaultIcon
141
-
142
- return (
143
- <GestureDetector gesture={panGesture}>
144
- <Animated.View style={[styles.toast, { backgroundColor: bgColor, borderColor }, animatedStyle]}>
145
- <View style={styles.leftIconContainer}>{leftIcon}</View>
146
- <View style={styles.toastContent}>
147
- {item.title ? (
148
- <Text style={[styles.toastTitle, { color: titleColor }]} allowFontScaling={true}>{item.title}</Text>
149
- ) : null}
150
- {item.description ? (
151
- <Text style={[styles.toastDescription, { color: descColor }]} allowFontScaling={true}>
152
- {item.description}
153
- </Text>
154
- ) : null}
155
- </View>
156
- {item.action && (
157
- <TouchableOpacity
158
- onPress={() => { item.action!.onPress(); onDismiss() }}
159
- style={styles.actionButton}
160
- touchSoundDisabled={true}
161
- >
162
- <Text style={[styles.actionLabel, { color: accentColor }]} allowFontScaling={true}>
163
- {item.action.label}
164
- </Text>
165
- </TouchableOpacity>
166
- )}
167
- <TouchableOpacity onPress={onDismiss} style={styles.dismissButton} touchSoundDisabled={true}>
168
- <AntDesign name="close-circle" size={16} color={descColor} />
169
- </TouchableOpacity>
170
- </Animated.View>
171
- </GestureDetector>
172
- )
173
- }
174
-
175
- /**
176
- * Must wrap the app root alongside ThemeProvider.
177
- * Renders toasts in an absolute overlay at the top of the screen.
178
- * Use `useToast()` anywhere inside to trigger toasts.
179
- */
180
- export interface ToastProviderProps {
181
- children: React.ReactNode
12
+ return {
13
+ toast: sonnerToast,
14
+ dismiss: sonnerToast.dismiss,
15
+ }
182
16
  }
183
17
 
184
- export function ToastProvider({ children }: ToastProviderProps) {
185
- const [toasts, setToasts] = useState<ToastItem[]>([])
18
+ // ToastProvider — wraps children + renders the Toaster
19
+ export function ToastProvider({ children }: { children: React.ReactNode }) {
20
+ const { colorScheme } = useTheme()
186
21
  const insets = useSafeAreaInsets()
187
22
 
188
- const toast = useCallback((item: Omit<ToastItem, 'id'>) => {
189
- const id = Math.random().toString(36).slice(2)
190
- if (item.variant === 'success') {
191
- notificationSuccess()
192
- } else if (item.variant === 'destructive') {
193
- notificationError()
194
- } else if (item.variant === 'warning') {
195
- notificationError()
196
- } else {
197
- impactLight()
198
- }
199
- setToasts((prev) => [{ ...item, id }, ...prev].slice(0, 3))
200
- }, [])
201
-
202
- const dismiss = useCallback((id: string) => {
203
- setToasts((prev) => prev.filter((t) => t.id !== id))
204
- }, [])
205
-
206
23
  return (
207
- <ToastContext.Provider value={{ toast, dismiss }}>
24
+ <>
208
25
  {children}
209
- <View style={[styles.container, Platform.OS === 'web' && styles.containerWeb, { top: insets.top + 8 }]} pointerEvents="box-none">
210
- {toasts.map((item) => (
211
- <ToastNotification key={item.id} item={item} onDismiss={() => dismiss(item.id)} />
212
- ))}
213
- </View>
214
- </ToastContext.Provider>
26
+ <Toaster
27
+ theme={colorScheme}
28
+ position="top-center"
29
+ richColors={false}
30
+ gap={vs(8)}
31
+ offset={insets.top + vs(8)}
32
+ visibleToasts={3}
33
+ closeButton={false}
34
+ swipeToDismissDirection="up"
35
+ duration={4000}
36
+ toastOptions={{
37
+ style: {
38
+ borderRadius: ms(12),
39
+ paddingHorizontal: s(12),
40
+ paddingVertical: vs(10),
41
+ },
42
+ titleStyle: {
43
+ fontFamily: 'Poppins-Medium',
44
+ fontSize: ms(13),
45
+ },
46
+ descriptionStyle: {
47
+ fontFamily: 'Poppins-Regular',
48
+ fontSize: ms(12),
49
+ opacity: 0.85,
50
+ },
51
+ }}
52
+ />
53
+ </>
215
54
  )
216
55
  }
217
-
218
- const styles = StyleSheet.create({
219
- container: {
220
- position: 'absolute',
221
- left: s(16),
222
- right: s(16),
223
- gap: vs(8),
224
- zIndex: 9999,
225
- },
226
- containerWeb: {
227
- left: undefined,
228
- right: undefined,
229
- alignSelf: 'center',
230
- width: s(400),
231
- },
232
- toast: {
233
- flexDirection: 'row',
234
- alignItems: 'flex-start',
235
- borderRadius: ms(10),
236
- borderWidth: 0.5,
237
- paddingHorizontal: s(12),
238
- paddingVertical: vs(10),
239
- shadowColor: '#000',
240
- shadowOffset: { width: 0, height: 2 },
241
- shadowOpacity: 0.06,
242
- shadowRadius: 4,
243
- elevation: 3,
244
- },
245
- toastContent: {
246
- flex: 1,
247
- gap: vs(2),
248
- },
249
- leftIconContainer: {
250
- marginTop: vs(1),
251
- alignItems: 'center',
252
- justifyContent: 'center',
253
- marginRight: s(10),
254
- },
255
- toastTitle: {
256
- fontFamily: 'Poppins-Medium',
257
- fontSize: ms(13),
258
- lineHeight: ms(18),
259
- },
260
- toastDescription: {
261
- fontFamily: 'Poppins-Regular',
262
- fontSize: ms(12),
263
- lineHeight: ms(17),
264
- opacity: 0.85,
265
- },
266
- actionButton: {
267
- paddingHorizontal: s(8),
268
- paddingVertical: vs(4),
269
- marginLeft: s(4),
270
- },
271
- actionLabel: {
272
- fontFamily: 'Poppins-Medium',
273
- fontSize: ms(12),
274
- textDecorationLine: 'underline',
275
- },
276
- dismissButton: {
277
- padding: s(6),
278
- marginLeft: s(2),
279
- marginTop: vs(0),
280
- },
281
- })
@@ -1,2 +1 @@
1
- export { ToastProvider, useToast } from './Toast'
2
- export type { ToastProviderProps, ToastItem, ToastVariant } from './Toast'
1
+ export { ToastProvider, useToast, toast } from './Toast'
@@ -77,11 +77,11 @@ export function Toggle({
77
77
 
78
78
  const handlePressIn = () => {
79
79
  if (disabled) return
80
- Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver, speed: 40, bounciness: 0 }).start()
80
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver, stiffness: 600, damping: 35, mass: 0.8 }).start()
81
81
  }
82
82
 
83
83
  const handlePressOut = () => {
84
- Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, speed: 40, bounciness: 4 }).start()
84
+ Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, stiffness: 280, damping: 22, mass: 0.8 }).start()
85
85
  }
86
86
 
87
87
  // Keep borderWidth constant at 2 to prevent layout jumps when pressing.
package/src/index.ts CHANGED
@@ -34,6 +34,7 @@ export * from './components/CurrencyDisplay'
34
34
  // CurrencyInputLarge is deprecated — use <CurrencyInput size="large" /> instead
35
35
  export { CurrencyInput as CurrencyInputLarge } from './components/CurrencyInput'
36
36
  export * from './components/ListItem'
37
+ export * from './components/MenuItem'
37
38
  export * from './components/Chip'
38
39
  export * from './components/ConfirmDialog'
39
40
  export * from './components/LabelValue'
@@ -89,6 +89,9 @@ export function deriveColors(t: ThemeColors, scheme: 'light' | 'dark'): Resolved
89
89
  successBorder,
90
90
  warningTint,
91
91
  warningBorder,
92
+ overlay: t.overlay ?? 'rgba(0,0,0,0.45)',
93
+ accentResolved: t.accent ?? t.primary,
94
+ accentForegroundResolved: t.accentForeground ?? t.primaryForeground,
92
95
  ring: t.primary, // focus ring always = primary
93
96
  input: t.border, // input border always = border
94
97
  }
@@ -14,6 +14,12 @@ export type ThemeColors = {
14
14
  successForeground: string
15
15
  warning: string
16
16
  warningForeground: string
17
+ /** Backdrop/overlay color. Default: 'rgba(0,0,0,0.45)' */
18
+ overlay?: string
19
+ /** Color accent (e.g. Airbnb coral). Default: same as primary */
20
+ accent?: string
21
+ /** Text color on accent background. Default: same as primaryForeground */
22
+ accentForeground?: string
17
23
  }
18
24
 
19
25
  // Full resolved palette — what components actually consume via useTheme().
@@ -35,6 +41,11 @@ export type ResolvedColors = ThemeColors & {
35
41
  warningTint: string
36
42
  warningBorder: string
37
43
 
44
+ // Derived optional tokens (always present in ResolvedColors)
45
+ overlay: string // backdrop — default 'rgba(0,0,0,0.45)'
46
+ accentResolved: string // accent — default = primary
47
+ accentForegroundResolved: string // text on accent — default = primaryForeground
48
+
38
49
  // Aliases (ring + input always equal primary + border for coherence)
39
50
  ring: string // = primary
40
51
  input: string // = border
package/src/tokens.ts CHANGED
@@ -170,10 +170,10 @@ export const TYPOGRAPHY = {
170
170
  },
171
171
  'uppercase-tag': {
172
172
  fontFamily: 'Poppins-Bold',
173
- fontSize: 8,
173
+ fontSize: 10,
174
174
  fontWeight: '700' as const,
175
- lineHeight: 10,
176
- letterSpacing: 0.32,
175
+ lineHeight: 13,
176
+ letterSpacing: 0.8,
177
177
  textTransform: 'uppercase' as const,
178
178
  },
179
179
  'button-lg': {