@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
@@ -3,6 +3,7 @@ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
3
  import {
4
4
  BottomSheetModal,
5
5
  BottomSheetView,
6
+ BottomSheetScrollView,
6
7
  BottomSheetBackdrop,
7
8
  BottomSheetModalProvider,
8
9
  type BottomSheetBackdropProps,
@@ -19,8 +20,12 @@ export interface SheetProps {
19
20
  title?: string
20
21
  description?: string
21
22
  children?: React.ReactNode
22
- /** Style for the inner `BottomSheetView` content container. */
23
+ /** Style for the inner content container. */
23
24
  style?: ViewStyle
25
+ /** Render children inside BottomSheetScrollView so gestures are handled correctly on both platforms. */
26
+ scrollable?: boolean
27
+ /** Cap sheet height (dp). Children scroll when content exceeds this value. */
28
+ maxHeight?: number
24
29
  }
25
30
 
26
31
  export function Sheet({
@@ -30,6 +35,8 @@ export function Sheet({
30
35
  description,
31
36
  children,
32
37
  style,
38
+ scrollable,
39
+ maxHeight,
33
40
  }: SheetProps) {
34
41
  const { colors } = useTheme()
35
42
  const ref = useRef<BottomSheetModal>(null)
@@ -52,6 +59,21 @@ export function Sheet({
52
59
  />
53
60
  )
54
61
 
62
+ const headerNode = (title || description) ? (
63
+ <View style={styles.header}>
64
+ {title ? (
65
+ <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>{title}</Text>
66
+ ) : null}
67
+ {description ? (
68
+ <Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
69
+ {description}
70
+ </Text>
71
+ ) : null}
72
+ </View>
73
+ ) : null
74
+
75
+ const useScroll = scrollable || !!maxHeight
76
+
55
77
  return (
56
78
  <BottomSheetModal
57
79
  ref={ref}
@@ -62,20 +84,18 @@ export function Sheet({
62
84
  handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
63
85
  enablePanDownToClose
64
86
  >
65
- <BottomSheetView style={[styles.content, style]}>
66
- {title || description ? (
67
- <View style={styles.header}>
68
- {title ? (
69
- <Text style={[styles.title, { color: colors.cardForeground }]} allowFontScaling={true}>{title}</Text>
70
- ) : null}
71
- {description ? (
72
- <Text style={[styles.description, { color: colors.mutedForeground }]} allowFontScaling={true}>
73
- {description}
74
- </Text>
75
- ) : null}
76
- </View>
77
- ) : null}
78
- {children}
87
+ <BottomSheetView style={maxHeight ? { maxHeight } : undefined}>
88
+ {useScroll ? (
89
+ <BottomSheetScrollView contentContainerStyle={[styles.content, style]}>
90
+ {headerNode}
91
+ {children}
92
+ </BottomSheetScrollView>
93
+ ) : (
94
+ <BottomSheetView style={[styles.content, style]}>
95
+ {headerNode}
96
+ {children}
97
+ </BottomSheetView>
98
+ )}
79
99
  </BottomSheetView>
80
100
  </BottomSheetModal>
81
101
  )
@@ -2,15 +2,30 @@ import React, { useEffect, useRef, useState } from 'react'
2
2
  import { Animated, StyleSheet, View, ViewStyle } from 'react-native'
3
3
  import { LinearGradient } from 'expo-linear-gradient'
4
4
  import { useTheme } from '../../theme'
5
+ import { s } from '../../utils/scaling'
6
+
7
+ // circle: circular avatar placeholder text: short line preset base: custom dimensions
8
+ export type SkeletonPreset = 'base' | 'circle' | 'text'
5
9
 
6
10
  export interface SkeletonProps {
7
11
  width?: number | string
8
12
  height?: number
9
13
  borderRadius?: number
14
+ /** Preset shape. `'circle'` forces width=height square with full radius. `'text'` renders a short line. */
15
+ preset?: SkeletonPreset
16
+ /** Only used with `preset='circle'` — overrides the diameter. Defaults to 40. */
17
+ diameter?: number
10
18
  style?: ViewStyle
11
19
  }
12
20
 
13
- export function Skeleton({ width = '100%', height = 16, borderRadius = 6, style }: SkeletonProps) {
21
+ export function Skeleton({
22
+ width = '100%',
23
+ height = 16,
24
+ borderRadius = 6,
25
+ preset = 'base',
26
+ diameter = 40,
27
+ style,
28
+ }: SkeletonProps) {
14
29
  const { colors, colorScheme } = useTheme()
15
30
  const shimmerAnim = useRef(new Animated.Value(0)).current
16
31
  const [containerWidth, setContainerWidth] = useState(300)
@@ -20,11 +35,7 @@ export function Skeleton({ width = '100%', height = 16, borderRadius = 6, style
20
35
 
21
36
  useEffect(() => {
22
37
  const animation = Animated.loop(
23
- Animated.timing(shimmerAnim, {
24
- toValue: 1,
25
- duration: 1200,
26
- useNativeDriver: true,
27
- })
38
+ Animated.timing(shimmerAnim, { toValue: 1, duration: 1200, useNativeDriver: true })
28
39
  )
29
40
  animation.start()
30
41
  return () => animation.stop()
@@ -35,11 +46,27 @@ export function Skeleton({ width = '100%', height = 16, borderRadius = 6, style
35
46
  outputRange: [-containerWidth, containerWidth],
36
47
  })
37
48
 
49
+ // Resolve dimensions by preset
50
+ const resolvedWidth: number | string =
51
+ preset === 'circle' ? s(diameter)
52
+ : preset === 'text' ? '60%'
53
+ : width
54
+
55
+ const resolvedHeight: number =
56
+ preset === 'circle' ? s(diameter)
57
+ : preset === 'text' ? 14
58
+ : height
59
+
60
+ const resolvedRadius: number =
61
+ preset === 'circle' ? 9999
62
+ : preset === 'text' ? 4
63
+ : borderRadius
64
+
38
65
  return (
39
66
  <View
40
67
  style={[
41
68
  styles.base,
42
- { width: width as any, height, borderRadius, backgroundColor: colors.muted },
69
+ { width: resolvedWidth as any, height: resolvedHeight, borderRadius: resolvedRadius, backgroundColor: colors.surface },
43
70
  style,
44
71
  ]}
45
72
  onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
@@ -55,7 +55,7 @@ export function Slider({
55
55
  </Text>
56
56
  ) : null}
57
57
  {showValue ? (
58
- <Text style={[styles.valueText, { color: colors.mutedForeground }]} allowFontScaling={true}>
58
+ <Text style={[styles.valueText, { color: colors.foregroundMuted }]} allowFontScaling={true}>
59
59
  {formatValue(value)}
60
60
  </Text>
61
61
  ) : null}
@@ -71,7 +71,7 @@ export function Slider({
71
71
  onValueChange={handleValueChange}
72
72
  onSlidingComplete={onSlidingComplete}
73
73
  minimumTrackTintColor={colors.primary}
74
- maximumTrackTintColor={colors.muted}
74
+ maximumTrackTintColor={colors.surface}
75
75
  thumbTintColor={colors.primary}
76
76
  style={styles.slider}
77
77
  accessibilityLabel={accessibilityLabel}
@@ -31,7 +31,7 @@ export function Spinner({ size = 'md', color, label, ...props }: SpinnerProps) {
31
31
  <View style={styles.wrapper}>
32
32
  <ActivityIndicator size={sizeMap[size]} color={color ?? colors.primary} {...props} />
33
33
  <Text
34
- style={[styles.label, { color: colors.mutedForeground, fontSize: labelFontSize[size] }]}
34
+ style={[styles.label, { color: colors.foregroundMuted, fontSize: labelFontSize[size] }]}
35
35
  allowFontScaling={true}
36
36
  >
37
37
  {label}
@@ -1,10 +1,11 @@
1
1
  import React, { useEffect, useRef } from 'react'
2
2
  import { TouchableOpacity, Animated, StyleSheet, ViewStyle, Platform, View } from 'react-native'
3
+ import { Feather } from '@expo/vector-icons'
3
4
 
4
5
  const nativeDriver = Platform.OS !== 'web'
5
6
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
6
7
  import { useTheme } from '../../theme'
7
- import { s, vs } from '../../utils/scaling'
8
+ import { s } from '../../utils/scaling'
8
9
 
9
10
  const TRACK_WIDTH = s(52)
10
11
  const TRACK_HEIGHT = s(30)
@@ -19,10 +20,14 @@ export interface SwitchProps {
19
20
  style?: ViewStyle
20
21
  }
21
22
 
23
+ const ICON_SIZE = s(13)
24
+
22
25
  export function Switch({ checked = false, onCheckedChange, disabled, style }: SwitchProps) {
23
26
  const { colors } = useTheme()
24
27
  const translateX = useRef(new Animated.Value(checked ? THUMB_TRAVEL : 0)).current
25
28
  const trackOpacity = useRef(new Animated.Value(checked ? 1 : 0)).current
29
+ const checkOpacity = useRef(new Animated.Value(checked ? 1 : 0)).current
30
+ const crossOpacity = useRef(new Animated.Value(checked ? 0 : 1)).current
26
31
 
27
32
  useEffect(() => {
28
33
  Animated.parallel([
@@ -36,12 +41,22 @@ export function Switch({ checked = false, onCheckedChange, disabled, style }: Sw
36
41
  duration: 150,
37
42
  useNativeDriver: false,
38
43
  }),
44
+ Animated.timing(checkOpacity, {
45
+ toValue: checked ? 1 : 0,
46
+ duration: 120,
47
+ useNativeDriver: true,
48
+ }),
49
+ Animated.timing(crossOpacity, {
50
+ toValue: checked ? 0 : 1,
51
+ duration: 120,
52
+ useNativeDriver: true,
53
+ }),
39
54
  ]).start()
40
- }, [checked, translateX, trackOpacity])
55
+ }, [checked, translateX, trackOpacity, checkOpacity, crossOpacity])
41
56
 
42
57
  const trackColor = trackOpacity.interpolate({
43
58
  inputRange: [0, 1],
44
- outputRange: [colors.muted, colors.primary],
59
+ outputRange: [colors.surface, colors.primary],
45
60
  })
46
61
 
47
62
  return (
@@ -62,7 +77,14 @@ export function Switch({ checked = false, onCheckedChange, disabled, style }: Sw
62
77
  styles.thumb,
63
78
  { backgroundColor: colors.primaryForeground, transform: [{ translateX }] },
64
79
  ]}
65
- />
80
+ >
81
+ <Animated.View style={[styles.iconWrapper, { opacity: checkOpacity }]}>
82
+ <Feather name="check" size={ICON_SIZE} color={colors.primary} />
83
+ </Animated.View>
84
+ <Animated.View style={[styles.iconWrapper, { opacity: crossOpacity }]}>
85
+ <Feather name="x" size={ICON_SIZE} color={colors.foregroundMuted} />
86
+ </Animated.View>
87
+ </Animated.View>
66
88
  </Animated.View>
67
89
  </TouchableOpacity>
68
90
  </View>
@@ -90,5 +112,10 @@ const styles = StyleSheet.create({
90
112
  shadowOpacity: 0.15,
91
113
  shadowRadius: 2,
92
114
  elevation: 2,
115
+ alignItems: 'center',
116
+ justifyContent: 'center',
117
+ },
118
+ iconWrapper: {
119
+ position: 'absolute',
93
120
  },
94
121
  })
@@ -12,12 +12,13 @@ export interface TabItem {
12
12
  icon?: React.ReactNode | ((active: boolean) => React.ReactNode)
13
13
  }
14
14
 
15
+ // pill: animated sliding pill background (default)
16
+ // underline: 2px bottom border on active tab — Airbnb product-tab style
17
+ export type TabsVariant = 'pill' | 'underline'
18
+
15
19
  export interface TabsProps {
16
20
  tabs: TabItem[]
17
- /**
18
- * Controlled active tab value. When omitted the component manages state internally
19
- * (uncontrolled), defaulting to the first tab.
20
- */
21
+ variant?: TabsVariant
21
22
  value?: string
22
23
  onValueChange?: (value: string) => void
23
24
  children?: React.ReactNode
@@ -36,11 +37,13 @@ function TabTrigger({
36
37
  isActive,
37
38
  onPress,
38
39
  onLayout,
40
+ variant,
39
41
  }: {
40
42
  tab: TabItem
41
43
  isActive: boolean
42
44
  onPress: () => void
43
45
  onLayout: (e: any) => void
46
+ variant: TabsVariant
44
47
  }) {
45
48
  const { colors } = useTheme()
46
49
  const scale = useRef(new Animated.Value(1)).current
@@ -53,9 +56,15 @@ function TabTrigger({
53
56
  Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, speed: 40, bounciness: 4 }).start()
54
57
  }
55
58
 
59
+ const isUnderline = variant === 'underline'
60
+
56
61
  return (
57
62
  <TouchableOpacity
58
- style={styles.trigger}
63
+ style={[
64
+ styles.trigger,
65
+ isUnderline && styles.triggerUnderline,
66
+ isUnderline && isActive && { borderBottomColor: colors.primary },
67
+ ]}
59
68
  onPress={onPress}
60
69
  onPressIn={handlePressIn}
61
70
  onPressOut={handlePressOut}
@@ -71,8 +80,8 @@ function TabTrigger({
71
80
  <Text
72
81
  style={[
73
82
  styles.triggerLabel,
74
- { color: isActive ? colors.foreground : colors.mutedForeground },
75
- isActive && styles.activeTriggerLabel,
83
+ { color: isActive ? colors.foreground : colors.foregroundMuted },
84
+ isActive && (isUnderline ? styles.activeTriggerLabelUnderline : styles.activeTriggerLabel),
76
85
  ]}
77
86
  allowFontScaling={true}
78
87
  >
@@ -84,7 +93,7 @@ function TabTrigger({
84
93
  )
85
94
  }
86
95
 
87
- export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps) {
96
+ export function Tabs({ tabs, variant = 'pill', value, onValueChange, children, style }: TabsProps) {
88
97
  const [internal, setInternal] = useState(tabs[0]?.value ?? '')
89
98
  const { colors } = useTheme()
90
99
  const active = value ?? internal
@@ -99,18 +108,8 @@ export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps)
99
108
  if (!layout) return
100
109
  if (animate) {
101
110
  Animated.parallel([
102
- Animated.spring(pillX, {
103
- toValue: layout.x,
104
- useNativeDriver: false,
105
- speed: 20,
106
- bounciness: 0,
107
- }),
108
- Animated.spring(pillWidth, {
109
- toValue: layout.width,
110
- useNativeDriver: false,
111
- speed: 20,
112
- bounciness: 0,
113
- }),
111
+ Animated.spring(pillX, { toValue: layout.x, useNativeDriver: false, speed: 20, bounciness: 0 }),
112
+ Animated.spring(pillWidth, { toValue: layout.width, useNativeDriver: false, speed: 20, bounciness: 0 }),
114
113
  ]).start()
115
114
  } else {
116
115
  pillX.setValue(layout.x)
@@ -119,9 +118,7 @@ export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps)
119
118
  }
120
119
 
121
120
  useEffect(() => {
122
- if (initialised.current) {
123
- animatePill(active, true)
124
- }
121
+ if (initialised.current) animatePill(active, true)
125
122
  }, [active])
126
123
 
127
124
  const handlePress = (v: string) => {
@@ -132,32 +129,37 @@ export function Tabs({ tabs, value, onValueChange, children, style }: TabsProps)
132
129
 
133
130
  return (
134
131
  <View style={style}>
135
- <View style={[styles.list, { backgroundColor: colors.muted }]}>
136
- <Animated.View
137
- style={[
138
- styles.pill,
139
- {
140
- backgroundColor: colors.background,
141
- position: 'absolute',
142
- top: 4,
143
- bottom: 4,
144
- left: pillX,
145
- width: pillWidth,
146
- borderRadius: 8,
147
- shadowColor: '#000',
148
- shadowOffset: { width: 0, height: 1 },
149
- shadowOpacity: 0.1,
150
- shadowRadius: 2,
151
- elevation: 2,
152
- },
153
- ]}
154
- />
132
+ <View style={[
133
+ variant === 'pill' ? [styles.list, { backgroundColor: colors.surface }] : styles.listUnderline,
134
+ ]}>
135
+ {variant === 'pill' && (
136
+ <Animated.View
137
+ style={[
138
+ styles.pill,
139
+ {
140
+ backgroundColor: colors.background,
141
+ position: 'absolute',
142
+ top: 4,
143
+ bottom: 4,
144
+ left: pillX,
145
+ width: pillWidth,
146
+ borderRadius: 8,
147
+ shadowColor: '#000',
148
+ shadowOffset: { width: 0, height: 1 },
149
+ shadowOpacity: 0.08,
150
+ shadowRadius: 2,
151
+ elevation: 2,
152
+ },
153
+ ]}
154
+ />
155
+ )}
155
156
  {tabs.map((tab) => (
156
157
  <TabTrigger
157
158
  key={tab.value}
158
159
  tab={tab}
159
160
  isActive={tab.value === active}
160
161
  onPress={() => handlePress(tab.value)}
162
+ variant={variant}
161
163
  onLayout={(e) => {
162
164
  const { x, width } = e.nativeEvent.layout
163
165
  tabLayouts.current[tab.value] = { x, width }
@@ -182,20 +184,32 @@ export function TabsContent({ value, activeValue, children, style }: TabsContent
182
184
  const styles = StyleSheet.create({
183
185
  list: {
184
186
  flexDirection: 'row',
185
- borderRadius: ms(12),
187
+ borderRadius: 12,
186
188
  padding: s(4),
187
189
  gap: s(4),
188
190
  },
191
+ listUnderline: {
192
+ flexDirection: 'row',
193
+ borderBottomWidth: 1,
194
+ },
189
195
  pill: {},
190
196
  trigger: {
191
197
  flex: 1,
192
198
  paddingVertical: vs(7),
193
199
  paddingHorizontal: s(10),
194
- borderRadius: ms(6),
200
+ borderRadius: 8,
195
201
  alignItems: 'center',
196
202
  justifyContent: 'center',
197
203
  zIndex: 1,
198
204
  },
205
+ triggerUnderline: {
206
+ flex: 0,
207
+ paddingVertical: vs(12),
208
+ paddingHorizontal: s(16),
209
+ borderRadius: 0,
210
+ borderBottomWidth: 2,
211
+ borderBottomColor: 'transparent',
212
+ },
199
213
  triggerInner: {
200
214
  flexDirection: 'row',
201
215
  alignItems: 'center',
@@ -209,4 +223,8 @@ const styles = StyleSheet.create({
209
223
  activeTriggerLabel: {
210
224
  fontFamily: 'Poppins-Medium',
211
225
  },
226
+ activeTriggerLabelUnderline: {
227
+ fontFamily: 'Poppins-SemiBold',
228
+ fontSize: ms(14),
229
+ },
212
230
  })
@@ -1,32 +1,81 @@
1
1
  import React from 'react'
2
2
  import { Text as RNText, TextProps as RNTextProps, TextStyle } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
+ import { TYPOGRAPHY } from '../../tokens'
4
5
  import { ms, mvs } from '../../utils/scaling'
5
6
 
6
- export type TextVariant = 'h1' | 'h2' | 'h3' | 'body' | 'caption' | 'label'
7
+ export type TextVariant =
8
+ | 'display-hero'
9
+ | 'display-xl'
10
+ | 'display-lg'
11
+ | 'display-md'
12
+ | 'display-sm'
13
+ | 'title-md'
14
+ | 'title-sm'
15
+ | 'body-md'
16
+ | 'body-sm'
17
+ | 'caption'
18
+ | 'caption-sm'
19
+ | 'badge-text'
20
+ | 'micro-label'
21
+ | 'uppercase-tag'
22
+ | 'button-lg'
23
+ | 'button-sm'
7
24
 
8
25
  export interface TextProps extends RNTextProps {
9
26
  variant?: TextVariant
10
27
  color?: string
11
28
  }
12
29
 
30
+ // Apply scaling to font/line-height values while preserving all other token props
13
31
  const variantStyles: Record<TextVariant, TextStyle> = {
14
- h1: { fontFamily: 'Poppins-Bold', fontSize: ms(40), lineHeight: mvs(52) },
15
- h2: { fontFamily: 'Poppins-Bold', fontSize: ms(28), lineHeight: mvs(36) },
16
- h3: { fontFamily: 'Poppins-SemiBold', fontSize: ms(22), lineHeight: mvs(30) },
17
- body: { fontFamily: 'Poppins-Regular', fontSize: ms(17), lineHeight: mvs(26) },
18
- caption: { fontFamily: 'Poppins-Regular', fontSize: ms(13), lineHeight: mvs(20) },
19
- label: { fontFamily: 'Poppins-Medium', fontSize: ms(15), lineHeight: mvs(22) },
32
+ 'display-hero': { ...TYPOGRAPHY['display-hero'], fontSize: ms(TYPOGRAPHY['display-hero'].fontSize), lineHeight: mvs(TYPOGRAPHY['display-hero'].lineHeight) },
33
+ 'display-xl': { ...TYPOGRAPHY['display-xl'], fontSize: ms(TYPOGRAPHY['display-xl'].fontSize), lineHeight: mvs(TYPOGRAPHY['display-xl'].lineHeight) },
34
+ 'display-lg': { ...TYPOGRAPHY['display-lg'], fontSize: ms(TYPOGRAPHY['display-lg'].fontSize), lineHeight: mvs(TYPOGRAPHY['display-lg'].lineHeight) },
35
+ 'display-md': { ...TYPOGRAPHY['display-md'], fontSize: ms(TYPOGRAPHY['display-md'].fontSize), lineHeight: mvs(TYPOGRAPHY['display-md'].lineHeight) },
36
+ 'display-sm': { ...TYPOGRAPHY['display-sm'], fontSize: ms(TYPOGRAPHY['display-sm'].fontSize), lineHeight: mvs(TYPOGRAPHY['display-sm'].lineHeight) },
37
+ 'title-md': { ...TYPOGRAPHY['title-md'], fontSize: ms(TYPOGRAPHY['title-md'].fontSize), lineHeight: mvs(TYPOGRAPHY['title-md'].lineHeight) },
38
+ 'title-sm': { ...TYPOGRAPHY['title-sm'], fontSize: ms(TYPOGRAPHY['title-sm'].fontSize), lineHeight: mvs(TYPOGRAPHY['title-sm'].lineHeight) },
39
+ 'body-md': { ...TYPOGRAPHY['body-md'], fontSize: ms(TYPOGRAPHY['body-md'].fontSize), lineHeight: mvs(TYPOGRAPHY['body-md'].lineHeight) },
40
+ 'body-sm': { ...TYPOGRAPHY['body-sm'], fontSize: ms(TYPOGRAPHY['body-sm'].fontSize), lineHeight: mvs(TYPOGRAPHY['body-sm'].lineHeight) },
41
+ caption: { ...TYPOGRAPHY['caption'], fontSize: ms(TYPOGRAPHY['caption'].fontSize), lineHeight: mvs(TYPOGRAPHY['caption'].lineHeight) },
42
+ 'caption-sm': { ...TYPOGRAPHY['caption-sm'], fontSize: ms(TYPOGRAPHY['caption-sm'].fontSize), lineHeight: mvs(TYPOGRAPHY['caption-sm'].lineHeight) },
43
+ 'badge-text': { ...TYPOGRAPHY['badge-text'], fontSize: ms(TYPOGRAPHY['badge-text'].fontSize), lineHeight: mvs(TYPOGRAPHY['badge-text'].lineHeight) },
44
+ 'micro-label': { ...TYPOGRAPHY['micro-label'], fontSize: ms(TYPOGRAPHY['micro-label'].fontSize), lineHeight: mvs(TYPOGRAPHY['micro-label'].lineHeight) },
45
+ 'uppercase-tag':{ ...TYPOGRAPHY['uppercase-tag'],fontSize: ms(TYPOGRAPHY['uppercase-tag'].fontSize),lineHeight: mvs(TYPOGRAPHY['uppercase-tag'].lineHeight) },
46
+ 'button-lg': { ...TYPOGRAPHY['button-lg'], fontSize: ms(TYPOGRAPHY['button-lg'].fontSize), lineHeight: mvs(TYPOGRAPHY['button-lg'].lineHeight) },
47
+ 'button-sm': { ...TYPOGRAPHY['button-sm'], fontSize: ms(TYPOGRAPHY['button-sm'].fontSize), lineHeight: mvs(TYPOGRAPHY['button-sm'].lineHeight) },
20
48
  }
21
49
 
22
- export function Text({ variant = 'body', color, style, children, ...props }: TextProps) {
50
+ // Default color by variant hierarchy matches Airbnb ink/body/muted pattern
51
+ const defaultColorVariant: Partial<Record<TextVariant, 'foreground' | 'foregroundSubtle' | 'foregroundMuted'>> = {
52
+ 'display-hero': 'foreground',
53
+ 'display-xl': 'foreground',
54
+ 'display-lg': 'foreground',
55
+ 'display-md': 'foreground',
56
+ 'display-sm': 'foreground',
57
+ 'title-md': 'foreground',
58
+ 'title-sm': 'foreground',
59
+ 'body-md': 'foregroundSubtle', // running text — slightly softer
60
+ 'body-sm': 'foregroundSubtle',
61
+ caption: 'foregroundMuted',
62
+ 'caption-sm': 'foregroundMuted',
63
+ 'badge-text': 'foreground',
64
+ 'micro-label': 'foreground',
65
+ 'uppercase-tag':'foregroundMuted',
66
+ 'button-lg': 'foreground',
67
+ 'button-sm': 'foreground',
68
+ }
69
+
70
+ export function Text({ variant = 'body-md', color, style, children, ...props }: TextProps) {
23
71
  const { colors } = useTheme()
24
72
 
25
- const defaultColor = variant === 'caption' ? colors.mutedForeground : colors.foreground
73
+ const colorKey = defaultColorVariant[variant] ?? 'foreground'
74
+ const resolvedColor = color ?? colors[colorKey]
26
75
 
27
76
  return (
28
77
  <RNText
29
- style={[variantStyles[variant], { color: color ?? defaultColor }, style]}
78
+ style={[variantStyles[variant], { color: resolvedColor }, style]}
30
79
  allowFontScaling={true}
31
80
  {...props}
32
81
  >
@@ -64,7 +64,7 @@ export function Textarea({
64
64
  setFocused(false)
65
65
  onBlur?.(e)
66
66
  }}
67
- placeholderTextColor={colors.mutedForeground}
67
+ placeholderTextColor={colors.foregroundMuted}
68
68
  allowFontScaling={true}
69
69
  {...props}
70
70
  />
@@ -72,7 +72,7 @@ export function Textarea({
72
72
  <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
73
73
  ) : null}
74
74
  {!error && hint ? (
75
- <Text style={[styles.helperText, { color: colors.mutedForeground }]} allowFontScaling={true}>{hint}</Text>
75
+ <Text style={[styles.helperText, { color: colors.foregroundMuted }]} allowFontScaling={true}>{hint}</Text>
76
76
  ) : null}
77
77
  </View>
78
78
  )
@@ -88,11 +88,12 @@ const styles = StyleSheet.create({
88
88
  },
89
89
  input: {
90
90
  fontFamily: 'Poppins-Regular',
91
- borderWidth: 1,
91
+ borderWidth: 2,
92
92
  borderRadius: ms(8),
93
93
  paddingHorizontal: s(14),
94
94
  paddingVertical: vs(11),
95
95
  fontSize: ms(15),
96
+ includeFontPadding: false,
96
97
  },
97
98
  helperText: {
98
99
  fontFamily: 'Poppins-Regular',