@retray-dev/ui-kit 0.1.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.
Files changed (56) hide show
  1. package/COMPONENTS.md +710 -0
  2. package/LICENSE +21 -0
  3. package/README.md +150 -0
  4. package/dist/index.d.mts +345 -4
  5. package/dist/index.d.ts +345 -4
  6. package/dist/index.js +1644 -58
  7. package/dist/index.mjs +1590 -58
  8. package/package.json +44 -5
  9. package/src/components/Accordion/Accordion.tsx +173 -0
  10. package/src/components/Accordion/index.ts +2 -0
  11. package/src/components/Alert/Alert.tsx +59 -0
  12. package/src/components/Alert/index.ts +2 -0
  13. package/src/components/Avatar/Avatar.tsx +71 -0
  14. package/src/components/Avatar/index.ts +2 -0
  15. package/src/components/Badge/Badge.tsx +48 -0
  16. package/src/components/Badge/index.ts +2 -0
  17. package/src/components/Button/Button.tsx +94 -45
  18. package/src/components/Card/Card.tsx +103 -0
  19. package/src/components/Card/index.ts +9 -0
  20. package/src/components/Checkbox/Checkbox.tsx +98 -0
  21. package/src/components/Checkbox/index.ts +2 -0
  22. package/src/components/EmptyState/EmptyState.tsx +67 -0
  23. package/src/components/EmptyState/index.ts +2 -0
  24. package/src/components/Input/Input.tsx +28 -35
  25. package/src/components/Progress/Progress.tsx +52 -0
  26. package/src/components/Progress/index.ts +2 -0
  27. package/src/components/RadioGroup/RadioGroup.tsx +132 -0
  28. package/src/components/RadioGroup/index.ts +2 -0
  29. package/src/components/Select/Select.tsx +232 -0
  30. package/src/components/Select/index.ts +2 -0
  31. package/src/components/Separator/Separator.tsx +33 -0
  32. package/src/components/Separator/index.ts +2 -0
  33. package/src/components/Sheet/Sheet.tsx +115 -0
  34. package/src/components/Sheet/index.ts +2 -0
  35. package/src/components/Skeleton/Skeleton.tsx +63 -0
  36. package/src/components/Skeleton/index.ts +2 -0
  37. package/src/components/Slider/Slider.tsx +143 -0
  38. package/src/components/Slider/index.ts +2 -0
  39. package/src/components/Spinner/Spinner.tsx +21 -0
  40. package/src/components/Spinner/index.ts +2 -0
  41. package/src/components/Switch/Switch.tsx +86 -0
  42. package/src/components/Switch/index.ts +2 -0
  43. package/src/components/Tabs/Tabs.tsx +196 -0
  44. package/src/components/Tabs/index.ts +2 -0
  45. package/src/components/Text/Text.tsx +10 -4
  46. package/src/components/Textarea/Textarea.tsx +89 -0
  47. package/src/components/Textarea/index.ts +2 -0
  48. package/src/components/Toast/Toast.tsx +200 -0
  49. package/src/components/Toast/index.ts +2 -0
  50. package/src/components/Toggle/Toggle.tsx +92 -0
  51. package/src/components/Toggle/index.ts +2 -0
  52. package/src/index.ts +26 -0
  53. package/src/theme/ThemeProvider.tsx +47 -0
  54. package/src/theme/colors.ts +45 -0
  55. package/src/theme/index.ts +4 -0
  56. package/src/theme/types.ts +33 -0
@@ -0,0 +1,86 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+ import { TouchableOpacity, Animated, StyleSheet, ViewStyle } from 'react-native'
3
+ import * as Haptics from 'expo-haptics'
4
+ import { useTheme } from '../../theme'
5
+
6
+ const TRACK_WIDTH = 56
7
+ const TRACK_HEIGHT = 32
8
+ const THUMB_SIZE = 24
9
+ const THUMB_OFFSET = 4
10
+ const THUMB_TRAVEL = TRACK_WIDTH - THUMB_SIZE - THUMB_OFFSET * 2
11
+
12
+ export interface SwitchProps {
13
+ checked?: boolean
14
+ onCheckedChange?: (checked: boolean) => void
15
+ disabled?: boolean
16
+ style?: ViewStyle
17
+ }
18
+
19
+ export function Switch({ checked = false, onCheckedChange, disabled, style }: SwitchProps) {
20
+ const { colors } = useTheme()
21
+ const translateX = useRef(new Animated.Value(checked ? THUMB_TRAVEL : 0)).current
22
+ const trackOpacity = useRef(new Animated.Value(checked ? 1 : 0)).current
23
+
24
+ useEffect(() => {
25
+ Animated.parallel([
26
+ Animated.spring(translateX, {
27
+ toValue: checked ? THUMB_TRAVEL : 0,
28
+ useNativeDriver: true,
29
+ bounciness: 4,
30
+ }),
31
+ Animated.timing(trackOpacity, {
32
+ toValue: checked ? 1 : 0,
33
+ duration: 150,
34
+ useNativeDriver: false,
35
+ }),
36
+ ]).start()
37
+ }, [checked, translateX, trackOpacity])
38
+
39
+ const trackColor = trackOpacity.interpolate({
40
+ inputRange: [0, 1],
41
+ outputRange: [colors.muted, colors.primary],
42
+ })
43
+
44
+ return (
45
+ <TouchableOpacity
46
+ onPress={() => {
47
+ Haptics.selectionAsync()
48
+ onCheckedChange?.(!checked)
49
+ }}
50
+ disabled={disabled}
51
+ activeOpacity={0.8}
52
+ touchSoundDisabled={true}
53
+ style={[styles.wrapper, { opacity: disabled ? 0.45 : 1 }, style]}
54
+ >
55
+ <Animated.View style={[styles.track, { backgroundColor: trackColor }]}>
56
+ <Animated.View
57
+ style={[
58
+ styles.thumb,
59
+ { backgroundColor: colors.primaryForeground, transform: [{ translateX }] },
60
+ ]}
61
+ />
62
+ </Animated.View>
63
+ </TouchableOpacity>
64
+ )
65
+ }
66
+
67
+ const styles = StyleSheet.create({
68
+ wrapper: {},
69
+ track: {
70
+ width: TRACK_WIDTH,
71
+ height: TRACK_HEIGHT,
72
+ borderRadius: TRACK_HEIGHT / 2,
73
+ justifyContent: 'center',
74
+ paddingHorizontal: THUMB_OFFSET,
75
+ },
76
+ thumb: {
77
+ width: THUMB_SIZE,
78
+ height: THUMB_SIZE,
79
+ borderRadius: THUMB_SIZE / 2,
80
+ shadowColor: '#000',
81
+ shadowOffset: { width: 0, height: 1 },
82
+ shadowOpacity: 0.15,
83
+ shadowRadius: 2,
84
+ elevation: 2,
85
+ },
86
+ })
@@ -0,0 +1,2 @@
1
+ export { Switch } from './Switch'
2
+ export type { SwitchProps } from './Switch'
@@ -0,0 +1,196 @@
1
+ import React, { useState, useRef, useEffect } from 'react'
2
+ import { View, TouchableOpacity, Text, Animated, StyleSheet, ViewStyle } from 'react-native'
3
+ import * as Haptics from 'expo-haptics'
4
+ import { useTheme } from '../../theme'
5
+
6
+ export interface TabItem {
7
+ label: string
8
+ value: string
9
+ }
10
+
11
+ export interface TabsProps {
12
+ tabs: TabItem[]
13
+ /**
14
+ * Controlled active tab value. When omitted the component manages state internally
15
+ * (uncontrolled), defaulting to the first tab.
16
+ */
17
+ value?: string
18
+ onValueChange?: (value: string) => void
19
+ children?: React.ReactNode
20
+ style?: ViewStyle
21
+ }
22
+
23
+ export interface TabsContentProps {
24
+ value: string
25
+ activeValue: string
26
+ children: React.ReactNode
27
+ style?: ViewStyle
28
+ }
29
+
30
+ function TabTrigger({
31
+ tab,
32
+ isActive,
33
+ onPress,
34
+ onLayout,
35
+ }: {
36
+ tab: TabItem
37
+ isActive: boolean
38
+ onPress: () => void
39
+ onLayout: (e: any) => void
40
+ }) {
41
+ const { colors } = useTheme()
42
+ const scale = useRef(new Animated.Value(1)).current
43
+
44
+ const handlePressIn = () => {
45
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
46
+ }
47
+
48
+ const handlePressOut = () => {
49
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
50
+ }
51
+
52
+ return (
53
+ <TouchableOpacity
54
+ style={styles.trigger}
55
+ onPress={onPress}
56
+ onPressIn={handlePressIn}
57
+ onPressOut={handlePressOut}
58
+ onLayout={onLayout}
59
+ activeOpacity={1}
60
+ touchSoundDisabled={true}
61
+ >
62
+ <Animated.View style={{ transform: [{ scale }] }}>
63
+ <Text
64
+ style={[
65
+ styles.triggerLabel,
66
+ { color: isActive ? colors.foreground : colors.mutedForeground },
67
+ isActive && styles.activeTriggerLabel,
68
+ ]}
69
+ >
70
+ {tab.label}
71
+ </Text>
72
+ </Animated.View>
73
+ </TouchableOpacity>
74
+ )
75
+ }
76
+
77
+ export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps) {
78
+ const [internal, setInternal] = useState(tabs[0]?.value ?? '')
79
+ const { colors } = useTheme()
80
+ const active = value ?? internal
81
+
82
+ const tabLayouts = useRef<Record<string, { x: number; width: number }>>({})
83
+ const pillX = useRef(new Animated.Value(0)).current
84
+ const pillWidth = useRef(new Animated.Value(0)).current
85
+ const initialised = useRef(false)
86
+
87
+ const animatePill = (tabValue: string, animate: boolean) => {
88
+ const layout = tabLayouts.current[tabValue]
89
+ if (!layout) return
90
+ if (animate) {
91
+ Animated.parallel([
92
+ Animated.spring(pillX, {
93
+ toValue: layout.x,
94
+ useNativeDriver: false,
95
+ speed: 20,
96
+ bounciness: 0,
97
+ }),
98
+ Animated.spring(pillWidth, {
99
+ toValue: layout.width,
100
+ useNativeDriver: false,
101
+ speed: 20,
102
+ bounciness: 0,
103
+ }),
104
+ ]).start()
105
+ } else {
106
+ pillX.setValue(layout.x)
107
+ pillWidth.setValue(layout.width)
108
+ }
109
+ }
110
+
111
+ useEffect(() => {
112
+ if (initialised.current) {
113
+ animatePill(active, true)
114
+ }
115
+ }, [active])
116
+
117
+ const handlePress = (v: string) => {
118
+ Haptics.selectionAsync()
119
+ if (!value) setInternal(v)
120
+ onValueChange?.(v)
121
+ }
122
+
123
+ return (
124
+ <View style={style}>
125
+ <View style={[styles.list, { backgroundColor: colors.muted }]}>
126
+ <Animated.View
127
+ style={[
128
+ styles.pill,
129
+ {
130
+ backgroundColor: colors.background,
131
+ position: 'absolute',
132
+ top: 4,
133
+ bottom: 4,
134
+ left: pillX,
135
+ width: pillWidth,
136
+ borderRadius: 6,
137
+ shadowColor: '#000',
138
+ shadowOffset: { width: 0, height: 1 },
139
+ shadowOpacity: 0.1,
140
+ shadowRadius: 2,
141
+ elevation: 2,
142
+ },
143
+ ]}
144
+ />
145
+ {tabs.map((tab) => (
146
+ <TabTrigger
147
+ key={tab.value}
148
+ tab={tab}
149
+ isActive={tab.value === active}
150
+ onPress={() => handlePress(tab.value)}
151
+ onLayout={(e) => {
152
+ const { x, width } = e.nativeEvent.layout
153
+ tabLayouts.current[tab.value] = { x, width }
154
+ if (tab.value === active) {
155
+ animatePill(tab.value, false)
156
+ initialised.current = true
157
+ }
158
+ }}
159
+ />
160
+ ))}
161
+ </View>
162
+ {children}
163
+ </View>
164
+ )
165
+ }
166
+
167
+ export function TabsContent({ value, activeValue, children, style }: TabsContentProps) {
168
+ if (value !== activeValue) return null
169
+ return <View style={style}>{children}</View>
170
+ }
171
+
172
+ const styles = StyleSheet.create({
173
+ list: {
174
+ flexDirection: 'row',
175
+ borderRadius: 8,
176
+ padding: 4,
177
+ gap: 4,
178
+ },
179
+ pill: {},
180
+ trigger: {
181
+ flex: 1,
182
+ paddingVertical: 8,
183
+ paddingHorizontal: 12,
184
+ borderRadius: 6,
185
+ alignItems: 'center',
186
+ justifyContent: 'center',
187
+ zIndex: 1,
188
+ },
189
+ triggerLabel: {
190
+ fontSize: 14,
191
+ fontWeight: '400',
192
+ },
193
+ activeTriggerLabel: {
194
+ fontWeight: '500',
195
+ },
196
+ })
@@ -0,0 +1,2 @@
1
+ export { Tabs, TabsContent } from './Tabs'
2
+ export type { TabsProps, TabsContentProps, TabItem } from './Tabs'
@@ -1,5 +1,6 @@
1
1
  import React from 'react'
2
- import { Text as RNText, StyleSheet, TextProps as RNTextProps, TextStyle } from 'react-native'
2
+ import { Text as RNText, TextProps as RNTextProps, TextStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
3
4
 
4
5
  export type TextVariant = 'h1' | 'h2' | 'h3' | 'body' | 'caption' | 'label'
5
6
 
@@ -9,18 +10,23 @@ export interface TextProps extends RNTextProps {
9
10
  }
10
11
 
11
12
  const variantStyles: Record<TextVariant, TextStyle> = {
12
- h1: { fontSize: 32, fontWeight: '700', lineHeight: 40 },
13
+ h1: { fontSize: 32, fontWeight: '700', lineHeight: 44 },
13
14
  h2: { fontSize: 24, fontWeight: '700', lineHeight: 32 },
14
15
  h3: { fontSize: 20, fontWeight: '600', lineHeight: 28 },
15
16
  body: { fontSize: 16, fontWeight: '400', lineHeight: 24 },
16
- caption: { fontSize: 12, fontWeight: '400', lineHeight: 18, color: '#6B7280' },
17
+ caption: { fontSize: 12, fontWeight: '400', lineHeight: 18 },
17
18
  label: { fontSize: 14, fontWeight: '500', lineHeight: 20 },
18
19
  }
19
20
 
20
21
  export function Text({ variant = 'body', color, style, children, ...props }: TextProps) {
22
+ const { colors } = useTheme()
23
+
24
+ const defaultColor = variant === 'caption' ? colors.mutedForeground : colors.foreground
25
+
21
26
  return (
22
27
  <RNText
23
- style={[variantStyles[variant], color ? { color } : undefined, style]}
28
+ style={[variantStyles[variant], { color: color ?? defaultColor }, style]}
29
+ allowFontScaling={true}
24
30
  {...props}
25
31
  >
26
32
  {children}
@@ -0,0 +1,89 @@
1
+ import React, { useState } from 'react'
2
+ import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export interface TextareaProps extends TextInputProps {
6
+ label?: string
7
+ /** Red helper text below the textarea; also changes border to `destructive` color. Takes priority over `hint`. */
8
+ error?: string
9
+ /** Helper text shown below the textarea when there is no error. */
10
+ hint?: string
11
+ /** Number of visible text rows. Defaults to `4`. Controls `numberOfLines` and `minHeight`. */
12
+ rows?: number
13
+ /** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
14
+ containerStyle?: ViewStyle
15
+ }
16
+
17
+ export function Textarea({
18
+ label,
19
+ error,
20
+ hint,
21
+ rows = 4,
22
+ containerStyle,
23
+ style,
24
+ onFocus,
25
+ onBlur,
26
+ ...props
27
+ }: TextareaProps) {
28
+ const { colors } = useTheme()
29
+ const [focused, setFocused] = useState(false)
30
+
31
+ return (
32
+ <View style={[styles.container, containerStyle]}>
33
+ {label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
34
+ <TextInput
35
+ multiline
36
+ numberOfLines={rows}
37
+ textAlignVertical="top"
38
+ style={[
39
+ styles.input,
40
+ {
41
+ borderColor: error ? colors.destructive : focused ? colors.ring : colors.border,
42
+ color: colors.foreground,
43
+ backgroundColor: colors.background,
44
+ minHeight: rows * 28,
45
+ },
46
+ style,
47
+ ]}
48
+ onFocus={(e) => {
49
+ setFocused(true)
50
+ onFocus?.(e)
51
+ }}
52
+ onBlur={(e) => {
53
+ setFocused(false)
54
+ onBlur?.(e)
55
+ }}
56
+ placeholderTextColor={colors.mutedForeground}
57
+ allowFontScaling={true}
58
+ {...props}
59
+ />
60
+ {error ? (
61
+ <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
62
+ ) : null}
63
+ {!error && hint ? (
64
+ <Text style={[styles.helperText, { color: colors.mutedForeground }]} allowFontScaling={true}>{hint}</Text>
65
+ ) : null}
66
+ </View>
67
+ )
68
+ }
69
+
70
+ const styles = StyleSheet.create({
71
+ container: {
72
+ gap: 4,
73
+ },
74
+ label: {
75
+ fontSize: 14,
76
+ fontWeight: '500',
77
+ marginBottom: 4,
78
+ },
79
+ input: {
80
+ borderWidth: 1.5,
81
+ borderRadius: 8,
82
+ paddingHorizontal: 16,
83
+ paddingVertical: 14,
84
+ fontSize: 16,
85
+ },
86
+ helperText: {
87
+ fontSize: 12,
88
+ },
89
+ })
@@ -0,0 +1,2 @@
1
+ export { Textarea } from './Textarea'
2
+ export type { TextareaProps } from './Textarea'
@@ -0,0 +1,200 @@
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'
12
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
13
+ import * as Haptics from 'expo-haptics'
14
+ import { useTheme } from '../../theme'
15
+
16
+ export type ToastVariant = 'default' | 'destructive' | 'success'
17
+
18
+ export interface ToastItem {
19
+ id: string
20
+ title?: string
21
+ description?: string
22
+ variant?: ToastVariant
23
+ /** Auto-dismiss delay in milliseconds. Defaults to `3000`. */
24
+ duration?: number
25
+ }
26
+
27
+ interface ToastContextValue {
28
+ toast: (item: Omit<ToastItem, 'id'>) => void
29
+ dismiss: (id: string) => void
30
+ }
31
+
32
+ const ToastContext = createContext<ToastContextValue>({
33
+ toast: () => {},
34
+ dismiss: () => {},
35
+ })
36
+
37
+ export function useToast() {
38
+ return useContext(ToastContext)
39
+ }
40
+
41
+ const SWIPE_THRESHOLD = 80
42
+ const VELOCITY_THRESHOLD = 800
43
+
44
+ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: () => void }) {
45
+ const { colors } = useTheme()
46
+ const translateY = useSharedValue(-80)
47
+ const translateX = useSharedValue(0)
48
+ const opacity = useSharedValue(0)
49
+
50
+ useEffect(() => {
51
+ translateY.value = withTiming(0, { duration: 120, easing: Easing.out(Easing.exp) })
52
+ opacity.value = withTiming(1, { duration: 100 })
53
+
54
+ const timer = setTimeout(() => {
55
+ translateY.value = withTiming(-80, { duration: 200 })
56
+ opacity.value = withTiming(0, { duration: 200 }, (done) => {
57
+ if (done) runOnJS(onDismiss)()
58
+ })
59
+ }, item.duration ?? 3000)
60
+
61
+ return () => clearTimeout(timer)
62
+ }, [])
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
+
88
+ const bgColor = {
89
+ default: colors.foreground,
90
+ destructive: colors.destructive,
91
+ success: colors.success,
92
+ }[item.variant ?? 'default']
93
+
94
+ const textColor = {
95
+ default: colors.background,
96
+ destructive: colors.destructiveForeground,
97
+ success: colors.successForeground,
98
+ }[item.variant ?? 'default']
99
+
100
+ return (
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>
118
+ )
119
+ }
120
+
121
+ /**
122
+ * Must wrap the app root alongside ThemeProvider.
123
+ * Renders toasts in an absolute overlay at the top of the screen.
124
+ * Use `useToast()` anywhere inside to trigger toasts.
125
+ */
126
+ export interface ToastProviderProps {
127
+ children: React.ReactNode
128
+ }
129
+
130
+ export function ToastProvider({ children }: ToastProviderProps) {
131
+ const [toasts, setToasts] = useState<ToastItem[]>([])
132
+ const insets = useSafeAreaInsets()
133
+
134
+ const toast = useCallback((item: Omit<ToastItem, 'id'>) => {
135
+ const id = Math.random().toString(36).slice(2)
136
+ if (item.variant === 'success') {
137
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
138
+ } else if (item.variant === 'destructive') {
139
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)
140
+ } else {
141
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
142
+ }
143
+ setToasts((prev) => [{ ...item, id }, ...prev].slice(0, 3))
144
+ }, [])
145
+
146
+ const dismiss = useCallback((id: string) => {
147
+ setToasts((prev) => prev.filter((t) => t.id !== id))
148
+ }, [])
149
+
150
+ return (
151
+ <ToastContext.Provider value={{ toast, dismiss }}>
152
+ {children}
153
+ <View style={[styles.container, { top: insets.top + 8 }]} pointerEvents="box-none">
154
+ {toasts.map((item) => (
155
+ <ToastNotification key={item.id} item={item} onDismiss={() => dismiss(item.id)} />
156
+ ))}
157
+ </View>
158
+ </ToastContext.Provider>
159
+ )
160
+ }
161
+
162
+ const styles = StyleSheet.create({
163
+ container: {
164
+ position: 'absolute',
165
+ left: 16,
166
+ right: 16,
167
+ gap: 8,
168
+ zIndex: 9999,
169
+ },
170
+ toast: {
171
+ flexDirection: 'row',
172
+ alignItems: 'center',
173
+ borderRadius: 12,
174
+ paddingHorizontal: 16,
175
+ paddingVertical: 12,
176
+ shadowColor: '#000',
177
+ shadowOffset: { width: 0, height: 4 },
178
+ shadowOpacity: 0.15,
179
+ shadowRadius: 8,
180
+ elevation: 6,
181
+ },
182
+ toastContent: {
183
+ flex: 1,
184
+ gap: 4,
185
+ },
186
+ toastTitle: {
187
+ fontSize: 14,
188
+ fontWeight: '600',
189
+ },
190
+ toastDescription: {
191
+ fontSize: 13,
192
+ },
193
+ dismissButton: {
194
+ padding: 12,
195
+ marginLeft: 4,
196
+ },
197
+ dismissIcon: {
198
+ fontSize: 12,
199
+ },
200
+ })
@@ -0,0 +1,2 @@
1
+ export { ToastProvider, useToast } from './Toast'
2
+ export type { ToastProviderProps, ToastItem, ToastVariant } from './Toast'