@retray-dev/ui-kit 1.7.0 → 1.8.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.
@@ -0,0 +1,142 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+ import {
3
+ TouchableOpacity,
4
+ Animated,
5
+ View,
6
+ Text,
7
+ StyleSheet,
8
+ ViewStyle,
9
+ Platform,
10
+ Easing,
11
+ } from 'react-native'
12
+ import * as Haptics from 'expo-haptics'
13
+ import { useTheme } from '../../theme'
14
+
15
+ const nativeDriver = Platform.OS !== 'web'
16
+
17
+ export interface ChipProps {
18
+ label: string
19
+ selected?: boolean
20
+ onPress?: () => void
21
+ style?: ViewStyle
22
+ }
23
+
24
+ export interface ChipOption {
25
+ label: string
26
+ value: string | number
27
+ }
28
+
29
+ export interface ChipGroupProps {
30
+ options: ChipOption[]
31
+ value?: string | number
32
+ onValueChange?: (value: string | number) => void
33
+ style?: ViewStyle
34
+ }
35
+
36
+ export function Chip({ label, selected = false, onPress, style }: ChipProps) {
37
+ const { colors } = useTheme()
38
+ const scale = useRef(new Animated.Value(1)).current
39
+ const pressAnim = useRef(new Animated.Value(selected ? 1 : 0)).current
40
+
41
+ useEffect(() => {
42
+ Animated.timing(pressAnim, {
43
+ toValue: selected ? 1 : 0,
44
+ duration: 150,
45
+ easing: Easing.out(Easing.ease),
46
+ useNativeDriver: false,
47
+ }).start()
48
+ }, [selected, pressAnim])
49
+
50
+ const handlePressIn = () => {
51
+ Animated.spring(scale, {
52
+ toValue: 0.95,
53
+ useNativeDriver: nativeDriver,
54
+ speed: 40,
55
+ bounciness: 0,
56
+ }).start()
57
+ }
58
+
59
+ const handlePressOut = () => {
60
+ Animated.spring(scale, {
61
+ toValue: 1,
62
+ useNativeDriver: nativeDriver,
63
+ speed: 40,
64
+ bounciness: 4,
65
+ }).start()
66
+ }
67
+
68
+ const handlePress = () => {
69
+ Haptics.selectionAsync()
70
+ onPress?.()
71
+ }
72
+
73
+ const backgroundColor = pressAnim.interpolate({
74
+ inputRange: [0, 1],
75
+ outputRange: [colors.secondary, colors.primary],
76
+ })
77
+
78
+ const textColor = pressAnim.interpolate({
79
+ inputRange: [0, 1],
80
+ outputRange: [colors.foreground, colors.primaryForeground],
81
+ })
82
+
83
+ const borderColor = pressAnim.interpolate({
84
+ inputRange: [0, 1],
85
+ outputRange: [colors.border, colors.primary],
86
+ })
87
+
88
+ return (
89
+ <Animated.View style={[styles.wrapper, { transform: [{ scale }] }, style]}>
90
+ <TouchableOpacity
91
+ onPress={handlePress}
92
+ onPressIn={handlePressIn}
93
+ onPressOut={handlePressOut}
94
+ activeOpacity={1}
95
+ touchSoundDisabled={true}
96
+ >
97
+ <Animated.View style={[styles.chip, { backgroundColor, borderColor }]}>
98
+ <Animated.Text style={[styles.label, { color: textColor }]} allowFontScaling={true}>
99
+ {label}
100
+ </Animated.Text>
101
+ </Animated.View>
102
+ </TouchableOpacity>
103
+ </Animated.View>
104
+ )
105
+ }
106
+
107
+ export function ChipGroup({ options, value, onValueChange, style }: ChipGroupProps) {
108
+ return (
109
+ <View style={[styles.group, style]}>
110
+ {options.map((opt) => (
111
+ <Chip
112
+ key={opt.value}
113
+ label={opt.label}
114
+ selected={opt.value === value}
115
+ onPress={() => onValueChange?.(opt.value)}
116
+ />
117
+ ))}
118
+ </View>
119
+ )
120
+ }
121
+
122
+ const styles = StyleSheet.create({
123
+ wrapper: {},
124
+ chip: {
125
+ borderRadius: 999,
126
+ paddingHorizontal: 14,
127
+ paddingVertical: 8,
128
+ borderWidth: 1.5,
129
+ alignItems: 'center',
130
+ justifyContent: 'center',
131
+ },
132
+ label: {
133
+ fontSize: 14,
134
+ fontWeight: '500',
135
+ lineHeight: 20,
136
+ },
137
+ group: {
138
+ flexDirection: 'row',
139
+ flexWrap: 'wrap',
140
+ gap: 8,
141
+ },
142
+ })
@@ -0,0 +1,2 @@
1
+ export { Chip, ChipGroup } from './Chip'
2
+ export type { ChipProps, ChipOption, ChipGroupProps } from './Chip'
@@ -0,0 +1,87 @@
1
+ import React from 'react'
2
+ import { Modal, View, Text, StyleSheet, TouchableOpacity } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+ import { Button } from '../Button'
5
+
6
+ export interface ConfirmDialogProps {
7
+ visible: boolean
8
+ title: string
9
+ description?: string
10
+ confirmLabel?: string
11
+ cancelLabel?: string
12
+ confirmVariant?: 'primary' | 'destructive'
13
+ onConfirm: () => void
14
+ onCancel: () => void
15
+ }
16
+
17
+ export function ConfirmDialog({
18
+ visible,
19
+ title,
20
+ description,
21
+ confirmLabel = 'Confirm',
22
+ cancelLabel = 'Cancel',
23
+ confirmVariant = 'primary',
24
+ onConfirm,
25
+ onCancel,
26
+ }: ConfirmDialogProps) {
27
+ const { colors } = useTheme()
28
+
29
+ return (
30
+ <Modal visible={visible} transparent animationType="fade" onRequestClose={onCancel}>
31
+ <TouchableOpacity style={styles.overlay} activeOpacity={1} onPress={onCancel}>
32
+ <View
33
+ style={[styles.dialog, { backgroundColor: colors.card }]}
34
+ onStartShouldSetResponder={() => true}
35
+ >
36
+ <Text style={[styles.title, { color: colors.cardForeground }]} allowFontScaling={true}>
37
+ {title}
38
+ </Text>
39
+ {description ? (
40
+ <Text style={[styles.description, { color: colors.mutedForeground }]} allowFontScaling={true}>
41
+ {description}
42
+ </Text>
43
+ ) : null}
44
+ <View style={styles.actions}>
45
+ <Button label={cancelLabel} variant="outline" fullWidth onPress={onCancel} />
46
+ <Button label={confirmLabel} variant={confirmVariant} fullWidth onPress={onConfirm} />
47
+ </View>
48
+ </View>
49
+ </TouchableOpacity>
50
+ </Modal>
51
+ )
52
+ }
53
+
54
+ const styles = StyleSheet.create({
55
+ overlay: {
56
+ flex: 1,
57
+ backgroundColor: 'rgba(0,0,0,0.5)',
58
+ justifyContent: 'center',
59
+ alignItems: 'center',
60
+ padding: 24,
61
+ },
62
+ dialog: {
63
+ width: '100%',
64
+ maxWidth: 400,
65
+ borderRadius: 16,
66
+ padding: 24,
67
+ gap: 12,
68
+ shadowColor: '#000',
69
+ shadowOffset: { width: 0, height: 8 },
70
+ shadowOpacity: 0.15,
71
+ shadowRadius: 16,
72
+ elevation: 8,
73
+ },
74
+ title: {
75
+ fontSize: 18,
76
+ fontWeight: '600',
77
+ lineHeight: 26,
78
+ },
79
+ description: {
80
+ fontSize: 15,
81
+ lineHeight: 22,
82
+ },
83
+ actions: {
84
+ gap: 10,
85
+ marginTop: 8,
86
+ },
87
+ })
@@ -0,0 +1,2 @@
1
+ export { ConfirmDialog } from './ConfirmDialog'
2
+ export type { ConfirmDialogProps } from './ConfirmDialog'
@@ -8,6 +8,8 @@ export interface CurrencyDisplayProps {
8
8
  prefix?: string
9
9
  /** When true, shows two decimal places separated by a comma (e.g. `$25.000,00`). Defaults to `false`. */
10
10
  showDecimals?: boolean
11
+ /** Override the color of the formatted text. Defaults to the `foreground` theme token. */
12
+ textColor?: string
11
13
  style?: ViewStyle
12
14
  }
13
15
 
@@ -24,13 +26,13 @@ function formatValue(value: number | string, prefix: string, showDecimals: boole
24
26
  return `${sign}${prefix}${intPart}`
25
27
  }
26
28
 
27
- export function CurrencyDisplay({ value, prefix = '$', showDecimals = false, style }: CurrencyDisplayProps) {
29
+ export function CurrencyDisplay({ value, prefix = '$', showDecimals = false, textColor, style }: CurrencyDisplayProps) {
28
30
  const { colors } = useTheme()
29
31
  const formatted = formatValue(value, prefix, showDecimals)
30
32
 
31
33
  return (
32
34
  <View style={[styles.container, style]}>
33
- <Text style={[styles.amount, { color: colors.foreground }]} allowFontScaling={true}>
35
+ <Text style={[styles.amount, { color: textColor ?? colors.foreground }]} allowFontScaling={true}>
34
36
  {formatted}
35
37
  </Text>
36
38
  </View>
@@ -7,24 +7,46 @@ export interface EmptyStateProps {
7
7
  title: string
8
8
  description?: string
9
9
  action?: React.ReactNode
10
+ /** `compact` hides description/action and uses tighter spacing and a smaller icon. */
11
+ size?: 'default' | 'compact'
10
12
  style?: ViewStyle
11
13
  }
12
14
 
13
- export function EmptyState({ icon, title, description, action, style }: EmptyStateProps) {
15
+ export function EmptyState({ icon, title, description, action, size = 'default', style }: EmptyStateProps) {
14
16
  const { colors } = useTheme()
17
+ const isCompact = size === 'compact'
15
18
 
16
19
  return (
17
- <View style={[styles.container, { borderColor: colors.border }, style]}>
20
+ <View
21
+ style={[
22
+ styles.container,
23
+ isCompact && styles.containerCompact,
24
+ { borderColor: colors.border },
25
+ style,
26
+ ]}
27
+ >
18
28
  {icon ? (
19
- <View style={[styles.iconWrapper, { backgroundColor: colors.muted }]}>{icon}</View>
29
+ <View
30
+ style={[
31
+ styles.iconWrapper,
32
+ isCompact && styles.iconWrapperCompact,
33
+ { backgroundColor: colors.muted },
34
+ ]}
35
+ >
36
+ {icon}
37
+ </View>
20
38
  ) : null}
21
39
  <View style={styles.textWrapper}>
22
- <Text style={[styles.title, { color: colors.foreground }]}>{title}</Text>
23
- {description ? (
40
+ <Text
41
+ style={[styles.title, isCompact && styles.titleCompact, { color: colors.foreground }]}
42
+ >
43
+ {title}
44
+ </Text>
45
+ {description && !isCompact ? (
24
46
  <Text style={[styles.description, { color: colors.mutedForeground }]}>{description}</Text>
25
47
  ) : null}
26
48
  </View>
27
- {action ? <View style={styles.action}>{action}</View> : null}
49
+ {action && !isCompact ? <View style={styles.action}>{action}</View> : null}
28
50
  </View>
29
51
  )
30
52
  }
@@ -39,6 +61,10 @@ const styles = StyleSheet.create({
39
61
  padding: 32,
40
62
  gap: 16,
41
63
  },
64
+ containerCompact: {
65
+ padding: 20,
66
+ gap: 10,
67
+ },
42
68
  iconWrapper: {
43
69
  width: 48,
44
70
  height: 48,
@@ -46,6 +72,11 @@ const styles = StyleSheet.create({
46
72
  alignItems: 'center',
47
73
  justifyContent: 'center',
48
74
  },
75
+ iconWrapperCompact: {
76
+ width: 36,
77
+ height: 36,
78
+ borderRadius: 8,
79
+ },
49
80
  textWrapper: {
50
81
  alignItems: 'center',
51
82
  gap: 8,
@@ -56,6 +87,9 @@ const styles = StyleSheet.create({
56
87
  fontWeight: '500',
57
88
  textAlign: 'center',
58
89
  },
90
+ titleCompact: {
91
+ fontSize: 15,
92
+ },
59
93
  description: {
60
94
  fontSize: 14,
61
95
  lineHeight: 20,
@@ -0,0 +1,47 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export interface LabelValueProps {
6
+ label: string
7
+ value: string | React.ReactNode
8
+ style?: ViewStyle
9
+ }
10
+
11
+ export function LabelValue({ label, value, style }: LabelValueProps) {
12
+ const { colors } = useTheme()
13
+
14
+ return (
15
+ <View style={[styles.container, style]}>
16
+ <Text style={[styles.label, { color: colors.mutedForeground }]} allowFontScaling={true}>
17
+ {label}
18
+ </Text>
19
+ {typeof value === 'string' ? (
20
+ <Text style={[styles.value, { color: colors.foreground }]} allowFontScaling={true}>
21
+ {value}
22
+ </Text>
23
+ ) : (
24
+ value
25
+ )}
26
+ </View>
27
+ )
28
+ }
29
+
30
+ const styles = StyleSheet.create({
31
+ container: {
32
+ flexDirection: 'row',
33
+ justifyContent: 'space-between',
34
+ alignItems: 'center',
35
+ gap: 12,
36
+ },
37
+ label: {
38
+ fontSize: 13,
39
+ lineHeight: 18,
40
+ },
41
+ value: {
42
+ fontSize: 15,
43
+ fontWeight: '500',
44
+ lineHeight: 22,
45
+ textAlign: 'right',
46
+ },
47
+ })
@@ -0,0 +1,2 @@
1
+ export { LabelValue } from './LabelValue'
2
+ export type { LabelValueProps } from './LabelValue'
@@ -0,0 +1,121 @@
1
+ import React, { useRef } from 'react'
2
+ import {
3
+ TouchableOpacity,
4
+ Animated,
5
+ View,
6
+ Text,
7
+ StyleSheet,
8
+ ViewStyle,
9
+ Platform,
10
+ } from 'react-native'
11
+ import * as Haptics from 'expo-haptics'
12
+ import { useTheme } from '../../theme'
13
+
14
+ const nativeDriver = Platform.OS !== 'web'
15
+
16
+ export interface ListItemProps {
17
+ icon?: React.ReactNode
18
+ title: string
19
+ subtitle?: string
20
+ trailing?: string | React.ReactNode
21
+ onPress?: () => void
22
+ disabled?: boolean
23
+ style?: ViewStyle
24
+ }
25
+
26
+ export function ListItem({ icon, title, subtitle, trailing, onPress, disabled, style }: ListItemProps) {
27
+ const { colors } = useTheme()
28
+ const scale = useRef(new Animated.Value(1)).current
29
+
30
+ const handlePressIn = () => {
31
+ if (!onPress || disabled) return
32
+ Animated.spring(scale, {
33
+ toValue: 0.97,
34
+ useNativeDriver: nativeDriver,
35
+ speed: 40,
36
+ bounciness: 0,
37
+ }).start()
38
+ }
39
+
40
+ const handlePressOut = () => {
41
+ Animated.spring(scale, {
42
+ toValue: 1,
43
+ useNativeDriver: nativeDriver,
44
+ speed: 40,
45
+ bounciness: 4,
46
+ }).start()
47
+ }
48
+
49
+ const handlePress = () => {
50
+ Haptics.selectionAsync()
51
+ onPress?.()
52
+ }
53
+
54
+ return (
55
+ <Animated.View style={[{ transform: [{ scale }] }, disabled && styles.disabled]}>
56
+ <TouchableOpacity
57
+ style={[styles.container, style]}
58
+ onPress={onPress ? handlePress : undefined}
59
+ onPressIn={handlePressIn}
60
+ onPressOut={handlePressOut}
61
+ disabled={disabled}
62
+ activeOpacity={1}
63
+ touchSoundDisabled={true}
64
+ >
65
+ {icon ? <View style={styles.iconWrapper}>{icon}</View> : null}
66
+ <View style={styles.content}>
67
+ <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
68
+ {title}
69
+ </Text>
70
+ {subtitle ? (
71
+ <Text style={[styles.subtitle, { color: colors.mutedForeground }]} allowFontScaling={true}>
72
+ {subtitle}
73
+ </Text>
74
+ ) : null}
75
+ </View>
76
+ {trailing !== undefined ? (
77
+ typeof trailing === 'string' ? (
78
+ <Text style={[styles.trailing, { color: colors.mutedForeground }]} allowFontScaling={true}>
79
+ {trailing}
80
+ </Text>
81
+ ) : (
82
+ trailing
83
+ )
84
+ ) : null}
85
+ </TouchableOpacity>
86
+ </Animated.View>
87
+ )
88
+ }
89
+
90
+ const styles = StyleSheet.create({
91
+ container: {
92
+ flexDirection: 'row',
93
+ alignItems: 'center',
94
+ paddingHorizontal: 16,
95
+ paddingVertical: 14,
96
+ gap: 12,
97
+ },
98
+ iconWrapper: {
99
+ alignItems: 'center',
100
+ justifyContent: 'center',
101
+ },
102
+ content: {
103
+ flex: 1,
104
+ gap: 3,
105
+ },
106
+ title: {
107
+ fontSize: 16,
108
+ fontWeight: '500',
109
+ lineHeight: 22,
110
+ },
111
+ subtitle: {
112
+ fontSize: 13,
113
+ lineHeight: 18,
114
+ },
115
+ trailing: {
116
+ fontSize: 15,
117
+ },
118
+ disabled: {
119
+ opacity: 0.45,
120
+ },
121
+ })
@@ -0,0 +1,2 @@
1
+ export { ListItem } from './ListItem'
2
+ export type { ListItemProps } from './ListItem'
@@ -0,0 +1,92 @@
1
+ import React from 'react'
2
+ import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'
3
+ import * as Haptics from 'expo-haptics'
4
+ import { useTheme } from '../../theme'
5
+
6
+ const MONTH_NAMES = [
7
+ 'January', 'February', 'March', 'April', 'May', 'June',
8
+ 'July', 'August', 'September', 'October', 'November', 'December',
9
+ ]
10
+
11
+ export interface MonthPickerValue {
12
+ /** Month number 1–12 */
13
+ month: number
14
+ year: number
15
+ }
16
+
17
+ export interface MonthPickerProps {
18
+ value: MonthPickerValue
19
+ onChange: (value: MonthPickerValue) => void
20
+ style?: ViewStyle
21
+ }
22
+
23
+ export function MonthPicker({ value, onChange, style }: MonthPickerProps) {
24
+ const { colors } = useTheme()
25
+
26
+ const handlePrev = () => {
27
+ Haptics.selectionAsync()
28
+ if (value.month === 1) {
29
+ onChange({ month: 12, year: value.year - 1 })
30
+ } else {
31
+ onChange({ month: value.month - 1, year: value.year })
32
+ }
33
+ }
34
+
35
+ const handleNext = () => {
36
+ Haptics.selectionAsync()
37
+ if (value.month === 12) {
38
+ onChange({ month: 1, year: value.year + 1 })
39
+ } else {
40
+ onChange({ month: value.month + 1, year: value.year })
41
+ }
42
+ }
43
+
44
+ return (
45
+ <View style={[styles.container, style]}>
46
+ <TouchableOpacity
47
+ style={styles.arrow}
48
+ onPress={handlePrev}
49
+ activeOpacity={0.6}
50
+ touchSoundDisabled={true}
51
+ >
52
+ <Text style={[styles.arrowText, { color: colors.foreground }]}>‹</Text>
53
+ </TouchableOpacity>
54
+ <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>
55
+ {MONTH_NAMES[value.month - 1]} {value.year}
56
+ </Text>
57
+ <TouchableOpacity
58
+ style={styles.arrow}
59
+ onPress={handleNext}
60
+ activeOpacity={0.6}
61
+ touchSoundDisabled={true}
62
+ >
63
+ <Text style={[styles.arrowText, { color: colors.foreground }]}>›</Text>
64
+ </TouchableOpacity>
65
+ </View>
66
+ )
67
+ }
68
+
69
+ const styles = StyleSheet.create({
70
+ container: {
71
+ flexDirection: 'row',
72
+ alignItems: 'center',
73
+ justifyContent: 'space-between',
74
+ },
75
+ arrow: {
76
+ width: 44,
77
+ height: 44,
78
+ alignItems: 'center',
79
+ justifyContent: 'center',
80
+ },
81
+ arrowText: {
82
+ fontSize: 24,
83
+ lineHeight: 30,
84
+ },
85
+ label: {
86
+ fontSize: 17,
87
+ fontWeight: '500',
88
+ lineHeight: 24,
89
+ textAlign: 'center',
90
+ minWidth: 160,
91
+ },
92
+ })
@@ -0,0 +1,2 @@
1
+ export { MonthPicker } from './MonthPicker'
2
+ export type { MonthPickerProps, MonthPickerValue } from './MonthPicker'
@@ -1,5 +1,7 @@
1
1
  import React, { useEffect, useRef } from 'react'
2
- import { TouchableOpacity, Animated, StyleSheet, ViewStyle } from 'react-native'
2
+ import { TouchableOpacity, Animated, StyleSheet, ViewStyle, Platform } from 'react-native'
3
+
4
+ const nativeDriver = Platform.OS !== 'web'
3
5
  import * as Haptics from 'expo-haptics'
4
6
  import { useTheme } from '../../theme'
5
7
 
@@ -25,7 +27,7 @@ export function Switch({ checked = false, onCheckedChange, disabled, style }: Sw
25
27
  Animated.parallel([
26
28
  Animated.spring(translateX, {
27
29
  toValue: checked ? THUMB_TRAVEL : 0,
28
- useNativeDriver: true,
30
+ useNativeDriver: nativeDriver,
29
31
  bounciness: 4,
30
32
  }),
31
33
  Animated.timing(trackOpacity, {
package/src/index.ts CHANGED
@@ -30,3 +30,8 @@ export * from './components/Toast'
30
30
  export * from './components/CurrencyInput'
31
31
  export * from './components/CurrencyDisplay'
32
32
  export * from './components/CurrencyInputLarge'
33
+ export * from './components/ListItem'
34
+ export * from './components/Chip'
35
+ export * from './components/ConfirmDialog'
36
+ export * from './components/LabelValue'
37
+ export * from './components/MonthPicker'