@retray-dev/ui-kit 1.6.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.
- package/COMPONENTS.md +264 -15
- package/README.md +7 -6
- package/dist/index.d.mts +114 -11
- package/dist/index.d.ts +114 -11
- package/dist/index.js +660 -134
- package/dist/index.mjs +656 -138
- package/package.json +8 -8
- package/src/components/Accordion/Accordion.tsx +4 -4
- package/src/components/Alert/Alert.tsx +32 -8
- package/src/components/Alert/index.ts +2 -2
- package/src/components/Avatar/Avatar.tsx +8 -8
- package/src/components/Badge/Badge.tsx +4 -4
- package/src/components/Button/Button.tsx +21 -14
- package/src/components/Card/Card.tsx +9 -9
- package/src/components/Checkbox/Checkbox.tsx +8 -8
- package/src/components/Chip/Chip.tsx +142 -0
- package/src/components/Chip/index.ts +2 -0
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +87 -0
- package/src/components/ConfirmDialog/index.ts +2 -0
- package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +48 -0
- package/src/components/CurrencyDisplay/index.ts +1 -0
- package/src/components/CurrencyInputLarge/CurrencyInputLarge.tsx +66 -0
- package/src/components/CurrencyInputLarge/index.ts +1 -0
- package/src/components/EmptyState/EmptyState.tsx +40 -6
- package/src/components/Input/Input.tsx +8 -8
- package/src/components/LabelValue/LabelValue.tsx +47 -0
- package/src/components/LabelValue/index.ts +2 -0
- package/src/components/ListItem/ListItem.tsx +121 -0
- package/src/components/ListItem/index.ts +2 -0
- package/src/components/MonthPicker/MonthPicker.tsx +92 -0
- package/src/components/MonthPicker/index.ts +2 -0
- package/src/components/Select/Select.tsx +19 -19
- package/src/components/Switch/Switch.tsx +12 -7
- package/src/components/Tabs/Tabs.tsx +34 -15
- package/src/components/Text/Text.tsx +6 -6
- package/src/components/Textarea/Textarea.tsx +9 -9
- package/src/components/Toast/Toast.tsx +25 -7
- package/src/components/Toggle/Toggle.tsx +93 -24
- package/src/index.ts +7 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { ViewStyle } from 'react-native'
|
|
3
|
+
import { Input } from '../Input'
|
|
4
|
+
|
|
5
|
+
export interface CurrencyInputLargeProps {
|
|
6
|
+
value?: string
|
|
7
|
+
onChangeText?: (formatted: string) => void
|
|
8
|
+
/** Called with the parsed numeric value (no separators, no prefix). */
|
|
9
|
+
onChangeValue?: (raw: number) => void
|
|
10
|
+
/** Symbol prepended to the formatted value. Defaults to `'$'`. */
|
|
11
|
+
prefix?: string
|
|
12
|
+
/** Character used to separate groups of three digits. Defaults to `'.'`. */
|
|
13
|
+
thousandsSeparator?: '.' | ','
|
|
14
|
+
label?: string
|
|
15
|
+
/** Red helper text; also changes input border to destructive color. */
|
|
16
|
+
error?: string
|
|
17
|
+
hint?: string
|
|
18
|
+
placeholder?: string
|
|
19
|
+
editable?: boolean
|
|
20
|
+
containerStyle?: ViewStyle
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatCurrency(raw: string, separator: '.' | ','): string {
|
|
24
|
+
const digits = raw.replace(/\D/g, '')
|
|
25
|
+
if (!digits) return ''
|
|
26
|
+
return digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function CurrencyInputLarge({
|
|
30
|
+
value,
|
|
31
|
+
onChangeText,
|
|
32
|
+
onChangeValue,
|
|
33
|
+
prefix = '$',
|
|
34
|
+
thousandsSeparator = '.',
|
|
35
|
+
label,
|
|
36
|
+
error,
|
|
37
|
+
hint,
|
|
38
|
+
placeholder,
|
|
39
|
+
editable,
|
|
40
|
+
containerStyle,
|
|
41
|
+
}: CurrencyInputLargeProps) {
|
|
42
|
+
const handleChange = (text: string) => {
|
|
43
|
+
const withoutPrefix = prefix && text.startsWith(prefix) ? text.slice(prefix.length) : text
|
|
44
|
+
const formatted = formatCurrency(withoutPrefix, thousandsSeparator)
|
|
45
|
+
const display = formatted ? `${prefix}${formatted}` : ''
|
|
46
|
+
onChangeText?.(display)
|
|
47
|
+
const separatorRegex = new RegExp(`\\${thousandsSeparator}`, 'g')
|
|
48
|
+
const raw = parseFloat(formatted.replace(separatorRegex, '') || '0')
|
|
49
|
+
onChangeValue?.(isNaN(raw) ? 0 : raw)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Input
|
|
54
|
+
value={value}
|
|
55
|
+
onChangeText={handleChange}
|
|
56
|
+
keyboardType="numeric"
|
|
57
|
+
label={label}
|
|
58
|
+
error={error}
|
|
59
|
+
hint={hint}
|
|
60
|
+
placeholder={placeholder ?? `${prefix}0`}
|
|
61
|
+
editable={editable}
|
|
62
|
+
containerStyle={containerStyle}
|
|
63
|
+
style={{ fontSize: 36 }}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './CurrencyInputLarge'
|
|
@@ -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
|
|
20
|
+
<View
|
|
21
|
+
style={[
|
|
22
|
+
styles.container,
|
|
23
|
+
isCompact && styles.containerCompact,
|
|
24
|
+
{ borderColor: colors.border },
|
|
25
|
+
style,
|
|
26
|
+
]}
|
|
27
|
+
>
|
|
18
28
|
{icon ? (
|
|
19
|
-
<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
|
|
23
|
-
|
|
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,
|
|
@@ -53,21 +53,21 @@ export function Input({ label, error, hint, containerStyle, style, onFocus, onBl
|
|
|
53
53
|
|
|
54
54
|
const styles = StyleSheet.create({
|
|
55
55
|
container: {
|
|
56
|
-
gap:
|
|
56
|
+
gap: 6,
|
|
57
57
|
},
|
|
58
58
|
label: {
|
|
59
|
-
fontSize:
|
|
59
|
+
fontSize: 15,
|
|
60
60
|
fontWeight: '500',
|
|
61
|
-
marginBottom:
|
|
61
|
+
marginBottom: 6,
|
|
62
62
|
},
|
|
63
63
|
input: {
|
|
64
64
|
borderWidth: 1.5,
|
|
65
|
-
borderRadius:
|
|
66
|
-
paddingHorizontal:
|
|
67
|
-
paddingVertical:
|
|
68
|
-
fontSize:
|
|
65
|
+
borderRadius: 14,
|
|
66
|
+
paddingHorizontal: 20,
|
|
67
|
+
paddingVertical: 16,
|
|
68
|
+
fontSize: 17,
|
|
69
69
|
},
|
|
70
70
|
helperText: {
|
|
71
|
-
fontSize:
|
|
71
|
+
fontSize: 13,
|
|
72
72
|
},
|
|
73
73
|
})
|
|
@@ -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,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,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
|
+
})
|
|
@@ -163,10 +163,10 @@ export function Select({
|
|
|
163
163
|
|
|
164
164
|
const styles = StyleSheet.create({
|
|
165
165
|
container: {
|
|
166
|
-
gap:
|
|
166
|
+
gap: 6,
|
|
167
167
|
},
|
|
168
168
|
label: {
|
|
169
|
-
fontSize:
|
|
169
|
+
fontSize: 15,
|
|
170
170
|
fontWeight: '500',
|
|
171
171
|
marginBottom: 2,
|
|
172
172
|
},
|
|
@@ -175,24 +175,24 @@ const styles = StyleSheet.create({
|
|
|
175
175
|
alignItems: 'center',
|
|
176
176
|
justifyContent: 'space-between',
|
|
177
177
|
borderWidth: 1.5,
|
|
178
|
-
borderRadius:
|
|
179
|
-
paddingHorizontal:
|
|
180
|
-
paddingVertical:
|
|
178
|
+
borderRadius: 14,
|
|
179
|
+
paddingHorizontal: 20,
|
|
180
|
+
paddingVertical: 16,
|
|
181
181
|
},
|
|
182
182
|
triggerText: {
|
|
183
|
-
fontSize:
|
|
183
|
+
fontSize: 17,
|
|
184
184
|
flex: 1,
|
|
185
185
|
},
|
|
186
186
|
chevron: {
|
|
187
|
-
fontSize:
|
|
187
|
+
fontSize: 16,
|
|
188
188
|
marginLeft: 8,
|
|
189
189
|
},
|
|
190
190
|
helperText: {
|
|
191
|
-
fontSize:
|
|
191
|
+
fontSize: 13,
|
|
192
192
|
},
|
|
193
193
|
sheetBackground: {
|
|
194
|
-
borderTopLeftRadius:
|
|
195
|
-
borderTopRightRadius:
|
|
194
|
+
borderTopLeftRadius: 24,
|
|
195
|
+
borderTopRightRadius: 24,
|
|
196
196
|
},
|
|
197
197
|
sheetHandle: {
|
|
198
198
|
width: 36,
|
|
@@ -200,32 +200,32 @@ const styles = StyleSheet.create({
|
|
|
200
200
|
borderRadius: 2,
|
|
201
201
|
},
|
|
202
202
|
sheetContent: {
|
|
203
|
-
paddingHorizontal:
|
|
204
|
-
paddingBottom:
|
|
203
|
+
paddingHorizontal: 20,
|
|
204
|
+
paddingBottom: 36,
|
|
205
205
|
},
|
|
206
206
|
sheetTitle: {
|
|
207
|
-
fontSize:
|
|
207
|
+
fontSize: 17,
|
|
208
208
|
fontWeight: '600',
|
|
209
|
-
paddingVertical:
|
|
209
|
+
paddingVertical: 16,
|
|
210
210
|
paddingHorizontal: 4,
|
|
211
211
|
},
|
|
212
212
|
option: {
|
|
213
213
|
flexDirection: 'row',
|
|
214
214
|
alignItems: 'center',
|
|
215
215
|
justifyContent: 'space-between',
|
|
216
|
-
paddingHorizontal:
|
|
217
|
-
paddingVertical:
|
|
218
|
-
borderRadius:
|
|
216
|
+
paddingHorizontal: 16,
|
|
217
|
+
paddingVertical: 16,
|
|
218
|
+
borderRadius: 12,
|
|
219
219
|
},
|
|
220
220
|
optionText: {
|
|
221
|
-
fontSize:
|
|
221
|
+
fontSize: 17,
|
|
222
222
|
flex: 1,
|
|
223
223
|
},
|
|
224
224
|
disabledOption: {
|
|
225
225
|
opacity: 0.45,
|
|
226
226
|
},
|
|
227
227
|
checkmark: {
|
|
228
|
-
fontSize:
|
|
228
|
+
fontSize: 16,
|
|
229
229
|
fontWeight: '600',
|
|
230
230
|
marginLeft: 8,
|
|
231
231
|
},
|
|
@@ -1,11 +1,13 @@
|
|
|
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
|
|
|
6
|
-
const TRACK_WIDTH =
|
|
7
|
-
const TRACK_HEIGHT =
|
|
8
|
-
const THUMB_SIZE =
|
|
8
|
+
const TRACK_WIDTH = 60
|
|
9
|
+
const TRACK_HEIGHT = 36
|
|
10
|
+
const THUMB_SIZE = 28
|
|
9
11
|
const THUMB_OFFSET = 4
|
|
10
12
|
const THUMB_TRAVEL = TRACK_WIDTH - THUMB_SIZE - THUMB_OFFSET * 2
|
|
11
13
|
|
|
@@ -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:
|
|
30
|
+
useNativeDriver: nativeDriver,
|
|
29
31
|
bounciness: 4,
|
|
30
32
|
}),
|
|
31
33
|
Animated.timing(trackOpacity, {
|
|
@@ -70,10 +72,13 @@ const styles = StyleSheet.create({
|
|
|
70
72
|
width: TRACK_WIDTH,
|
|
71
73
|
height: TRACK_HEIGHT,
|
|
72
74
|
borderRadius: TRACK_HEIGHT / 2,
|
|
73
|
-
justifyContent
|
|
74
|
-
|
|
75
|
+
// No justifyContent/alignItems — thumb uses absolute positioning
|
|
76
|
+
// so the track's flex layout doesn't interfere with translateX animation
|
|
75
77
|
},
|
|
76
78
|
thumb: {
|
|
79
|
+
position: 'absolute',
|
|
80
|
+
top: THUMB_OFFSET,
|
|
81
|
+
left: THUMB_OFFSET,
|
|
77
82
|
width: THUMB_SIZE,
|
|
78
83
|
height: THUMB_SIZE,
|
|
79
84
|
borderRadius: THUMB_SIZE / 2,
|
|
@@ -6,6 +6,7 @@ import { useTheme } from '../../theme'
|
|
|
6
6
|
export interface TabItem {
|
|
7
7
|
label: string
|
|
8
8
|
value: string
|
|
9
|
+
icon?: React.ReactNode
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export interface TabsProps {
|
|
@@ -60,15 +61,22 @@ function TabTrigger({
|
|
|
60
61
|
touchSoundDisabled={true}
|
|
61
62
|
>
|
|
62
63
|
<Animated.View style={{ transform: [{ scale }] }}>
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
styles.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
<View style={styles.triggerInner}>
|
|
65
|
+
{tab.icon ? (
|
|
66
|
+
<View style={styles.triggerIcon}>
|
|
67
|
+
{(typeof tab.icon === 'function' ? (tab.icon as any)(isActive) : tab.icon) as React.ReactNode}
|
|
68
|
+
</View>
|
|
69
|
+
) : null}
|
|
70
|
+
<Text
|
|
71
|
+
style={[
|
|
72
|
+
styles.triggerLabel,
|
|
73
|
+
{ color: isActive ? colors.foreground : colors.mutedForeground },
|
|
74
|
+
isActive && styles.activeTriggerLabel,
|
|
75
|
+
]}
|
|
76
|
+
>
|
|
77
|
+
{tab.label}
|
|
78
|
+
</Text>
|
|
79
|
+
</View>
|
|
72
80
|
</Animated.View>
|
|
73
81
|
</TouchableOpacity>
|
|
74
82
|
)
|
|
@@ -133,7 +141,7 @@ export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps)
|
|
|
133
141
|
bottom: 4,
|
|
134
142
|
left: pillX,
|
|
135
143
|
width: pillWidth,
|
|
136
|
-
borderRadius:
|
|
144
|
+
borderRadius: 8,
|
|
137
145
|
shadowColor: '#000',
|
|
138
146
|
shadowOffset: { width: 0, height: 1 },
|
|
139
147
|
shadowOpacity: 0.1,
|
|
@@ -172,22 +180,33 @@ export function TabsContent({ value, activeValue, children, style }: TabsContent
|
|
|
172
180
|
const styles = StyleSheet.create({
|
|
173
181
|
list: {
|
|
174
182
|
flexDirection: 'row',
|
|
175
|
-
borderRadius:
|
|
183
|
+
borderRadius: 12,
|
|
176
184
|
padding: 4,
|
|
177
185
|
gap: 4,
|
|
178
186
|
},
|
|
179
187
|
pill: {},
|
|
180
188
|
trigger: {
|
|
181
189
|
flex: 1,
|
|
182
|
-
paddingVertical:
|
|
183
|
-
paddingHorizontal:
|
|
184
|
-
borderRadius:
|
|
190
|
+
paddingVertical: 12,
|
|
191
|
+
paddingHorizontal: 16,
|
|
192
|
+
borderRadius: 8,
|
|
185
193
|
alignItems: 'center',
|
|
186
194
|
justifyContent: 'center',
|
|
187
195
|
zIndex: 1,
|
|
188
196
|
},
|
|
197
|
+
triggerInner: {
|
|
198
|
+
flexDirection: 'row',
|
|
199
|
+
alignItems: 'center',
|
|
200
|
+
justifyContent: 'center',
|
|
201
|
+
gap: 8,
|
|
202
|
+
},
|
|
203
|
+
triggerIcon: {
|
|
204
|
+
marginRight: 6,
|
|
205
|
+
alignItems: 'center',
|
|
206
|
+
justifyContent: 'center',
|
|
207
|
+
},
|
|
189
208
|
triggerLabel: {
|
|
190
|
-
fontSize:
|
|
209
|
+
fontSize: 15,
|
|
191
210
|
fontWeight: '400',
|
|
192
211
|
},
|
|
193
212
|
activeTriggerLabel: {
|