@retray-dev/ui-kit 0.1.0 → 1.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.
Files changed (56) hide show
  1. package/COMPONENTS.md +654 -0
  2. package/LICENSE +21 -0
  3. package/README.md +151 -0
  4. package/dist/index.d.mts +309 -3
  5. package/dist/index.d.ts +309 -3
  6. package/dist/index.js +1477 -57
  7. package/dist/index.mjs +1424 -57
  8. package/package.json +27 -5
  9. package/src/components/Accordion/Accordion.tsx +161 -0
  10. package/src/components/Accordion/index.ts +2 -0
  11. package/src/components/Alert/Alert.tsx +57 -0
  12. package/src/components/Alert/index.ts +2 -0
  13. package/src/components/Avatar/Avatar.tsx +67 -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 +78 -45
  18. package/src/components/Card/Card.tsx +109 -0
  19. package/src/components/Card/index.ts +9 -0
  20. package/src/components/Checkbox/Checkbox.tsx +70 -0
  21. package/src/components/Checkbox/index.ts +2 -0
  22. package/src/components/EmptyState/EmptyState.tsx +69 -0
  23. package/src/components/EmptyState/index.ts +2 -0
  24. package/src/components/Input/Input.tsx +26 -41
  25. package/src/components/Progress/Progress.tsx +53 -0
  26. package/src/components/Progress/index.ts +2 -0
  27. package/src/components/RadioGroup/RadioGroup.tsx +105 -0
  28. package/src/components/RadioGroup/index.ts +2 -0
  29. package/src/components/Select/Select.tsx +185 -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 +108 -0
  34. package/src/components/Sheet/index.ts +2 -0
  35. package/src/components/Skeleton/Skeleton.tsx +40 -0
  36. package/src/components/Skeleton/index.ts +2 -0
  37. package/src/components/Slider/Slider.tsx +142 -0
  38. package/src/components/Slider/index.ts +2 -0
  39. package/src/components/Spinner/Spinner.tsx +27 -0
  40. package/src/components/Spinner/index.ts +2 -0
  41. package/src/components/Switch/Switch.tsx +82 -0
  42. package/src/components/Switch/index.ts +2 -0
  43. package/src/components/Tabs/Tabs.tsx +145 -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 +70 -0
  47. package/src/components/Textarea/index.ts +2 -0
  48. package/src/components/Toast/Toast.tsx +164 -0
  49. package/src/components/Toast/index.ts +2 -0
  50. package/src/components/Toggle/Toggle.tsx +80 -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 +41 -0
  55. package/src/theme/index.ts +4 -0
  56. package/src/theme/types.ts +31 -0
@@ -0,0 +1,164 @@
1
+ import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react'
2
+ import { View, Text, TouchableOpacity, Animated, StyleSheet } from 'react-native'
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
4
+ import * as Haptics from 'expo-haptics'
5
+ import { useTheme } from '../../theme'
6
+
7
+ export type ToastVariant = 'default' | 'destructive' | 'success'
8
+
9
+ export interface ToastItem {
10
+ id: string
11
+ title?: string
12
+ description?: string
13
+ variant?: ToastVariant
14
+ duration?: number
15
+ }
16
+
17
+ interface ToastContextValue {
18
+ toast: (item: Omit<ToastItem, 'id'>) => void
19
+ dismiss: (id: string) => void
20
+ }
21
+
22
+ const ToastContext = createContext<ToastContextValue>({
23
+ toast: () => {},
24
+ dismiss: () => {},
25
+ })
26
+
27
+ export function useToast() {
28
+ return useContext(ToastContext)
29
+ }
30
+
31
+ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: () => void }) {
32
+ const { colors } = useTheme()
33
+ const translateY = useRef(new Animated.Value(-80)).current
34
+ const opacity = useRef(new Animated.Value(0)).current
35
+
36
+ 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()
41
+
42
+ 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)
47
+ }, item.duration ?? 3000)
48
+
49
+ return () => clearTimeout(timer)
50
+ }, [])
51
+
52
+ const bgColor = {
53
+ default: colors.foreground,
54
+ destructive: colors.destructive,
55
+ success: '#16a34a',
56
+ }[item.variant ?? 'default']
57
+
58
+ const textColor = {
59
+ default: colors.background,
60
+ destructive: colors.destructiveForeground,
61
+ success: '#ffffff',
62
+ }[item.variant ?? 'default']
63
+
64
+ 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>
82
+ )
83
+ }
84
+
85
+ /**
86
+ * Must wrap the app root alongside ThemeProvider.
87
+ * Renders toasts in an absolute overlay at the top of the screen.
88
+ * Use `useToast()` anywhere inside to trigger toasts.
89
+ */
90
+ export interface ToastProviderProps {
91
+ children: React.ReactNode
92
+ }
93
+
94
+ export function ToastProvider({ children }: ToastProviderProps) {
95
+ const [toasts, setToasts] = useState<ToastItem[]>([])
96
+ const insets = useSafeAreaInsets()
97
+
98
+ const toast = useCallback((item: Omit<ToastItem, 'id'>) => {
99
+ const id = Math.random().toString(36).slice(2)
100
+ if (item.variant === 'success') {
101
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
102
+ } else if (item.variant === 'destructive') {
103
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)
104
+ } else {
105
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
106
+ }
107
+ setToasts((prev) => [{ ...item, id }, ...prev].slice(0, 3))
108
+ }, [])
109
+
110
+ const dismiss = useCallback((id: string) => {
111
+ setToasts((prev) => prev.filter((t) => t.id !== id))
112
+ }, [])
113
+
114
+ return (
115
+ <ToastContext.Provider value={{ toast, dismiss }}>
116
+ {children}
117
+ <View style={[styles.container, { top: insets.top + 8 }]} pointerEvents="box-none">
118
+ {toasts.map((item) => (
119
+ <ToastNotification key={item.id} item={item} onDismiss={() => dismiss(item.id)} />
120
+ ))}
121
+ </View>
122
+ </ToastContext.Provider>
123
+ )
124
+ }
125
+
126
+ const styles = StyleSheet.create({
127
+ container: {
128
+ position: 'absolute',
129
+ left: 16,
130
+ right: 16,
131
+ gap: 8,
132
+ zIndex: 9999,
133
+ },
134
+ toast: {
135
+ flexDirection: 'row',
136
+ alignItems: 'center',
137
+ borderRadius: 12,
138
+ paddingHorizontal: 16,
139
+ paddingVertical: 12,
140
+ shadowColor: '#000',
141
+ shadowOffset: { width: 0, height: 4 },
142
+ shadowOpacity: 0.15,
143
+ shadowRadius: 8,
144
+ elevation: 6,
145
+ },
146
+ toastContent: {
147
+ flex: 1,
148
+ gap: 4,
149
+ },
150
+ toastTitle: {
151
+ fontSize: 14,
152
+ fontWeight: '600',
153
+ },
154
+ toastDescription: {
155
+ fontSize: 13,
156
+ },
157
+ dismissButton: {
158
+ padding: 4,
159
+ marginLeft: 8,
160
+ },
161
+ dismissIcon: {
162
+ fontSize: 12,
163
+ },
164
+ })
@@ -0,0 +1,2 @@
1
+ export { ToastProvider, useToast } from './Toast'
2
+ export type { ToastProviderProps, ToastItem, ToastVariant } from './Toast'
@@ -0,0 +1,80 @@
1
+ import React from 'react'
2
+ import { TouchableOpacity, Text, StyleSheet, TouchableOpacityProps, ViewStyle } from 'react-native'
3
+ import * as Haptics from 'expo-haptics'
4
+ import { useTheme } from '../../theme'
5
+
6
+ export type ToggleVariant = 'default' | 'outline'
7
+ export type ToggleSize = 'sm' | 'md' | 'lg'
8
+
9
+ export interface ToggleProps extends TouchableOpacityProps {
10
+ pressed?: boolean
11
+ onPressedChange?: (pressed: boolean) => void
12
+ variant?: ToggleVariant
13
+ size?: ToggleSize
14
+ label?: string
15
+ icon?: React.ReactNode
16
+ }
17
+
18
+ const sizeStyles: Record<ToggleSize, ViewStyle> = {
19
+ sm: { paddingHorizontal: 12, paddingVertical: 8, minWidth: 40, minHeight: 40 },
20
+ md: { paddingHorizontal: 16, paddingVertical: 12, minWidth: 44, minHeight: 44 },
21
+ lg: { paddingHorizontal: 20, paddingVertical: 14, minWidth: 48, minHeight: 48 },
22
+ }
23
+
24
+ export function Toggle({
25
+ pressed = false,
26
+ onPressedChange,
27
+ variant = 'default',
28
+ size = 'md',
29
+ label,
30
+ icon,
31
+ disabled,
32
+ style,
33
+ ...props
34
+ }: ToggleProps) {
35
+ const { colors } = useTheme()
36
+
37
+ const containerStyle: ViewStyle = pressed
38
+ ? { backgroundColor: colors.accent }
39
+ : variant === 'outline'
40
+ ? { backgroundColor: 'transparent', borderWidth: 1, borderColor: colors.border }
41
+ : { backgroundColor: 'transparent' }
42
+
43
+ const textColor = pressed ? colors.accentForeground : colors.foreground
44
+
45
+ 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>
62
+ )
63
+ }
64
+
65
+ const styles = StyleSheet.create({
66
+ base: {
67
+ borderRadius: 8,
68
+ flexDirection: 'row',
69
+ alignItems: 'center',
70
+ justifyContent: 'center',
71
+ gap: 8,
72
+ },
73
+ disabled: {
74
+ opacity: 0.45,
75
+ },
76
+ label: {
77
+ fontSize: 14,
78
+ fontWeight: '500',
79
+ },
80
+ })
@@ -0,0 +1,2 @@
1
+ export { Toggle } from './Toggle'
2
+ export type { ToggleProps, ToggleVariant, ToggleSize } from './Toggle'
package/src/index.ts CHANGED
@@ -1,3 +1,29 @@
1
+ // Theme
2
+ export { ThemeProvider, useTheme } from './theme'
3
+ export type { ThemeProviderProps, ThemeColors, Theme, ColorScheme } from './theme'
4
+ export { defaultLight, defaultDark } from './theme'
5
+
6
+ // Components
1
7
  export * from './components/Button'
2
8
  export * from './components/Text'
3
9
  export * from './components/Input'
10
+ export * from './components/Badge'
11
+ export * from './components/Card'
12
+ export * from './components/Separator'
13
+ export * from './components/Spinner'
14
+ export * from './components/Skeleton'
15
+ export * from './components/Avatar'
16
+ export * from './components/Alert'
17
+ export * from './components/Progress'
18
+ export * from './components/EmptyState'
19
+ export * from './components/Textarea'
20
+ export * from './components/Checkbox'
21
+ export * from './components/Switch'
22
+ export * from './components/Toggle'
23
+ export * from './components/RadioGroup'
24
+ export * from './components/Tabs'
25
+ export * from './components/Accordion'
26
+ export * from './components/Slider'
27
+ export * from './components/Sheet'
28
+ export * from './components/Select'
29
+ export * from './components/Toast'
@@ -0,0 +1,47 @@
1
+ import React, { createContext, useContext, useMemo } from 'react'
2
+ import { useColorScheme } from 'react-native'
3
+ import { ThemeColors, Theme, ColorScheme, ThemeContextValue } from './types'
4
+ import { defaultLight, defaultDark } from './colors'
5
+
6
+ const ThemeContext = createContext<ThemeContextValue>({
7
+ colors: defaultLight,
8
+ colorScheme: 'light',
9
+ })
10
+
11
+ export interface ThemeProviderProps {
12
+ children: React.ReactNode
13
+ /**
14
+ * Override individual color tokens per scheme. Only provide the tokens you want
15
+ * to change — the rest fall back to the defaults.
16
+ * @example
17
+ * { light: { primary: '#6366f1', primaryForeground: '#fff' },
18
+ * dark: { primary: '#818cf8', primaryForeground: '#fff' } }
19
+ */
20
+ theme?: Theme
21
+ /**
22
+ * - `'system'` (default): auto-detects device setting and updates when it changes.
23
+ * - `'light'` / `'dark'`: forces a specific scheme regardless of device setting.
24
+ */
25
+ colorScheme?: ColorScheme
26
+ }
27
+
28
+ export function ThemeProvider({ children, theme, colorScheme = 'system' }: ThemeProviderProps) {
29
+ const systemScheme = useColorScheme() ?? 'light'
30
+ const resolvedScheme: 'light' | 'dark' = colorScheme === 'system' ? systemScheme : colorScheme
31
+
32
+ const colors = useMemo<ThemeColors>(() => {
33
+ const base = resolvedScheme === 'dark' ? defaultDark : defaultLight
34
+ const overrides = resolvedScheme === 'dark' ? theme?.dark : theme?.light
35
+ return { ...base, ...overrides }
36
+ }, [resolvedScheme, theme])
37
+
38
+ return (
39
+ <ThemeContext.Provider value={{ colors, colorScheme: resolvedScheme }}>
40
+ {children}
41
+ </ThemeContext.Provider>
42
+ )
43
+ }
44
+
45
+ export function useTheme(): ThemeContextValue {
46
+ return useContext(ThemeContext)
47
+ }
@@ -0,0 +1,41 @@
1
+ import { ThemeColors } from './types'
2
+
3
+ export const defaultLight: ThemeColors = {
4
+ background: '#ffffff',
5
+ foreground: '#171717',
6
+ card: '#ffffff',
7
+ cardForeground: '#171717',
8
+ primary: '#1a1a1a',
9
+ primaryForeground: '#fafafa',
10
+ secondary: '#f5f5f5',
11
+ secondaryForeground: '#1a1a1a',
12
+ muted: '#f5f5f5',
13
+ mutedForeground: '#646464',
14
+ accent: '#f5f5f5',
15
+ accentForeground: '#1a1a1a',
16
+ destructive: '#ef4444',
17
+ destructiveForeground: '#fafafa',
18
+ border: '#e5e5e5',
19
+ input: '#e5e5e5',
20
+ ring: '#a3a3a3',
21
+ }
22
+
23
+ export const defaultDark: ThemeColors = {
24
+ background: '#171717',
25
+ foreground: '#fafafa',
26
+ card: '#1f1f1f',
27
+ cardForeground: '#fafafa',
28
+ primary: '#fafafa',
29
+ primaryForeground: '#1a1a1a',
30
+ secondary: '#2a2a2a',
31
+ secondaryForeground: '#fafafa',
32
+ muted: '#2a2a2a',
33
+ mutedForeground: '#a3a3a3',
34
+ accent: '#2a2a2a',
35
+ accentForeground: '#fafafa',
36
+ destructive: '#dc2626',
37
+ destructiveForeground: '#fafafa',
38
+ border: '#2a2a2a',
39
+ input: '#2a2a2a',
40
+ ring: '#d4d4d4',
41
+ }
@@ -0,0 +1,4 @@
1
+ export { ThemeProvider, useTheme } from './ThemeProvider'
2
+ export type { ThemeProviderProps } from './ThemeProvider'
3
+ export type { ThemeColors, Theme, ColorScheme } from './types'
4
+ export { defaultLight, defaultDark } from './colors'
@@ -0,0 +1,31 @@
1
+ export type ThemeColors = {
2
+ background: string
3
+ foreground: string
4
+ card: string
5
+ cardForeground: string
6
+ primary: string
7
+ primaryForeground: string
8
+ secondary: string
9
+ secondaryForeground: string
10
+ muted: string
11
+ mutedForeground: string
12
+ accent: string
13
+ accentForeground: string
14
+ destructive: string
15
+ destructiveForeground: string
16
+ border: string
17
+ input: string
18
+ ring: string
19
+ }
20
+
21
+ export type Theme = {
22
+ light?: Partial<ThemeColors>
23
+ dark?: Partial<ThemeColors>
24
+ }
25
+
26
+ export type ColorScheme = 'light' | 'dark' | 'system'
27
+
28
+ export type ThemeContextValue = {
29
+ colors: ThemeColors
30
+ colorScheme: 'light' | 'dark'
31
+ }