@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.
Files changed (50) hide show
  1. package/COMPONENTS.md +1806 -663
  2. package/README.md +14 -10
  3. package/dist/index.d.mts +274 -85
  4. package/dist/index.d.ts +274 -85
  5. package/dist/index.js +1048 -321
  6. package/dist/index.mjs +1046 -324
  7. package/package.json +3 -2
  8. package/src/components/Accordion/Accordion.tsx +1 -1
  9. package/src/components/AlertBanner/AlertBanner.tsx +50 -45
  10. package/src/components/Avatar/Avatar.tsx +61 -17
  11. package/src/components/Badge/Badge.tsx +17 -15
  12. package/src/components/Button/Button.tsx +31 -42
  13. package/src/components/Card/Card.tsx +4 -4
  14. package/src/components/CategoryStrip/CategoryStrip.tsx +185 -0
  15. package/src/components/CategoryStrip/index.ts +2 -0
  16. package/src/components/Checkbox/Checkbox.tsx +44 -16
  17. package/src/components/Chip/Chip.tsx +1 -1
  18. package/src/components/ConfirmDialog/ConfirmDialog.tsx +9 -9
  19. package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +1 -0
  20. package/src/components/CurrencyInput/CurrencyInput.tsx +6 -4
  21. package/src/components/EmptyState/EmptyState.tsx +9 -9
  22. package/src/components/IconButton/IconButton.tsx +74 -34
  23. package/src/components/Input/Input.tsx +15 -13
  24. package/src/components/LabelValue/LabelValue.tsx +1 -1
  25. package/src/components/ListItem/ListItem.tsx +5 -5
  26. package/src/components/MediaCard/MediaCard.tsx +249 -0
  27. package/src/components/MediaCard/index.ts +2 -0
  28. package/src/components/Pressable/Pressable.tsx +100 -0
  29. package/src/components/Pressable/index.ts +1 -0
  30. package/src/components/Progress/Progress.tsx +14 -7
  31. package/src/components/RadioGroup/RadioGroup.tsx +1 -1
  32. package/src/components/Select/Select.tsx +5 -5
  33. package/src/components/Sheet/Sheet.tsx +35 -15
  34. package/src/components/Skeleton/Skeleton.tsx +34 -7
  35. package/src/components/Slider/Slider.tsx +2 -2
  36. package/src/components/Spinner/Spinner.tsx +1 -1
  37. package/src/components/Switch/Switch.tsx +31 -4
  38. package/src/components/Tabs/Tabs.tsx +63 -45
  39. package/src/components/Text/Text.tsx +59 -10
  40. package/src/components/Textarea/Textarea.tsx +4 -3
  41. package/src/components/Toast/Toast.tsx +77 -36
  42. package/src/components/Toggle/Toggle.tsx +3 -3
  43. package/src/index.ts +8 -2
  44. package/src/theme/ThemeProvider.tsx +11 -10
  45. package/src/theme/colorUtils.ts +80 -0
  46. package/src/theme/colors.ts +76 -35
  47. package/src/theme/index.ts +2 -2
  48. package/src/theme/types.ts +27 -13
  49. package/src/tokens.ts +150 -13
  50. 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": "4.0.0",
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.mutedForeground} />
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, mvs } from '../../utils/scaling'
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.destructiveBorder
30
- : variant === 'success' ? colors.successBorder
24
+ variant === 'destructive' ? colors.destructiveTint
25
+ : variant === 'success' ? colors.successTint
26
+ : variant === 'warning' ? colors.warningTint
31
27
  : colors.card
32
28
 
33
- const textColor =
34
- variant === 'destructive' ? '#991b1b'
35
- : variant === 'success' ? '#166534'
36
- : colors.foreground
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 borderColor = textColor
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={18} color={textColor} />
49
+ <FontAwesome5 name="check-circle" size={16} color={accentColor} />
43
50
  ) : variant === 'destructive' ? (
44
- <MaterialIcons name="error-outline" size={20} color={textColor} />
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={18} color={textColor} />
55
+ <Entypo name="info-with-circle" size={16} color={accentColor} />
47
56
  )
48
57
 
49
58
  const effectiveIcon: React.ReactNode = iconName
50
- ? renderIcon(iconName, 18, iconColor ?? textColor)
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.header}>
56
- <View style={styles.icon}>{effectiveIcon}</View>
57
- {title ? <Text style={[styles.title, { color: textColor }]} allowFontScaling={true}>{title}</Text> : null}
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: 'center',
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
- icon: {
85
- marginTop: 0,
85
+ iconSlot: {
86
+ marginTop: vs(1),
86
87
  },
87
- title: {
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(14),
96
- lineHeight: mvs(20),
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
- export function Avatar({ src, fallback, size = 'md', style }: AvatarProps) {
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.muted,
59
+ backgroundColor: colors.surface,
42
60
  overflow: 'hidden',
43
61
  }
44
62
 
45
63
  return (
46
- <View style={[styles.base, containerStyle, style]}>
47
- {!showFallback ? (
48
- <Image
49
- source={{ uri: src }}
50
- style={{ width: dimension, height: dimension }}
51
- onError={() => setImageError(true)}
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: { backgroundColor: colors.primary },
53
- secondary: { backgroundColor: colors.secondary },
54
- destructive: { backgroundColor: colors.destructive },
55
- outline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: colors.border },
56
- success: { backgroundColor: colors.success },
57
- warning: { backgroundColor: '#f59e0b', },
58
- successOutline: { backgroundColor: colors.successTint, borderWidth: 1, borderColor: colors.successBorder },
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: colors.primaryForeground,
64
- secondary: colors.secondaryForeground,
65
- destructive: colors.destructiveForeground,
66
- outline: colors.foreground,
67
- success: colors.successForeground,
68
- warning: '#ffffff',
69
- successOutline: colors.success,
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
- export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'
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(12), paddingVertical: vs(10), minHeight: 44 },
52
- md: { paddingHorizontal: s(16), paddingVertical: vs(10), minHeight: 44 },
53
- lg: { paddingHorizontal: s(20), paddingVertical: vs(12), minHeight: 48 },
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(13) },
58
- md: { fontSize: ms(15) },
59
- lg: { fontSize: ms(16) },
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: { backgroundColor: colors.primary },
104
- secondary: { backgroundColor: colors.secondary },
105
- outline: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.border },
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: { color: colors.primaryForeground },
112
- secondary: { color: colors.secondaryForeground },
113
- outline: { color: colors.foreground },
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' || variant === 'secondary' ? colors.primaryForeground
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 style={[styles.label, labelVariantStyle, labelSizeStyles[size], effectiveIcon ? styles.labelWithIcon : undefined]} allowFontScaling={true}>{label}</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: 8,
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.5,
160
+ opacity: 0.45,
172
161
  },
173
162
  label: {
174
- fontFamily: 'Poppins-SemiBold',
163
+ fontFamily: 'Poppins-Medium',
175
164
  },
176
165
  labelWithIcon: {
177
- marginHorizontal: s(8),
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.accent,
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.cardForeground }, style]} allowFontScaling={true}>{children}</Text>
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.mutedForeground }, style]} allowFontScaling={true}>{children}</Text>
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: ms(12),
150
+ borderRadius: 14, // RADIUS.md — Airbnb property card spec
151
151
  borderWidth: 1,
152
152
  },
153
153
  header: {