@retray-dev/ui-kit 3.1.0 → 5.1.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 +1792 -659
  2. package/README.md +8 -7
  3. package/dist/index.d.mts +269 -89
  4. package/dist/index.d.ts +269 -89
  5. package/dist/index.js +1034 -312
  6. package/dist/index.mjs +1031 -314
  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 +4 -4
  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 +3 -9
  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
@@ -0,0 +1,185 @@
1
+ import React, { useRef } from 'react'
2
+ import {
3
+ ScrollView,
4
+ TouchableOpacity,
5
+ Animated,
6
+ Text,
7
+ View,
8
+ StyleSheet,
9
+ ViewStyle,
10
+ Platform,
11
+ } from 'react-native'
12
+ import { selectionAsync as hapticSelection } from '../../utils/haptics'
13
+ import { useTheme } from '../../theme'
14
+ import { s, vs, ms } from '../../utils/scaling'
15
+ import { renderIcon } from '../../utils/icons'
16
+ import { RADIUS } from '../../tokens'
17
+
18
+ const nativeDriver = Platform.OS !== 'web'
19
+
20
+ export interface CategoryItem {
21
+ label: string
22
+ value: string
23
+ /** Icon rendered to the left of the label. ReactNode or icon name string. */
24
+ icon?: React.ReactNode | string
25
+ /** Badge count over the icon/label. */
26
+ badge?: number
27
+ }
28
+
29
+ export interface CategoryStripProps {
30
+ categories: CategoryItem[]
31
+ value?: string | string[]
32
+ /** Called with new value(s) on selection change. */
33
+ onValueChange?: (value: string | string[]) => void
34
+ /** Allow multiple simultaneous selections. Defaults to false. */
35
+ multiSelect?: boolean
36
+ style?: ViewStyle
37
+ /** Style applied to each pill item. */
38
+ itemStyle?: ViewStyle
39
+ }
40
+
41
+ function CategoryChip({
42
+ item,
43
+ selected,
44
+ onPress,
45
+ }: {
46
+ item: CategoryItem
47
+ selected: boolean
48
+ onPress: () => void
49
+ }) {
50
+ const { colors } = useTheme()
51
+ const scale = useRef(new Animated.Value(1)).current
52
+
53
+ const handlePressIn = () => {
54
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver, speed: 40, bounciness: 0 }).start()
55
+ }
56
+
57
+ const handlePressOut = () => {
58
+ Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, speed: 40, bounciness: 4 }).start()
59
+ }
60
+
61
+ const bgColor = selected ? colors.primary : colors.surface
62
+ const textColor = selected ? colors.primaryForeground : colors.foregroundSubtle
63
+ const borderColor = selected ? colors.primary : colors.border
64
+
65
+ const resolvedIcon =
66
+ typeof item.icon === 'string'
67
+ ? renderIcon(item.icon, 16, textColor)
68
+ : item.icon ?? null
69
+
70
+ return (
71
+ <Animated.View style={{ transform: [{ scale }] }}>
72
+ <TouchableOpacity
73
+ style={[
74
+ styles.chip,
75
+ {
76
+ backgroundColor: bgColor,
77
+ borderColor,
78
+ },
79
+ ]}
80
+ onPress={onPress}
81
+ onPressIn={handlePressIn}
82
+ onPressOut={handlePressOut}
83
+ activeOpacity={1}
84
+ touchSoundDisabled={true}
85
+ >
86
+ {resolvedIcon && <View style={styles.chipIcon}>{resolvedIcon}</View>}
87
+ <Text style={[styles.chipLabel, { color: textColor }]} allowFontScaling={true}>
88
+ {item.label}
89
+ </Text>
90
+ {item.badge !== undefined && item.badge > 0 && (
91
+ <View style={[styles.chipBadge, { backgroundColor: colors.primary }]}>
92
+ <Text style={[styles.chipBadgeText, { color: colors.primaryForeground }]}>
93
+ {Math.min(item.badge, 99)}
94
+ </Text>
95
+ </View>
96
+ )}
97
+ </TouchableOpacity>
98
+ </Animated.View>
99
+ )
100
+ }
101
+
102
+ export function CategoryStrip({
103
+ categories,
104
+ value,
105
+ onValueChange,
106
+ multiSelect = false,
107
+ style,
108
+ itemStyle,
109
+ }: CategoryStripProps) {
110
+ const selected = Array.isArray(value) ? value : value ? [value] : []
111
+
112
+ const handlePress = (v: string) => {
113
+ hapticSelection()
114
+ if (multiSelect) {
115
+ const current = Array.isArray(value) ? value : value ? [value] : []
116
+ const next = current.includes(v)
117
+ ? current.filter((x) => x !== v)
118
+ : [...current, v]
119
+ onValueChange?.(next)
120
+ } else {
121
+ onValueChange?.(v === value ? '' : v)
122
+ }
123
+ }
124
+
125
+ return (
126
+ <ScrollView
127
+ horizontal
128
+ showsHorizontalScrollIndicator={false}
129
+ contentContainerStyle={[styles.container, style]}
130
+ style={styles.scroll}
131
+ >
132
+ {categories.map((cat) => (
133
+ <View key={cat.value} style={itemStyle}>
134
+ <CategoryChip
135
+ item={cat}
136
+ selected={selected.includes(cat.value)}
137
+ onPress={() => handlePress(cat.value)}
138
+ />
139
+ </View>
140
+ ))}
141
+ </ScrollView>
142
+ )
143
+ }
144
+
145
+ const styles = StyleSheet.create({
146
+ scroll: {
147
+ flexGrow: 0,
148
+ },
149
+ container: {
150
+ flexDirection: 'row',
151
+ gap: s(8),
152
+ paddingHorizontal: s(4),
153
+ paddingVertical: vs(4),
154
+ },
155
+ chip: {
156
+ flexDirection: 'row',
157
+ alignItems: 'center',
158
+ borderRadius: RADIUS.full,
159
+ borderWidth: 1,
160
+ paddingHorizontal: s(14),
161
+ paddingVertical: vs(8),
162
+ gap: s(6),
163
+ },
164
+ chipIcon: {
165
+ alignItems: 'center',
166
+ justifyContent: 'center',
167
+ },
168
+ chipLabel: {
169
+ fontFamily: 'Poppins-Medium',
170
+ fontSize: ms(13),
171
+ },
172
+ chipBadge: {
173
+ minWidth: 16,
174
+ height: 16,
175
+ borderRadius: 9999,
176
+ paddingHorizontal: 3,
177
+ alignItems: 'center',
178
+ justifyContent: 'center',
179
+ },
180
+ chipBadgeText: {
181
+ fontFamily: 'Poppins-Bold',
182
+ fontSize: ms(9),
183
+ lineHeight: 14,
184
+ },
185
+ })
@@ -0,0 +1,2 @@
1
+ export { CategoryStrip } from './CategoryStrip'
2
+ export type { CategoryStripProps, CategoryItem } from './CategoryStrip'
@@ -1,4 +1,4 @@
1
- import React, { useRef } from 'react'
1
+ import React, { useRef, useEffect } from 'react'
2
2
  import { TouchableOpacity, Animated, View, Text, StyleSheet, ViewStyle, Platform } from 'react-native'
3
3
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
4
4
 
@@ -23,6 +23,33 @@ export function Checkbox({
23
23
  }: CheckboxProps) {
24
24
  const { colors } = useTheme()
25
25
  const scale = useRef(new Animated.Value(1)).current
26
+ const bgOpacity = useRef(new Animated.Value(checked ? 1 : 0)).current
27
+ const checkOpacity = useRef(new Animated.Value(checked ? 1 : 0)).current
28
+
29
+ useEffect(() => {
30
+ Animated.parallel([
31
+ Animated.timing(bgOpacity, {
32
+ toValue: checked ? 1 : 0,
33
+ duration: 150,
34
+ useNativeDriver: false,
35
+ }),
36
+ Animated.timing(checkOpacity, {
37
+ toValue: checked ? 1 : 0,
38
+ duration: 120,
39
+ useNativeDriver: false,
40
+ }),
41
+ ]).start()
42
+ }, [checked, bgOpacity, checkOpacity])
43
+
44
+ const borderColor = bgOpacity.interpolate({
45
+ inputRange: [0, 1],
46
+ outputRange: [colors.border, colors.primary],
47
+ })
48
+
49
+ const backgroundColor = bgOpacity.interpolate({
50
+ inputRange: [0, 1],
51
+ outputRange: ['transparent', colors.primary],
52
+ })
26
53
 
27
54
  const handlePressIn = () => {
28
55
  if (disabled) return
@@ -46,24 +73,25 @@ export function Checkbox({
46
73
  activeOpacity={1}
47
74
  touchSoundDisabled={true}
48
75
  >
49
- <Animated.View
50
- style={[
51
- styles.box,
52
- {
53
- borderColor: checked ? colors.primary : colors.border,
54
- backgroundColor: checked ? colors.primary : 'transparent',
55
- opacity: disabled ? 0.45 : 1,
56
- transform: [{ scale }],
57
- },
58
- ]}
59
- >
60
- {checked ? (
61
- <View style={[styles.checkmark, { borderColor: colors.primaryForeground }]} />
62
- ) : null}
76
+ <Animated.View style={{ transform: [{ scale }] }}>
77
+ <Animated.View
78
+ style={[
79
+ styles.box,
80
+ {
81
+ borderColor,
82
+ backgroundColor,
83
+ opacity: disabled ? 0.45 : 1,
84
+ },
85
+ ]}
86
+ >
87
+ <Animated.View style={{ opacity: checkOpacity }}>
88
+ <View style={[styles.checkmark, { borderColor: colors.primaryForeground }]} />
89
+ </Animated.View>
90
+ </Animated.View>
63
91
  </Animated.View>
64
92
  {label ? (
65
93
  <Text
66
- style={[styles.label, { color: disabled ? colors.mutedForeground : colors.foreground }]}
94
+ style={[styles.label, { color: disabled ? colors.foregroundMuted : colors.foreground }]}
67
95
  allowFontScaling={true}
68
96
  >
69
97
  {label}
@@ -80,7 +80,7 @@ export function Chip({ label, selected = false, onPress, icon, iconName, style }
80
80
 
81
81
  const backgroundColor = pressAnim.interpolate({
82
82
  inputRange: [0, 1],
83
- outputRange: [colors.secondary, colors.primary],
83
+ outputRange: [colors.surface, colors.primary],
84
84
  })
85
85
 
86
86
  const textColor = pressAnim.interpolate({
@@ -57,7 +57,7 @@ export function ConfirmDialog({
57
57
  return (
58
58
  <BottomSheetModal
59
59
  ref={ref}
60
- snapPoints={['35%']}
60
+ enableDynamicSizing
61
61
  onDismiss={onCancel}
62
62
  backdropComponent={renderBackdrop}
63
63
  backgroundStyle={[styles.background, { backgroundColor: colors.card }]}
@@ -65,18 +65,18 @@ export function ConfirmDialog({
65
65
  enablePanDownToClose
66
66
  >
67
67
  <BottomSheetView style={styles.content}>
68
- <Text style={[styles.title, { color: colors.cardForeground }]} allowFontScaling={true}>
68
+ <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
69
69
  {title}
70
70
  </Text>
71
71
  {description ? (
72
- <Text style={[styles.description, { color: colors.mutedForeground }]} allowFontScaling={true}>
72
+ <Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
73
73
  {description}
74
74
  </Text>
75
75
  ) : null}
76
76
  <View style={styles.actions}>
77
77
  <Button
78
78
  label={cancelLabel}
79
- variant="outline"
79
+ variant="secondary"
80
80
  fullWidth
81
81
  onPress={onCancel}
82
82
  icon={<Feather name="x" size={15} color={colors.foreground} />}
@@ -45,5 +45,6 @@ const styles = StyleSheet.create({
45
45
  amount: {
46
46
  fontFamily: 'Poppins-Bold',
47
47
  fontSize: ms(56),
48
+ letterSpacing: -2,
48
49
  },
49
50
  })
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { ViewStyle, TextStyle } from 'react-native'
3
3
  import { Input } from '../Input'
4
- import { ms } from '../../utils/scaling'
4
+ import { ms, vs } from '../../utils/scaling'
5
5
  import { renderIcon } from '../../utils/icons'
6
6
  import { useTheme } from '../../theme'
7
7
 
@@ -58,9 +58,11 @@ export function CurrencyInput({
58
58
  onChangeValue?.(isNaN(raw) ? 0 : raw)
59
59
  }
60
60
 
61
- const inputStyle: TextStyle = size === 'large' ? { fontFamily: 'Poppins-Regular', fontSize: ms(36) } : { fontFamily: 'Poppins-Regular' }
61
+ const inputStyle: TextStyle = size === 'large'
62
+ ? { fontFamily: 'Poppins-Regular', fontSize: ms(36) }
63
+ : { fontFamily: 'Poppins-Regular' }
62
64
 
63
- const dollarIcon = renderIcon('dollar-sign', size === 'large' ? 24 : 16, colors.mutedForeground)
65
+ const dollarIcon = renderIcon('dollar-sign', size === 'large' ? 24 : 16, colors.foregroundMuted)
64
66
 
65
67
  // Remove prefix from display value if present
66
68
  const displayValue = value && prefix && value.startsWith(prefix) ? value.slice(prefix.length) : value
@@ -77,7 +79,7 @@ export function CurrencyInput({
77
79
  editable={editable}
78
80
  prefix={dollarIcon}
79
81
  containerStyle={containerStyle}
80
- inputWrapperStyle={size === 'large' ? { paddingVertical: 10 } : undefined}
82
+ inputWrapperStyle={size === 'large' ? { paddingVertical: vs(16), minHeight: 72 } : undefined}
81
83
  style={[inputStyle, style]}
82
84
  />
83
85
  )
@@ -26,7 +26,7 @@ export function EmptyState({ icon, iconName, iconColor, title, description, acti
26
26
  const isCompact = size === 'compact'
27
27
 
28
28
  const effectiveIcon: React.ReactNode = iconName
29
- ? renderIcon(iconName, isCompact ? 32 : 48, iconColor ?? colors.mutedForeground)
29
+ ? renderIcon(iconName, isCompact ? 32 : 48, iconColor ?? colors.foregroundMuted)
30
30
  : icon
31
31
 
32
32
  return (
@@ -43,7 +43,7 @@ export function EmptyState({ icon, iconName, iconColor, title, description, acti
43
43
  style={[
44
44
  styles.iconWrapper,
45
45
  isCompact && styles.iconWrapperCompact,
46
- { backgroundColor: colors.muted },
46
+ { backgroundColor: colors.surface },
47
47
  ]}
48
48
  >
49
49
  {effectiveIcon}
@@ -57,7 +57,7 @@ export function EmptyState({ icon, iconName, iconColor, title, description, acti
57
57
  {title}
58
58
  </Text>
59
59
  {description && !isCompact ? (
60
- <Text style={[styles.description, { color: colors.mutedForeground }]} allowFontScaling={true}>{description}</Text>
60
+ <Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>{description}</Text>
61
61
  ) : null}
62
62
  </View>
63
63
  {action && !isCompact ? <View style={styles.action}>{action}</View> : null}
@@ -80,16 +80,16 @@ const styles = StyleSheet.create({
80
80
  gap: vs(10),
81
81
  },
82
82
  iconWrapper: {
83
- width: s(48),
84
- height: s(48),
85
- borderRadius: ms(12),
83
+ width: s(80),
84
+ height: s(80),
85
+ borderRadius: ms(20),
86
86
  alignItems: 'center',
87
87
  justifyContent: 'center',
88
88
  },
89
89
  iconWrapperCompact: {
90
- width: s(36),
91
- height: s(36),
92
- borderRadius: ms(8),
90
+ width: s(56),
91
+ height: s(56),
92
+ borderRadius: ms(14),
93
93
  },
94
94
  textWrapper: {
95
95
  alignItems: 'center',
@@ -4,6 +4,8 @@ import {
4
4
  Animated,
5
5
  ActivityIndicator,
6
6
  StyleSheet,
7
+ View,
8
+ Text,
7
9
  TouchableOpacityProps,
8
10
  ViewStyle,
9
11
  Platform,
@@ -12,30 +14,33 @@ import {
12
14
  const nativeDriver = Platform.OS !== 'web'
13
15
  import { impactLight } from '../../utils/haptics'
14
16
  import { useTheme } from '../../theme'
15
- import { s } from '../../utils/scaling'
17
+ import { s, ms } from '../../utils/scaling'
16
18
  import { renderIcon } from '../../utils/icons'
17
19
 
18
- export type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'
20
+ // primary: filled primary
21
+ // secondary: filled surface — icon on neutral bg (Airbnb icon-button-circle)
22
+ // outline: transparent + border
23
+ // text: fully transparent
24
+ // destructive: filled destructive
25
+ export type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'text' | 'destructive'
19
26
  export type IconButtonSize = 'sm' | 'md' | 'lg'
20
27
 
21
28
  export interface IconButtonProps extends TouchableOpacityProps {
22
- /**
23
- * Icon name from `@expo/vector-icons` (e.g. `"home"`, `"star"`, `"plus"`).
24
- * See https://icons.expo.fyi. Takes precedence over `icon` when both supplied.
25
- */
26
29
  iconName?: string
27
- /** ReactNode icon — used when `iconName` is not provided. */
28
30
  icon?: React.ReactNode
29
- /** Override the resolved icon color. Defaults to the foreground color for the active variant. */
30
31
  iconColor?: string
31
32
  variant?: IconButtonVariant
32
33
  size?: IconButtonSize
33
- /** Replaces icon with a spinner and forces `disabled`. */
34
34
  loading?: boolean
35
+ /**
36
+ * Badge overlay. `true` shows a dot. A number shows a count (capped at 99).
37
+ * The dot/count appears top-right of the button.
38
+ */
39
+ badge?: boolean | number
35
40
  }
36
41
 
37
42
  const sizeMap: Record<IconButtonSize, { container: number; icon: number }> = {
38
- sm: { container: s(40), icon: 18 },
43
+ sm: { container: s(32), icon: 16 },
39
44
  md: { container: s(44), icon: 20 },
40
45
  lg: { container: s(52), icon: 24 },
41
46
  }
@@ -47,6 +52,7 @@ export function IconButton({
47
52
  variant = 'primary',
48
53
  size = 'md',
49
54
  loading = false,
55
+ badge,
50
56
  disabled,
51
57
  style,
52
58
  onPress,
@@ -58,21 +64,11 @@ export function IconButton({
58
64
 
59
65
  const handlePressIn = () => {
60
66
  if (isDisabled) return
61
- Animated.spring(scale, {
62
- toValue: 0.95,
63
- useNativeDriver: nativeDriver,
64
- speed: 40,
65
- bounciness: 0,
66
- }).start()
67
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver, speed: 40, bounciness: 0 }).start()
67
68
  }
68
69
 
69
70
  const handlePressOut = () => {
70
- Animated.spring(scale, {
71
- toValue: 1,
72
- useNativeDriver: nativeDriver,
73
- speed: 40,
74
- bounciness: 4,
75
- }).start()
71
+ Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, speed: 40, bounciness: 4 }).start()
76
72
  }
77
73
 
78
74
  const handlePress: TouchableOpacityProps['onPress'] = (e) => {
@@ -81,24 +77,24 @@ export function IconButton({
81
77
  }
82
78
 
83
79
  const containerVariantStyle: ViewStyle = {
84
- primary: { backgroundColor: colors.primary },
85
- secondary: { backgroundColor: colors.secondary },
86
- outline: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.border },
87
- ghost: { backgroundColor: 'transparent' },
80
+ primary: { backgroundColor: colors.primary },
81
+ secondary: { backgroundColor: colors.surface },
82
+ outline: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.border },
83
+ text: { backgroundColor: 'transparent' },
88
84
  destructive: { backgroundColor: colors.destructive },
89
85
  }[variant]
90
86
 
91
87
  const defaultIconColor: string = {
92
- primary: colors.primaryForeground,
93
- secondary: colors.secondaryForeground,
94
- outline: colors.foreground,
95
- ghost: colors.foreground,
88
+ primary: colors.primaryForeground,
89
+ secondary: colors.foreground,
90
+ outline: colors.foreground,
91
+ text: colors.foreground,
96
92
  destructive: colors.destructiveForeground,
97
93
  }[variant]
98
94
 
99
95
  const spinnerColor =
100
96
  variant === 'destructive' ? colors.destructiveForeground
101
- : variant === 'primary' || variant === 'secondary' ? colors.primaryForeground
97
+ : variant === 'primary' ? colors.primaryForeground
102
98
  : colors.foreground
103
99
 
104
100
  const { container: containerSize, icon: iconSize } = sizeMap[size]
@@ -107,8 +103,13 @@ export function IconButton({
107
103
  ? renderIcon(iconName, iconSize, iconColor ?? defaultIconColor)
108
104
  : icon
109
105
 
106
+ // Badge rendering
107
+ const showBadge = badge !== undefined && badge !== false && badge !== 0
108
+ const badgeCount = typeof badge === 'number' ? Math.min(badge, 99) : null
109
+ const showCount = typeof badge === 'number' && badge > 0
110
+
110
111
  return (
111
- <Animated.View style={{ transform: [{ scale }] }}>
112
+ <Animated.View style={[styles.wrapper, { transform: [{ scale }] }]}>
112
113
  <TouchableOpacity
113
114
  style={[
114
115
  styles.base,
@@ -131,17 +132,56 @@ export function IconButton({
131
132
  resolvedIcon
132
133
  )}
133
134
  </TouchableOpacity>
135
+ {showBadge && (
136
+ <View style={[
137
+ styles.badge,
138
+ { backgroundColor: colors.primary },
139
+ showCount ? styles.badgeCount : styles.badgeDot,
140
+ ]}>
141
+ {showCount && (
142
+ <Text style={[styles.badgeText, { color: colors.primaryForeground }]}>
143
+ {badgeCount}
144
+ </Text>
145
+ )}
146
+ </View>
147
+ )}
134
148
  </Animated.View>
135
149
  )
136
150
  }
137
151
 
138
152
  const styles = StyleSheet.create({
153
+ wrapper: {
154
+ alignSelf: 'flex-start',
155
+ },
139
156
  base: {
140
- borderRadius: 999,
157
+ borderRadius: 9999,
141
158
  alignItems: 'center',
142
159
  justifyContent: 'center',
143
160
  },
144
161
  disabled: {
145
- opacity: 0.5,
162
+ opacity: 0.45,
163
+ },
164
+ badge: {
165
+ position: 'absolute',
166
+ top: -2,
167
+ right: -2,
168
+ alignItems: 'center',
169
+ justifyContent: 'center',
170
+ },
171
+ badgeDot: {
172
+ width: 8,
173
+ height: 8,
174
+ borderRadius: 9999,
175
+ },
176
+ badgeCount: {
177
+ minWidth: 16,
178
+ height: 16,
179
+ borderRadius: 9999,
180
+ paddingHorizontal: 3,
181
+ },
182
+ badgeText: {
183
+ fontFamily: 'Poppins-Bold',
184
+ fontSize: ms(9),
185
+ lineHeight: 14,
146
186
  },
147
187
  })
@@ -55,16 +55,16 @@ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixS
55
55
  const effectiveSecure = isPassword ? !showPassword : secureTextEntry
56
56
 
57
57
  const effectivePrefix: React.ReactNode = prefixIcon
58
- ? renderIcon(prefixIcon, 20, prefixIconColor ?? colors.mutedForeground)
58
+ ? renderIcon(prefixIcon, 20, prefixIconColor ?? colors.foregroundMuted)
59
59
  : prefix
60
60
 
61
61
  // If type is password and no suffix override is provided, add the toggle button
62
62
  const effectiveSuffix: React.ReactNode = isPassword && !suffix && !suffixIcon ? (
63
63
  <TouchableOpacity onPress={() => setShowPassword(!showPassword)} style={styles.passwordToggle} activeOpacity={0.6}>
64
- <AntDesign name={showPassword ? 'eye' : 'eye-invisible'} size={20} color={colors.mutedForeground} />
64
+ <AntDesign name={showPassword ? 'eye' : 'eye-invisible'} size={20} color={colors.foregroundMuted} />
65
65
  </TouchableOpacity>
66
66
  ) : suffixIcon
67
- ? renderIcon(suffixIcon, 20, suffixIconColor ?? colors.mutedForeground)
67
+ ? renderIcon(suffixIcon, 20, suffixIconColor ?? colors.foregroundMuted)
68
68
  : suffix
69
69
 
70
70
  return (
@@ -77,7 +77,7 @@ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixS
77
77
  borderColor: error
78
78
  ? colors.destructive
79
79
  : focused
80
- ? (colors.ring ?? colors.primary)
80
+ ? colors.primary
81
81
  : colors.border,
82
82
  backgroundColor: colors.background,
83
83
  },
@@ -86,7 +86,7 @@ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixS
86
86
  >
87
87
  {effectivePrefix ? (
88
88
  typeof effectivePrefix === 'string' ? (
89
- <Text style={[styles.prefixText, { color: colors.mutedForeground }, prefixStyle]} allowFontScaling={true}>
89
+ <Text style={[styles.prefixText, { color: colors.foregroundMuted }, prefixStyle]} allowFontScaling={true}>
90
90
  {effectivePrefix}
91
91
  </Text>
92
92
  ) : (
@@ -110,14 +110,14 @@ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixS
110
110
  setFocused(false)
111
111
  onBlur?.(e)
112
112
  }}
113
- placeholderTextColor={colors.mutedForeground}
113
+ placeholderTextColor={colors.foregroundMuted}
114
114
  allowFontScaling={true}
115
115
  secureTextEntry={effectiveSecure}
116
116
  {...props}
117
117
  />
118
118
  {effectiveSuffix ? (
119
119
  typeof effectiveSuffix === 'string' ? (
120
- <Text style={[styles.suffixText, { color: colors.mutedForeground }, suffixStyle]} allowFontScaling={true}>
120
+ <Text style={[styles.suffixText, { color: colors.foregroundMuted }, suffixStyle]} allowFontScaling={true}>
121
121
  {effectiveSuffix}
122
122
  </Text>
123
123
  ) : (
@@ -129,7 +129,7 @@ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixS
129
129
  <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
130
130
  ) : null}
131
131
  {!error && hint ? (
132
- <Text style={[styles.helperText, { color: colors.mutedForeground }]} allowFontScaling={true}>{hint}</Text>
132
+ <Text style={[styles.helperText, { color: colors.foregroundMuted }]} allowFontScaling={true}>{hint}</Text>
133
133
  ) : null}
134
134
  </View>
135
135
  )
@@ -141,21 +141,23 @@ const styles = StyleSheet.create({
141
141
  },
142
142
  label: {
143
143
  fontFamily: 'Poppins-Medium',
144
- fontSize: ms(13),
144
+ fontSize: ms(14), // caption size for input labels
145
145
  },
146
146
  inputWrapper: {
147
147
  flexDirection: 'row',
148
148
  alignItems: 'center',
149
- borderWidth: 1,
150
- borderRadius: ms(8),
149
+ borderWidth: 2,
150
+ borderRadius: 8,
151
151
  paddingHorizontal: s(14),
152
152
  paddingVertical: vs(11),
153
+ minHeight: 48,
153
154
  },
154
155
  input: {
155
156
  fontFamily: 'Poppins-Regular',
156
157
  flex: 1,
157
- fontSize: ms(15),
158
- paddingVertical: 0,
158
+ fontSize: ms(16),
159
+ paddingVertical: vs(2),
160
+ includeFontPadding: false,
159
161
  },
160
162
  prefixContainer: {
161
163
  marginRight: s(8),