@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,103 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export interface CardProps {
6
+ children: React.ReactNode
7
+ style?: ViewStyle
8
+ }
9
+
10
+ export interface CardHeaderProps {
11
+ children: React.ReactNode
12
+ style?: ViewStyle
13
+ }
14
+
15
+ export interface CardTitleProps {
16
+ children: React.ReactNode
17
+ style?: TextStyle
18
+ }
19
+
20
+ export interface CardDescriptionProps {
21
+ children: React.ReactNode
22
+ style?: TextStyle
23
+ }
24
+
25
+ export interface CardContentProps {
26
+ children: React.ReactNode
27
+ style?: ViewStyle
28
+ }
29
+
30
+ export interface CardFooterProps {
31
+ children: React.ReactNode
32
+ style?: ViewStyle
33
+ }
34
+
35
+ export function Card({ children, style }: CardProps) {
36
+ const { colors } = useTheme()
37
+ return (
38
+ <View
39
+ style={[styles.card, { backgroundColor: colors.card, borderColor: colors.border }, style]}
40
+ >
41
+ {children}
42
+ </View>
43
+ )
44
+ }
45
+
46
+ export function CardHeader({ children, style }: CardHeaderProps) {
47
+ return <View style={[styles.header, style]}>{children}</View>
48
+ }
49
+
50
+ export function CardTitle({ children, style }: CardTitleProps) {
51
+ const { colors } = useTheme()
52
+ return <Text style={[styles.title, { color: colors.cardForeground }, style]}>{children}</Text>
53
+ }
54
+
55
+ export function CardDescription({ children, style }: CardDescriptionProps) {
56
+ const { colors } = useTheme()
57
+ return (
58
+ <Text style={[styles.description, { color: colors.mutedForeground }, style]}>{children}</Text>
59
+ )
60
+ }
61
+
62
+ export function CardContent({ children, style }: CardContentProps) {
63
+ return <View style={[styles.content, style]}>{children}</View>
64
+ }
65
+
66
+ export function CardFooter({ children, style }: CardFooterProps) {
67
+ return <View style={[styles.footer, style]}>{children}</View>
68
+ }
69
+
70
+ const styles = StyleSheet.create({
71
+ card: {
72
+ borderRadius: 12,
73
+ borderWidth: 1,
74
+ shadowColor: '#000',
75
+ shadowOffset: { width: 0, height: 1 },
76
+ shadowOpacity: 0.05,
77
+ shadowRadius: 2,
78
+ elevation: 1,
79
+ },
80
+ header: {
81
+ padding: 24,
82
+ paddingBottom: 0,
83
+ gap: 6,
84
+ },
85
+ title: {
86
+ fontSize: 18,
87
+ fontWeight: '600',
88
+ lineHeight: 24,
89
+ },
90
+ description: {
91
+ fontSize: 14,
92
+ lineHeight: 20,
93
+ },
94
+ content: {
95
+ padding: 24,
96
+ },
97
+ footer: {
98
+ padding: 24,
99
+ paddingTop: 0,
100
+ flexDirection: 'row',
101
+ alignItems: 'center',
102
+ },
103
+ })
@@ -0,0 +1,9 @@
1
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'
2
+ export type {
3
+ CardProps,
4
+ CardHeaderProps,
5
+ CardTitleProps,
6
+ CardDescriptionProps,
7
+ CardContentProps,
8
+ CardFooterProps,
9
+ } from './Card'
@@ -0,0 +1,98 @@
1
+ import React, { useRef } from 'react'
2
+ import { TouchableOpacity, Animated, View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import * as Haptics from 'expo-haptics'
4
+ import { useTheme } from '../../theme'
5
+
6
+ export interface CheckboxProps {
7
+ checked?: boolean
8
+ onCheckedChange?: (checked: boolean) => void
9
+ label?: string
10
+ disabled?: boolean
11
+ style?: ViewStyle
12
+ }
13
+
14
+ export function Checkbox({
15
+ checked = false,
16
+ onCheckedChange,
17
+ label,
18
+ disabled,
19
+ style,
20
+ }: CheckboxProps) {
21
+ const { colors } = useTheme()
22
+ const scale = useRef(new Animated.Value(1)).current
23
+
24
+ const handlePressIn = () => {
25
+ if (disabled) return
26
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
27
+ }
28
+
29
+ const handlePressOut = () => {
30
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
31
+ }
32
+
33
+ return (
34
+ <Animated.View style={{ transform: [{ scale }] }}>
35
+ <TouchableOpacity
36
+ style={[styles.row, style]}
37
+ onPress={() => {
38
+ Haptics.selectionAsync()
39
+ onCheckedChange?.(!checked)
40
+ }}
41
+ onPressIn={handlePressIn}
42
+ onPressOut={handlePressOut}
43
+ disabled={disabled}
44
+ activeOpacity={1}
45
+ touchSoundDisabled={true}
46
+ >
47
+ <View
48
+ style={[
49
+ styles.box,
50
+ {
51
+ borderColor: checked ? colors.primary : colors.border,
52
+ backgroundColor: checked ? colors.primary : 'transparent',
53
+ opacity: disabled ? 0.45 : 1,
54
+ },
55
+ ]}
56
+ >
57
+ {checked ? (
58
+ <View style={[styles.checkmark, { borderColor: colors.primaryForeground }]} />
59
+ ) : null}
60
+ </View>
61
+ {label ? (
62
+ <Text
63
+ style={[styles.label, { color: disabled ? colors.mutedForeground : colors.foreground }]}
64
+ >
65
+ {label}
66
+ </Text>
67
+ ) : null}
68
+ </TouchableOpacity>
69
+ </Animated.View>
70
+ )
71
+ }
72
+
73
+ const styles = StyleSheet.create({
74
+ row: {
75
+ flexDirection: 'row',
76
+ alignItems: 'center',
77
+ gap: 10,
78
+ },
79
+ box: {
80
+ width: 24,
81
+ height: 24,
82
+ borderRadius: 6,
83
+ borderWidth: 1.5,
84
+ alignItems: 'center',
85
+ justifyContent: 'center',
86
+ },
87
+ checkmark: {
88
+ width: 13,
89
+ height: 8,
90
+ borderLeftWidth: 2,
91
+ borderBottomWidth: 2,
92
+ transform: [{ rotate: '-45deg' }, { translateY: -1 }],
93
+ },
94
+ label: {
95
+ fontSize: 14,
96
+ lineHeight: 20,
97
+ },
98
+ })
@@ -0,0 +1,2 @@
1
+ export { Checkbox } from './Checkbox'
2
+ export type { CheckboxProps } from './Checkbox'
@@ -0,0 +1,67 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export interface EmptyStateProps {
6
+ icon?: React.ReactNode
7
+ title: string
8
+ description?: string
9
+ action?: React.ReactNode
10
+ style?: ViewStyle
11
+ }
12
+
13
+ export function EmptyState({ icon, title, description, action, style }: EmptyStateProps) {
14
+ const { colors } = useTheme()
15
+
16
+ return (
17
+ <View style={[styles.container, { borderColor: colors.border }, style]}>
18
+ {icon ? (
19
+ <View style={[styles.iconWrapper, { backgroundColor: colors.muted }]}>{icon}</View>
20
+ ) : null}
21
+ <View style={styles.textWrapper}>
22
+ <Text style={[styles.title, { color: colors.foreground }]}>{title}</Text>
23
+ {description ? (
24
+ <Text style={[styles.description, { color: colors.mutedForeground }]}>{description}</Text>
25
+ ) : null}
26
+ </View>
27
+ {action ? <View style={styles.action}>{action}</View> : null}
28
+ </View>
29
+ )
30
+ }
31
+
32
+ const styles = StyleSheet.create({
33
+ container: {
34
+ alignItems: 'center',
35
+ justifyContent: 'center',
36
+ borderWidth: 1,
37
+ borderStyle: 'dashed',
38
+ borderRadius: 12,
39
+ padding: 32,
40
+ gap: 16,
41
+ },
42
+ iconWrapper: {
43
+ width: 48,
44
+ height: 48,
45
+ borderRadius: 12,
46
+ alignItems: 'center',
47
+ justifyContent: 'center',
48
+ },
49
+ textWrapper: {
50
+ alignItems: 'center',
51
+ gap: 8,
52
+ maxWidth: 320,
53
+ },
54
+ title: {
55
+ fontSize: 18,
56
+ fontWeight: '500',
57
+ textAlign: 'center',
58
+ },
59
+ description: {
60
+ fontSize: 14,
61
+ lineHeight: 20,
62
+ textAlign: 'center',
63
+ },
64
+ action: {
65
+ marginTop: 8,
66
+ },
67
+ })
@@ -0,0 +1,2 @@
1
+ export { EmptyState } from './EmptyState'
2
+ export type { EmptyStateProps } from './EmptyState'
@@ -1,29 +1,32 @@
1
1
  import React, { useState } from 'react'
2
- import {
3
- TextInput,
4
- View,
5
- Text,
6
- StyleSheet,
7
- TextInputProps,
8
- } from 'react-native'
2
+ import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
9
4
 
10
5
  export interface InputProps extends TextInputProps {
11
6
  label?: string
7
+ /** Red helper text below the input; also changes border to `destructive` color. Takes priority over `hint`. */
12
8
  error?: string
9
+ /** Helper text shown below the input when there is no error. */
13
10
  hint?: string
11
+ /** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
12
+ containerStyle?: ViewStyle
14
13
  }
15
14
 
16
- export function Input({ label, error, hint, style, onFocus, onBlur, ...props }: InputProps) {
15
+ export function Input({ label, error, hint, containerStyle, style, onFocus, onBlur, ...props }: InputProps) {
16
+ const { colors } = useTheme()
17
17
  const [focused, setFocused] = useState(false)
18
18
 
19
19
  return (
20
- <View style={styles.container}>
21
- {label ? <Text style={styles.label}>{label}</Text> : null}
20
+ <View style={[styles.container, containerStyle]}>
21
+ {label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
22
22
  <TextInput
23
23
  style={[
24
24
  styles.input,
25
- focused && styles.inputFocused,
26
- error ? styles.inputError : undefined,
25
+ {
26
+ borderColor: error ? colors.destructive : focused ? colors.ring : colors.border,
27
+ color: colors.foreground,
28
+ backgroundColor: colors.background,
29
+ },
27
30
  style,
28
31
  ]}
29
32
  onFocus={(e) => {
@@ -34,11 +37,16 @@ export function Input({ label, error, hint, style, onFocus, onBlur, ...props }:
34
37
  setFocused(false)
35
38
  onBlur?.(e)
36
39
  }}
37
- placeholderTextColor="#9CA3AF"
40
+ placeholderTextColor={colors.mutedForeground}
41
+ allowFontScaling={true}
38
42
  {...props}
39
43
  />
40
- {error ? <Text style={styles.errorText}>{error}</Text> : null}
41
- {!error && hint ? <Text style={styles.hintText}>{hint}</Text> : null}
44
+ {error ? (
45
+ <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
46
+ ) : null}
47
+ {!error && hint ? (
48
+ <Text style={[styles.helperText, { color: colors.mutedForeground }]} allowFontScaling={true}>{hint}</Text>
49
+ ) : null}
42
50
  </View>
43
51
  )
44
52
  }
@@ -50,31 +58,16 @@ const styles = StyleSheet.create({
50
58
  label: {
51
59
  fontSize: 14,
52
60
  fontWeight: '500',
53
- color: '#111827',
54
- marginBottom: 2,
61
+ marginBottom: 4,
55
62
  },
56
63
  input: {
57
64
  borderWidth: 1.5,
58
- borderColor: '#D1D5DB',
59
65
  borderRadius: 8,
60
- paddingHorizontal: 12,
61
- paddingVertical: 10,
62
- fontSize: 15,
63
- color: '#111827',
64
- backgroundColor: '#fff',
66
+ paddingHorizontal: 16,
67
+ paddingVertical: 14,
68
+ fontSize: 16,
65
69
  },
66
- inputFocused: {
67
- borderColor: '#000',
68
- },
69
- inputError: {
70
- borderColor: '#EF4444',
71
- },
72
- errorText: {
73
- fontSize: 12,
74
- color: '#EF4444',
75
- },
76
- hintText: {
70
+ helperText: {
77
71
  fontSize: 12,
78
- color: '#6B7280',
79
72
  },
80
73
  })
@@ -0,0 +1,52 @@
1
+ import React, { useRef, useState, useEffect } from 'react'
2
+ import { View, Animated, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export interface ProgressProps {
6
+ /** Current progress value. Clamped to `[0, max]`. Defaults to `0`. */
7
+ value?: number
8
+ /** Maximum value. Defaults to `100`. */
9
+ max?: number
10
+ style?: ViewStyle
11
+ }
12
+
13
+ export function Progress({ value = 0, max = 100, style }: ProgressProps) {
14
+ const { colors } = useTheme()
15
+ const percent = Math.min(Math.max((value / max) * 100, 0), 100)
16
+ const [trackWidth, setTrackWidth] = useState(0)
17
+ const animatedWidth = useRef(new Animated.Value(0)).current
18
+
19
+ useEffect(() => {
20
+ if (trackWidth === 0) return
21
+ Animated.spring(animatedWidth, {
22
+ toValue: (percent / 100) * trackWidth,
23
+ useNativeDriver: false,
24
+ speed: 20,
25
+ bounciness: 0,
26
+ }).start()
27
+ }, [percent, trackWidth])
28
+
29
+ return (
30
+ <View
31
+ style={[styles.track, { backgroundColor: colors.muted }, style]}
32
+ onLayout={(e) => setTrackWidth(e.nativeEvent.layout.width)}
33
+ >
34
+ <Animated.View
35
+ style={[styles.indicator, { width: animatedWidth, backgroundColor: colors.primary }]}
36
+ />
37
+ </View>
38
+ )
39
+ }
40
+
41
+ const styles = StyleSheet.create({
42
+ track: {
43
+ height: 8,
44
+ borderRadius: 999,
45
+ overflow: 'hidden',
46
+ width: '100%',
47
+ },
48
+ indicator: {
49
+ height: '100%',
50
+ borderRadius: 999,
51
+ },
52
+ })
@@ -0,0 +1,2 @@
1
+ export { Progress } from './Progress'
2
+ export type { ProgressProps } from './Progress'
@@ -0,0 +1,132 @@
1
+ import React, { useRef } from 'react'
2
+ import { TouchableOpacity, Animated, View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import * as Haptics from 'expo-haptics'
4
+ import { useTheme } from '../../theme'
5
+
6
+ export interface RadioOption {
7
+ label: string
8
+ value: string
9
+ disabled?: boolean
10
+ }
11
+
12
+ export interface RadioGroupProps {
13
+ options: RadioOption[]
14
+ value?: string
15
+ onValueChange?: (value: string) => void
16
+ orientation?: 'vertical' | 'horizontal'
17
+ style?: ViewStyle
18
+ }
19
+
20
+ function RadioItem({
21
+ option,
22
+ selected,
23
+ onSelect,
24
+ }: {
25
+ option: RadioOption
26
+ selected: boolean
27
+ onSelect: () => void
28
+ }) {
29
+ const { colors } = useTheme()
30
+ const scale = useRef(new Animated.Value(1)).current
31
+
32
+ const handlePressIn = () => {
33
+ if (option.disabled) return
34
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
35
+ }
36
+
37
+ const handlePressOut = () => {
38
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
39
+ }
40
+
41
+ return (
42
+ <Animated.View style={{ transform: [{ scale }] }}>
43
+ <TouchableOpacity
44
+ style={styles.row}
45
+ onPress={() => {
46
+ if (!option.disabled) {
47
+ Haptics.selectionAsync()
48
+ onSelect()
49
+ }
50
+ }}
51
+ onPressIn={handlePressIn}
52
+ onPressOut={handlePressOut}
53
+ activeOpacity={1}
54
+ touchSoundDisabled={true}
55
+ disabled={option.disabled}
56
+ >
57
+ <View
58
+ style={[
59
+ styles.radio,
60
+ {
61
+ borderColor: selected ? colors.primary : colors.border,
62
+ opacity: option.disabled ? 0.45 : 1,
63
+ },
64
+ ]}
65
+ >
66
+ {selected ? <View style={[styles.dot, { backgroundColor: colors.primary }]} /> : null}
67
+ </View>
68
+ <Text
69
+ style={[
70
+ styles.label,
71
+ { color: option.disabled ? colors.mutedForeground : colors.foreground },
72
+ ]}
73
+ >
74
+ {option.label}
75
+ </Text>
76
+ </TouchableOpacity>
77
+ </Animated.View>
78
+ )
79
+ }
80
+
81
+ export function RadioGroup({
82
+ options,
83
+ value,
84
+ onValueChange,
85
+ orientation = 'vertical',
86
+ style,
87
+ }: RadioGroupProps) {
88
+ return (
89
+ <View style={[styles.container, orientation === 'horizontal' && styles.horizontal, style]}>
90
+ {options.map((option) => (
91
+ <RadioItem
92
+ key={option.value}
93
+ option={option}
94
+ selected={option.value === value}
95
+ onSelect={() => onValueChange?.(option.value)}
96
+ />
97
+ ))}
98
+ </View>
99
+ )
100
+ }
101
+
102
+ const styles = StyleSheet.create({
103
+ container: {
104
+ gap: 10,
105
+ },
106
+ horizontal: {
107
+ flexDirection: 'row',
108
+ flexWrap: 'wrap',
109
+ },
110
+ row: {
111
+ flexDirection: 'row',
112
+ alignItems: 'center',
113
+ gap: 10,
114
+ },
115
+ radio: {
116
+ width: 24,
117
+ height: 24,
118
+ borderRadius: 12,
119
+ borderWidth: 1.5,
120
+ alignItems: 'center',
121
+ justifyContent: 'center',
122
+ },
123
+ dot: {
124
+ width: 10,
125
+ height: 10,
126
+ borderRadius: 5,
127
+ },
128
+ label: {
129
+ fontSize: 14,
130
+ lineHeight: 20,
131
+ },
132
+ })
@@ -0,0 +1,2 @@
1
+ export { RadioGroup } from './RadioGroup'
2
+ export type { RadioGroupProps, RadioOption } from './RadioGroup'