@retray-dev/ui-kit 4.0.0 → 5.2.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 +1806 -663
- package/README.md +14 -10
- package/dist/index.d.mts +274 -85
- package/dist/index.d.ts +274 -85
- package/dist/index.js +1048 -321
- package/dist/index.mjs +1046 -324
- package/package.json +3 -2
- package/src/components/Accordion/Accordion.tsx +1 -1
- package/src/components/AlertBanner/AlertBanner.tsx +50 -45
- package/src/components/Avatar/Avatar.tsx +61 -17
- package/src/components/Badge/Badge.tsx +17 -15
- package/src/components/Button/Button.tsx +31 -42
- package/src/components/Card/Card.tsx +4 -4
- package/src/components/CategoryStrip/CategoryStrip.tsx +185 -0
- package/src/components/CategoryStrip/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.tsx +44 -16
- package/src/components/Chip/Chip.tsx +1 -1
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +9 -9
- package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +1 -0
- package/src/components/CurrencyInput/CurrencyInput.tsx +6 -4
- package/src/components/EmptyState/EmptyState.tsx +9 -9
- package/src/components/IconButton/IconButton.tsx +74 -34
- package/src/components/Input/Input.tsx +15 -13
- package/src/components/LabelValue/LabelValue.tsx +1 -1
- package/src/components/ListItem/ListItem.tsx +5 -5
- package/src/components/MediaCard/MediaCard.tsx +249 -0
- package/src/components/MediaCard/index.ts +2 -0
- package/src/components/Pressable/Pressable.tsx +100 -0
- package/src/components/Pressable/index.ts +1 -0
- package/src/components/Progress/Progress.tsx +14 -7
- package/src/components/RadioGroup/RadioGroup.tsx +1 -1
- package/src/components/Select/Select.tsx +5 -5
- package/src/components/Sheet/Sheet.tsx +35 -15
- package/src/components/Skeleton/Skeleton.tsx +34 -7
- package/src/components/Slider/Slider.tsx +2 -2
- package/src/components/Spinner/Spinner.tsx +1 -1
- package/src/components/Switch/Switch.tsx +31 -4
- package/src/components/Tabs/Tabs.tsx +63 -45
- package/src/components/Text/Text.tsx +59 -10
- package/src/components/Textarea/Textarea.tsx +4 -3
- package/src/components/Toast/Toast.tsx +77 -36
- package/src/components/Toggle/Toggle.tsx +3 -3
- package/src/index.ts +8 -2
- package/src/theme/ThemeProvider.tsx +11 -10
- package/src/theme/colorUtils.ts +80 -0
- package/src/theme/colors.ts +76 -35
- package/src/theme/index.ts +2 -2
- package/src/theme/types.ts +27 -13
- package/src/tokens.ts +150 -13
- package/src/utils/hover.ts +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retray-dev/ui-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "Personal UI Kit for React Native / Expo",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -89,5 +89,6 @@
|
|
|
89
89
|
"tsup": "^8.0.0",
|
|
90
90
|
"typescript": "^5.4.0",
|
|
91
91
|
"typescript-eslint": "^8.0.0"
|
|
92
|
-
}
|
|
92
|
+
},
|
|
93
|
+
"packageManager": "pnpm@11.0.8+sha512.4c4097e1dd2d42372c4e7fa5a791ff28fc75a484c7ac192e64b1df0fdef17594ba982f9b4fed9adfb3c757846f565b799b2763fb3733d1de1bcb82cf46684912"
|
|
93
94
|
}
|
|
@@ -93,7 +93,7 @@ function AccordionItemComponent({
|
|
|
93
93
|
>
|
|
94
94
|
<Text style={[styles.triggerText, { color: colors.foreground }]} allowFontScaling={true}>{item.trigger}</Text>
|
|
95
95
|
<Animated.View style={[styles.chevron, rotationStyle]}>
|
|
96
|
-
<Entypo name="chevron-down" size={18} color={colors.
|
|
96
|
+
<Entypo name="chevron-down" size={18} color={colors.foregroundMuted} />
|
|
97
97
|
</Animated.View>
|
|
98
98
|
</Pressable>
|
|
99
99
|
|
|
@@ -2,22 +2,17 @@ import React from 'react'
|
|
|
2
2
|
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
3
|
import { FontAwesome5, MaterialIcons, Entypo } from '@expo/vector-icons'
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
|
-
import { s, vs, ms
|
|
5
|
+
import { s, vs, ms } from '../../utils/scaling'
|
|
6
6
|
import { renderIcon } from '../../utils/icons'
|
|
7
7
|
|
|
8
|
-
export type AlertBannerVariant = 'default' | 'destructive' | 'success'
|
|
8
|
+
export type AlertBannerVariant = 'default' | 'destructive' | 'success' | 'warning'
|
|
9
9
|
|
|
10
10
|
export interface AlertBannerProps {
|
|
11
11
|
title: string
|
|
12
12
|
description?: string
|
|
13
13
|
variant?: AlertBannerVariant
|
|
14
14
|
icon?: React.ReactNode
|
|
15
|
-
/**
|
|
16
|
-
* Icon name from `@expo/vector-icons`. See https://icons.expo.fyi.
|
|
17
|
-
* Takes precedence over `icon`. When neither is set, a default variant icon is shown.
|
|
18
|
-
*/
|
|
19
15
|
iconName?: string
|
|
20
|
-
/** Override the resolved icon color. Defaults to the variant title color. */
|
|
21
16
|
iconColor?: string
|
|
22
17
|
style?: ViewStyle
|
|
23
18
|
}
|
|
@@ -26,73 +21,83 @@ export function AlertBanner({ title, description, variant = 'default', icon, ico
|
|
|
26
21
|
const { colors } = useTheme()
|
|
27
22
|
|
|
28
23
|
const bgColor =
|
|
29
|
-
variant === 'destructive' ? colors.
|
|
30
|
-
: variant === 'success'
|
|
24
|
+
variant === 'destructive' ? colors.destructiveTint
|
|
25
|
+
: variant === 'success' ? colors.successTint
|
|
26
|
+
: variant === 'warning' ? colors.warningTint
|
|
31
27
|
: colors.card
|
|
32
28
|
|
|
33
|
-
const
|
|
34
|
-
variant === 'destructive' ?
|
|
35
|
-
: variant === 'success'
|
|
36
|
-
: colors.
|
|
29
|
+
const borderColor =
|
|
30
|
+
variant === 'destructive' ? colors.destructiveBorder
|
|
31
|
+
: variant === 'success' ? colors.successBorder
|
|
32
|
+
: variant === 'warning' ? colors.warningBorder
|
|
33
|
+
: colors.border
|
|
34
|
+
|
|
35
|
+
const accentColor =
|
|
36
|
+
variant === 'destructive' ? colors.destructive
|
|
37
|
+
: variant === 'success' ? colors.success
|
|
38
|
+
: variant === 'warning' ? colors.warning
|
|
39
|
+
: colors.primary
|
|
40
|
+
|
|
41
|
+
const titleColor =
|
|
42
|
+
variant === 'default' ? colors.foreground : accentColor
|
|
37
43
|
|
|
38
|
-
const
|
|
44
|
+
const descColor =
|
|
45
|
+
variant === 'default' ? colors.foregroundMuted : accentColor
|
|
39
46
|
|
|
40
47
|
const defaultIcon =
|
|
41
48
|
variant === 'success' ? (
|
|
42
|
-
<FontAwesome5 name="check-circle" size={
|
|
49
|
+
<FontAwesome5 name="check-circle" size={16} color={accentColor} />
|
|
43
50
|
) : variant === 'destructive' ? (
|
|
44
|
-
<MaterialIcons name="error-outline" size={
|
|
51
|
+
<MaterialIcons name="error-outline" size={17} color={accentColor} />
|
|
52
|
+
) : variant === 'warning' ? (
|
|
53
|
+
<MaterialIcons name="warning-amber" size={17} color={accentColor} />
|
|
45
54
|
) : (
|
|
46
|
-
<Entypo name="info-with-circle" size={
|
|
55
|
+
<Entypo name="info-with-circle" size={16} color={accentColor} />
|
|
47
56
|
)
|
|
48
57
|
|
|
49
58
|
const effectiveIcon: React.ReactNode = iconName
|
|
50
|
-
? renderIcon(iconName,
|
|
59
|
+
? renderIcon(iconName, 16, iconColor ?? accentColor)
|
|
51
60
|
: icon ?? defaultIcon
|
|
52
61
|
|
|
53
62
|
return (
|
|
54
63
|
<View style={[styles.container, { backgroundColor: bgColor, borderColor }, style]}>
|
|
55
|
-
<View style={styles.
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
<View style={styles.iconSlot}>{effectiveIcon}</View>
|
|
65
|
+
<View style={styles.content}>
|
|
66
|
+
<Text style={[styles.title, { color: titleColor }]} allowFontScaling={true}>{title}</Text>
|
|
67
|
+
{description ? (
|
|
68
|
+
<Text style={[styles.description, { color: descColor }]} allowFontScaling={true}>{description}</Text>
|
|
69
|
+
) : null}
|
|
58
70
|
</View>
|
|
59
|
-
{description ? (
|
|
60
|
-
<Text style={[styles.description, { color: textColor, opacity: 0.85 }]} allowFontScaling={true}>{description}</Text>
|
|
61
|
-
) : null}
|
|
62
71
|
</View>
|
|
63
72
|
)
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
const styles = StyleSheet.create({
|
|
67
76
|
container: {
|
|
68
|
-
borderWidth: 1,
|
|
69
|
-
borderRadius: ms(12),
|
|
70
|
-
paddingHorizontal: s(14),
|
|
71
|
-
paddingVertical: vs(12),
|
|
72
|
-
gap: vs(8),
|
|
73
|
-
shadowColor: '#000',
|
|
74
|
-
shadowOffset: { width: 0, height: 3 },
|
|
75
|
-
shadowOpacity: 0.10,
|
|
76
|
-
shadowRadius: 8,
|
|
77
|
-
elevation: 5,
|
|
78
|
-
},
|
|
79
|
-
header: {
|
|
80
77
|
flexDirection: 'row',
|
|
81
|
-
alignItems: '
|
|
78
|
+
alignItems: 'flex-start',
|
|
79
|
+
borderWidth: 0.5,
|
|
80
|
+
borderRadius: 10,
|
|
81
|
+
paddingHorizontal: s(12),
|
|
82
|
+
paddingVertical: vs(10),
|
|
82
83
|
gap: s(10),
|
|
83
84
|
},
|
|
84
|
-
|
|
85
|
-
marginTop:
|
|
85
|
+
iconSlot: {
|
|
86
|
+
marginTop: vs(1),
|
|
86
87
|
},
|
|
87
|
-
|
|
88
|
-
fontFamily: 'Poppins-Bold',
|
|
89
|
-
fontSize: ms(15),
|
|
90
|
-
lineHeight: mvs(20),
|
|
88
|
+
content: {
|
|
91
89
|
flex: 1,
|
|
90
|
+
gap: vs(2),
|
|
91
|
+
},
|
|
92
|
+
title: {
|
|
93
|
+
fontFamily: 'Poppins-Medium',
|
|
94
|
+
fontSize: ms(13),
|
|
95
|
+
lineHeight: ms(18),
|
|
92
96
|
},
|
|
93
97
|
description: {
|
|
94
98
|
fontFamily: 'Poppins-Regular',
|
|
95
|
-
fontSize: ms(
|
|
96
|
-
lineHeight:
|
|
99
|
+
fontSize: ms(12),
|
|
100
|
+
lineHeight: ms(17),
|
|
101
|
+
opacity: 0.85,
|
|
97
102
|
},
|
|
98
103
|
})
|
|
@@ -4,13 +4,15 @@ import { useTheme } from '../../theme'
|
|
|
4
4
|
import { s, ms } from '../../utils/scaling'
|
|
5
5
|
|
|
6
6
|
export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl'
|
|
7
|
+
// online: green dot offline: border-only (no fill) busy: destructive away: warning
|
|
8
|
+
export type AvatarStatus = 'online' | 'offline' | 'busy' | 'away'
|
|
7
9
|
|
|
8
10
|
export interface AvatarProps {
|
|
9
|
-
/** Remote image URI. Falls back to `fallback` initials on error or when omitted. */
|
|
10
11
|
src?: string
|
|
11
|
-
/** Up to 2 characters shown when the image is unavailable. Auto-uppercased. Defaults to `'?'`. */
|
|
12
12
|
fallback?: string
|
|
13
13
|
size?: AvatarSize
|
|
14
|
+
/** Optional status indicator dot — bottom-right corner. */
|
|
15
|
+
status?: AvatarStatus
|
|
14
16
|
style?: ViewStyle
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -28,41 +30,78 @@ const fontSizeMap: Record<AvatarSize, number> = {
|
|
|
28
30
|
xl: ms(28),
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
const statusSizeMap: Record<AvatarSize, number> = {
|
|
34
|
+
sm: 8,
|
|
35
|
+
md: 10,
|
|
36
|
+
lg: 13,
|
|
37
|
+
xl: 16,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function Avatar({ src, fallback, size = 'md', status, style }: AvatarProps) {
|
|
32
41
|
const { colors } = useTheme()
|
|
33
42
|
const [imageError, setImageError] = useState(false)
|
|
34
43
|
const dimension = sizeMap[size]
|
|
35
44
|
const showFallback = !src || imageError
|
|
36
45
|
|
|
46
|
+
const statusSize = statusSizeMap[size]
|
|
47
|
+
|
|
48
|
+
const statusColor: Record<AvatarStatus, string> = {
|
|
49
|
+
online: '#22c55e',
|
|
50
|
+
offline: 'transparent',
|
|
51
|
+
busy: colors.destructive,
|
|
52
|
+
away: colors.warning,
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
const containerStyle: ViewStyle = {
|
|
38
56
|
width: dimension,
|
|
39
57
|
height: dimension,
|
|
40
58
|
borderRadius: dimension / 2,
|
|
41
|
-
backgroundColor: colors.
|
|
59
|
+
backgroundColor: colors.surface,
|
|
42
60
|
overflow: 'hidden',
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
return (
|
|
46
|
-
<View style={[styles.
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
<View style={[styles.wrapper, style]}>
|
|
65
|
+
<View style={[styles.base, containerStyle]}>
|
|
66
|
+
{!showFallback ? (
|
|
67
|
+
<Image
|
|
68
|
+
source={{ uri: src }}
|
|
69
|
+
style={{ width: dimension, height: dimension }}
|
|
70
|
+
onError={() => setImageError(true)}
|
|
71
|
+
/>
|
|
72
|
+
) : (
|
|
73
|
+
<Text
|
|
74
|
+
style={[styles.fallback, { color: colors.foregroundMuted, fontSize: fontSizeMap[size] }]}
|
|
75
|
+
allowFontScaling={true}
|
|
76
|
+
>
|
|
77
|
+
{fallback?.slice(0, 2).toUpperCase() ?? '?'}
|
|
78
|
+
</Text>
|
|
79
|
+
)}
|
|
80
|
+
</View>
|
|
81
|
+
{status && (
|
|
82
|
+
<View
|
|
83
|
+
style={[
|
|
84
|
+
styles.statusDot,
|
|
85
|
+
{
|
|
86
|
+
width: statusSize,
|
|
87
|
+
height: statusSize,
|
|
88
|
+
borderRadius: statusSize / 2,
|
|
89
|
+
backgroundColor: statusColor[status],
|
|
90
|
+
borderWidth: status === 'offline' ? 2 : 1.5,
|
|
91
|
+
borderColor: status === 'offline' ? colors.border : colors.background,
|
|
92
|
+
},
|
|
93
|
+
]}
|
|
52
94
|
/>
|
|
53
|
-
) : (
|
|
54
|
-
<Text
|
|
55
|
-
style={[styles.fallback, { color: colors.mutedForeground, fontSize: fontSizeMap[size] }]}
|
|
56
|
-
allowFontScaling={true}
|
|
57
|
-
>
|
|
58
|
-
{fallback?.slice(0, 2).toUpperCase() ?? '?'}
|
|
59
|
-
</Text>
|
|
60
95
|
)}
|
|
61
96
|
</View>
|
|
62
97
|
)
|
|
63
98
|
}
|
|
64
99
|
|
|
65
100
|
const styles = StyleSheet.create({
|
|
101
|
+
wrapper: {
|
|
102
|
+
alignSelf: 'flex-start',
|
|
103
|
+
position: 'relative',
|
|
104
|
+
},
|
|
66
105
|
base: {
|
|
67
106
|
alignItems: 'center',
|
|
68
107
|
justifyContent: 'center',
|
|
@@ -70,4 +109,9 @@ const styles = StyleSheet.create({
|
|
|
70
109
|
fallback: {
|
|
71
110
|
fontFamily: 'Poppins-Medium',
|
|
72
111
|
},
|
|
112
|
+
statusDot: {
|
|
113
|
+
position: 'absolute',
|
|
114
|
+
bottom: 0,
|
|
115
|
+
right: 0,
|
|
116
|
+
},
|
|
73
117
|
})
|
|
@@ -4,7 +4,7 @@ import { useTheme } from '../../theme'
|
|
|
4
4
|
import { s, vs, ms } from '../../utils/scaling'
|
|
5
5
|
import { renderIcon } from '../../utils/icons'
|
|
6
6
|
|
|
7
|
-
export type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'successOutline' | 'destructiveOutline'
|
|
7
|
+
export type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'successOutline' | 'destructiveOutline' | 'warningOutline'
|
|
8
8
|
export type BadgeSize = 'sm' | 'md' | 'lg'
|
|
9
9
|
|
|
10
10
|
export interface BadgeProps {
|
|
@@ -49,25 +49,27 @@ export function Badge({ label, children, variant = 'default', size = 'md', icon,
|
|
|
49
49
|
const { colors } = useTheme()
|
|
50
50
|
|
|
51
51
|
const containerStyle: ViewStyle = {
|
|
52
|
-
default:
|
|
53
|
-
secondary:
|
|
54
|
-
destructive:
|
|
55
|
-
outline:
|
|
56
|
-
success:
|
|
57
|
-
warning:
|
|
58
|
-
successOutline:
|
|
52
|
+
default: { backgroundColor: colors.primary },
|
|
53
|
+
secondary: { backgroundColor: colors.surface },
|
|
54
|
+
destructive: { backgroundColor: colors.destructive },
|
|
55
|
+
outline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: colors.border },
|
|
56
|
+
success: { backgroundColor: colors.success },
|
|
57
|
+
warning: { backgroundColor: colors.warning },
|
|
58
|
+
successOutline: { backgroundColor: colors.successTint, borderWidth: 1, borderColor: colors.successBorder },
|
|
59
59
|
destructiveOutline: { backgroundColor: colors.destructiveTint, borderWidth: 1, borderColor: colors.destructiveBorder },
|
|
60
|
+
warningOutline: { backgroundColor: colors.warningTint, borderWidth: 1, borderColor: colors.warningBorder },
|
|
60
61
|
}[variant]
|
|
61
62
|
|
|
62
63
|
const textColor = {
|
|
63
|
-
default:
|
|
64
|
-
secondary:
|
|
65
|
-
destructive:
|
|
66
|
-
outline:
|
|
67
|
-
success:
|
|
68
|
-
warning:
|
|
69
|
-
successOutline:
|
|
64
|
+
default: colors.primaryForeground,
|
|
65
|
+
secondary: colors.foreground,
|
|
66
|
+
destructive: colors.destructiveForeground,
|
|
67
|
+
outline: colors.foreground,
|
|
68
|
+
success: colors.successForeground,
|
|
69
|
+
warning: colors.warningForeground,
|
|
70
|
+
successOutline: colors.success,
|
|
70
71
|
destructiveOutline: colors.destructive,
|
|
72
|
+
warningOutline: colors.warning,
|
|
71
73
|
}[variant]
|
|
72
74
|
|
|
73
75
|
const effectiveIcon: React.ReactNode = iconName
|
|
@@ -16,47 +16,38 @@ import { impactLight } from '../../utils/haptics'
|
|
|
16
16
|
import { useTheme } from '../../theme'
|
|
17
17
|
import { s, vs, ms } from '../../utils/scaling'
|
|
18
18
|
import { renderIcon } from '../../utils/icons'
|
|
19
|
+
import { RADIUS, TYPOGRAPHY } from '../../tokens'
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
// primary: filled primary — main CTA (pill-shaped, Airbnb-style)
|
|
22
|
+
// secondary: outlined primary border — alternative actions
|
|
23
|
+
// text: fully transparent — low-emphasis, in-context actions
|
|
24
|
+
// destructive: filled destructive — delete/danger actions
|
|
25
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'text' | 'destructive'
|
|
21
26
|
export type ButtonSize = 'sm' | 'md' | 'lg'
|
|
22
27
|
|
|
23
28
|
export interface ButtonProps extends TouchableOpacityProps {
|
|
24
29
|
label: string
|
|
25
|
-
/**
|
|
26
|
-
* - `primary`: filled with `primary` token — main CTA
|
|
27
|
-
* - `secondary`: filled with `secondary` token — less prominent
|
|
28
|
-
* - `outline`: transparent with border — alternative actions
|
|
29
|
-
* - `ghost`: fully transparent — in-context or low-emphasis actions
|
|
30
|
-
*/
|
|
31
30
|
variant?: ButtonVariant
|
|
32
31
|
size?: ButtonSize
|
|
33
|
-
/** Replaces the label with a spinner and forces `disabled`. */
|
|
34
32
|
loading?: boolean
|
|
35
33
|
fullWidth?: boolean
|
|
36
|
-
/** Icon rendered alongside the label. Can be a ReactNode or a render function `(props) => ReactNode`. */
|
|
37
34
|
icon?: React.ReactNode | ((props: { label: string; size: ButtonSize; variant: ButtonVariant }) => React.ReactNode)
|
|
38
|
-
/**
|
|
39
|
-
* Icon name from `@expo/vector-icons` (e.g. `"home"`, `"star"`, `"arrow-right"`).
|
|
40
|
-
* See https://icons.expo.fyi to browse available icons.
|
|
41
|
-
* Takes precedence over `icon` when both are supplied.
|
|
42
|
-
*/
|
|
43
35
|
iconName?: string
|
|
44
|
-
/** Override the resolved icon color. Defaults to the label foreground color for the active variant. */
|
|
45
36
|
iconColor?: string
|
|
46
|
-
/** Side the icon appears on. Defaults to `'left'`. */
|
|
47
37
|
iconPosition?: 'left' | 'right'
|
|
48
38
|
}
|
|
49
39
|
|
|
40
|
+
// Airbnb-spec sizing: md=48px height, padding 14px vertical 24px horizontal
|
|
50
41
|
const containerSizeStyles: Record<ButtonSize, ViewStyle> = {
|
|
51
|
-
sm: { paddingHorizontal: s(
|
|
52
|
-
md: { paddingHorizontal: s(
|
|
53
|
-
lg: { paddingHorizontal: s(
|
|
42
|
+
sm: { paddingHorizontal: s(16), paddingVertical: vs(10), minHeight: 40 },
|
|
43
|
+
md: { paddingHorizontal: s(24), paddingVertical: vs(14), minHeight: 48 },
|
|
44
|
+
lg: { paddingHorizontal: s(28), paddingVertical: vs(16), minHeight: 56 },
|
|
54
45
|
}
|
|
55
46
|
|
|
56
47
|
const labelSizeStyles: Record<ButtonSize, TextStyle> = {
|
|
57
|
-
sm: { fontSize: ms(
|
|
58
|
-
md: { fontSize: ms(
|
|
59
|
-
lg: { fontSize: ms(
|
|
48
|
+
sm: { ...TYPOGRAPHY['button-sm'], fontSize: ms(TYPOGRAPHY['button-sm'].fontSize) },
|
|
49
|
+
md: { ...TYPOGRAPHY['button-lg'], fontSize: ms(TYPOGRAPHY['button-lg'].fontSize) },
|
|
50
|
+
lg: { ...TYPOGRAPHY['button-lg'], fontSize: ms(TYPOGRAPHY['button-lg'].fontSize + 1) },
|
|
60
51
|
}
|
|
61
52
|
|
|
62
53
|
const iconSizeMap: Record<ButtonSize, number> = { sm: 16, md: 18, lg: 20 }
|
|
@@ -82,12 +73,7 @@ export function Button({
|
|
|
82
73
|
|
|
83
74
|
const handlePressIn = () => {
|
|
84
75
|
if (isDisabled) return
|
|
85
|
-
Animated.spring(scale, {
|
|
86
|
-
toValue: 0.95,
|
|
87
|
-
useNativeDriver: nativeDriver,
|
|
88
|
-
speed: 40,
|
|
89
|
-
bounciness: 0,
|
|
90
|
-
}).start()
|
|
76
|
+
Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver, speed: 40, bounciness: 0 }).start()
|
|
91
77
|
}
|
|
92
78
|
|
|
93
79
|
const handlePressOut = () => {
|
|
@@ -100,18 +86,16 @@ export function Button({
|
|
|
100
86
|
}
|
|
101
87
|
|
|
102
88
|
const containerVariantStyle: ViewStyle = {
|
|
103
|
-
primary:
|
|
104
|
-
secondary:
|
|
105
|
-
|
|
106
|
-
ghost: { backgroundColor: 'transparent' },
|
|
89
|
+
primary: { backgroundColor: colors.primary },
|
|
90
|
+
secondary: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.primary },
|
|
91
|
+
text: { backgroundColor: 'transparent' },
|
|
107
92
|
destructive: { backgroundColor: colors.destructive },
|
|
108
93
|
}[variant]
|
|
109
94
|
|
|
110
95
|
const labelVariantStyle: TextStyle = {
|
|
111
|
-
primary:
|
|
112
|
-
secondary:
|
|
113
|
-
|
|
114
|
-
ghost: { color: colors.foreground },
|
|
96
|
+
primary: { color: colors.primaryForeground },
|
|
97
|
+
secondary: { color: colors.primary },
|
|
98
|
+
text: { color: colors.foreground },
|
|
115
99
|
destructive: { color: colors.destructiveForeground },
|
|
116
100
|
}[variant]
|
|
117
101
|
|
|
@@ -121,7 +105,7 @@ export function Button({
|
|
|
121
105
|
|
|
122
106
|
const spinnerColor =
|
|
123
107
|
variant === 'destructive' ? colors.destructiveForeground
|
|
124
|
-
: variant === 'primary'
|
|
108
|
+
: variant === 'primary' ? colors.primaryForeground
|
|
125
109
|
: colors.foreground
|
|
126
110
|
|
|
127
111
|
return (
|
|
@@ -148,7 +132,12 @@ export function Button({
|
|
|
148
132
|
) : (
|
|
149
133
|
<>
|
|
150
134
|
{effectiveIcon && iconPosition === 'left' && <>{effectiveIcon}</>}
|
|
151
|
-
<Text
|
|
135
|
+
<Text
|
|
136
|
+
style={[styles.label, labelVariantStyle, labelSizeStyles[size], effectiveIcon ? styles.labelWithIcon : undefined]}
|
|
137
|
+
allowFontScaling={true}
|
|
138
|
+
>
|
|
139
|
+
{label}
|
|
140
|
+
</Text>
|
|
152
141
|
{effectiveIcon && iconPosition === 'right' && <>{effectiveIcon}</>}
|
|
153
142
|
</>
|
|
154
143
|
)}
|
|
@@ -159,7 +148,7 @@ export function Button({
|
|
|
159
148
|
|
|
160
149
|
const styles = StyleSheet.create({
|
|
161
150
|
base: {
|
|
162
|
-
borderRadius:
|
|
151
|
+
borderRadius: RADIUS.xl, // 32px — pill-shaped primary CTA (Airbnb spec)
|
|
163
152
|
alignItems: 'center',
|
|
164
153
|
justifyContent: 'center',
|
|
165
154
|
flexDirection: 'row',
|
|
@@ -168,12 +157,12 @@ const styles = StyleSheet.create({
|
|
|
168
157
|
width: '100%',
|
|
169
158
|
},
|
|
170
159
|
disabled: {
|
|
171
|
-
opacity: 0.
|
|
160
|
+
opacity: 0.45,
|
|
172
161
|
},
|
|
173
162
|
label: {
|
|
174
|
-
fontFamily: 'Poppins-
|
|
163
|
+
fontFamily: 'Poppins-Medium',
|
|
175
164
|
},
|
|
176
165
|
labelWithIcon: {
|
|
177
|
-
marginHorizontal: s(
|
|
166
|
+
marginHorizontal: s(6),
|
|
178
167
|
},
|
|
179
168
|
})
|
|
@@ -89,7 +89,7 @@ export function Card({ children, variant = 'elevated', onPress, style }: CardPro
|
|
|
89
89
|
elevation: 0,
|
|
90
90
|
},
|
|
91
91
|
filled: {
|
|
92
|
-
backgroundColor: colors.
|
|
92
|
+
backgroundColor: colors.surfaceStrong,
|
|
93
93
|
borderColor: colors.border,
|
|
94
94
|
shadowOpacity: 0,
|
|
95
95
|
elevation: 0,
|
|
@@ -127,13 +127,13 @@ export function CardHeader({ children, style }: CardHeaderProps) {
|
|
|
127
127
|
|
|
128
128
|
export function CardTitle({ children, style }: CardTitleProps) {
|
|
129
129
|
const { colors } = useTheme()
|
|
130
|
-
return <Text style={[styles.title, { color: colors.
|
|
130
|
+
return <Text style={[styles.title, { color: colors.foreground }, style]} allowFontScaling={true}>{children}</Text>
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
export function CardDescription({ children, style }: CardDescriptionProps) {
|
|
134
134
|
const { colors } = useTheme()
|
|
135
135
|
return (
|
|
136
|
-
<Text style={[styles.description, { color: colors.
|
|
136
|
+
<Text style={[styles.description, { color: colors.foregroundMuted }, style]} allowFontScaling={true}>{children}</Text>
|
|
137
137
|
)
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -147,7 +147,7 @@ export function CardFooter({ children, style }: CardFooterProps) {
|
|
|
147
147
|
|
|
148
148
|
const styles = StyleSheet.create({
|
|
149
149
|
card: {
|
|
150
|
-
borderRadius:
|
|
150
|
+
borderRadius: 14, // RADIUS.md — Airbnb property card spec
|
|
151
151
|
borderWidth: 1,
|
|
152
152
|
},
|
|
153
153
|
header: {
|