@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
@@ -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',
@@ -16,7 +16,12 @@ import { useTheme } from '../../theme'
16
16
  import { s, vs, ms } from '../../utils/scaling'
17
17
  import { renderIcon } from '../../utils/icons'
18
18
 
19
- export type ToastVariant = 'default' | 'destructive' | 'success'
19
+ export type ToastVariant = 'default' | 'destructive' | 'success' | 'warning'
20
+
21
+ export interface ToastAction {
22
+ label: string
23
+ onPress: () => void
24
+ }
20
25
 
21
26
  export interface ToastItem {
22
27
  id: string
@@ -24,15 +29,12 @@ export interface ToastItem {
24
29
  description?: string
25
30
  variant?: ToastVariant
26
31
  icon?: React.ReactNode
27
- /**
28
- * Icon name from `@expo/vector-icons`. See https://icons.expo.fyi.
29
- * Takes precedence over `icon`. When neither is set, a default variant icon is shown.
30
- */
31
32
  iconName?: string
32
- /** Override the resolved icon color. Defaults to the variant text color. */
33
33
  iconColor?: string
34
34
  /** Auto-dismiss delay in milliseconds. Defaults to `3000`. */
35
35
  duration?: number
36
+ /** Optional inline action button rendered at the end of the toast. */
37
+ action?: ToastAction
36
38
  }
37
39
 
38
40
  interface ToastContextValue {
@@ -99,30 +101,42 @@ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: ()
99
101
  const variant = item.variant ?? 'default'
100
102
 
101
103
  const bgColor = {
102
- default: colors.foreground,
104
+ default: colors.card,
105
+ destructive: colors.destructiveTint,
106
+ success: colors.successTint,
107
+ warning: colors.warningTint,
108
+ }[variant]
109
+
110
+ const borderColor = {
111
+ default: colors.border,
103
112
  destructive: colors.destructiveBorder,
104
- success: colors.successBorder,
113
+ success: colors.successBorder,
114
+ warning: colors.warningBorder,
105
115
  }[variant]
106
116
 
107
- const textColor = {
108
- default: colors.background,
109
- destructive: '#991b1b',
110
- success: '#166534',
117
+ const accentColor = {
118
+ default: colors.primary,
119
+ destructive: colors.destructive,
120
+ success: colors.success,
121
+ warning: colors.warning,
111
122
  }[variant]
112
123
 
113
- const borderColor = textColor
124
+ const titleColor = variant === 'default' ? colors.foreground : accentColor
125
+ const descColor = variant === 'default' ? colors.foregroundMuted : accentColor
114
126
 
115
127
  const defaultIcon =
116
128
  variant === 'success' ? (
117
- <FontAwesome5 name="check-circle" size={18} color={textColor} />
129
+ <FontAwesome5 name="check-circle" size={16} color={accentColor} />
118
130
  ) : variant === 'destructive' ? (
119
- <AntDesign name="exclamation-circle" size={18} color={textColor} />
131
+ <AntDesign name="exclamation-circle" size={16} color={accentColor} />
132
+ ) : variant === 'warning' ? (
133
+ <MaterialIcons name="warning-amber" size={17} color={accentColor} />
120
134
  ) : (
121
- <Entypo name="info-with-circle" size={18} color={textColor} />
135
+ <Entypo name="info-with-circle" size={16} color={accentColor} />
122
136
  )
123
137
 
124
138
  const leftIcon: React.ReactNode = item.iconName
125
- ? renderIcon(item.iconName, 22, item.iconColor ?? textColor)
139
+ ? renderIcon(item.iconName, 16, item.iconColor ?? accentColor)
126
140
  : item.icon ?? defaultIcon
127
141
 
128
142
  return (
@@ -131,16 +145,27 @@ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: ()
131
145
  <View style={styles.leftIconContainer}>{leftIcon}</View>
132
146
  <View style={styles.toastContent}>
133
147
  {item.title ? (
134
- <Text style={[styles.toastTitle, { color: textColor }]} allowFontScaling={true}>{item.title}</Text>
148
+ <Text style={[styles.toastTitle, { color: titleColor }]} allowFontScaling={true}>{item.title}</Text>
135
149
  ) : null}
136
150
  {item.description ? (
137
- <Text style={[styles.toastDescription, { color: textColor, opacity: 0.85 }]} allowFontScaling={true}>
151
+ <Text style={[styles.toastDescription, { color: descColor }]} allowFontScaling={true}>
138
152
  {item.description}
139
153
  </Text>
140
154
  ) : null}
141
155
  </View>
156
+ {item.action && (
157
+ <TouchableOpacity
158
+ onPress={() => { item.action!.onPress(); onDismiss() }}
159
+ style={styles.actionButton}
160
+ touchSoundDisabled={true}
161
+ >
162
+ <Text style={[styles.actionLabel, { color: accentColor }]} allowFontScaling={true}>
163
+ {item.action.label}
164
+ </Text>
165
+ </TouchableOpacity>
166
+ )}
142
167
  <TouchableOpacity onPress={onDismiss} style={styles.dismissButton} touchSoundDisabled={true}>
143
- <AntDesign name="close-circle" size={18} color={textColor} />
168
+ <AntDesign name="close-circle" size={16} color={descColor} />
144
169
  </TouchableOpacity>
145
170
  </Animated.View>
146
171
  </GestureDetector>
@@ -166,6 +191,8 @@ export function ToastProvider({ children }: ToastProviderProps) {
166
191
  notificationSuccess()
167
192
  } else if (item.variant === 'destructive') {
168
193
  notificationError()
194
+ } else if (item.variant === 'warning') {
195
+ notificationError()
169
196
  } else {
170
197
  impactLight()
171
198
  }
@@ -204,37 +231,51 @@ const styles = StyleSheet.create({
204
231
  },
205
232
  toast: {
206
233
  flexDirection: 'row',
207
- alignItems: 'center',
208
- borderRadius: ms(12),
209
- borderWidth: 1,
210
- paddingHorizontal: s(14),
211
- paddingVertical: vs(12),
234
+ alignItems: 'flex-start',
235
+ borderRadius: ms(10),
236
+ borderWidth: 0.5,
237
+ paddingHorizontal: s(12),
238
+ paddingVertical: vs(10),
212
239
  shadowColor: '#000',
213
- shadowOffset: { width: 0, height: 3 },
214
- shadowOpacity: 0.10,
215
- shadowRadius: 8,
216
- elevation: 5,
240
+ shadowOffset: { width: 0, height: 2 },
241
+ shadowOpacity: 0.06,
242
+ shadowRadius: 4,
243
+ elevation: 3,
217
244
  },
218
245
  toastContent: {
219
246
  flex: 1,
220
- gap: vs(4),
247
+ gap: vs(2),
221
248
  },
222
249
  leftIconContainer: {
223
- width: s(28),
250
+ marginTop: vs(1),
224
251
  alignItems: 'center',
225
252
  justifyContent: 'center',
226
253
  marginRight: s(10),
227
254
  },
228
255
  toastTitle: {
229
- fontFamily: 'Poppins-SemiBold',
230
- fontSize: ms(15),
256
+ fontFamily: 'Poppins-Medium',
257
+ fontSize: ms(13),
258
+ lineHeight: ms(18),
231
259
  },
232
260
  toastDescription: {
233
261
  fontFamily: 'Poppins-Regular',
234
- fontSize: ms(14),
262
+ fontSize: ms(12),
263
+ lineHeight: ms(17),
264
+ opacity: 0.85,
235
265
  },
236
- dismissButton: {
237
- padding: s(8),
266
+ actionButton: {
267
+ paddingHorizontal: s(8),
268
+ paddingVertical: vs(4),
238
269
  marginLeft: s(4),
239
270
  },
271
+ actionLabel: {
272
+ fontFamily: 'Poppins-Medium',
273
+ fontSize: ms(12),
274
+ textDecorationLine: 'underline',
275
+ },
276
+ dismissButton: {
277
+ padding: s(6),
278
+ marginLeft: s(2),
279
+ marginTop: vs(0),
280
+ },
240
281
  })
@@ -93,7 +93,7 @@ export function Toggle({
93
93
 
94
94
  const backgroundColor = pressAnim.interpolate({
95
95
  inputRange: [0, 1],
96
- outputRange: ['transparent', colors.accent],
96
+ outputRange: ['transparent', colors.surfaceStrong],
97
97
  })
98
98
 
99
99
  const textColor = pressAnim.interpolate({
@@ -117,12 +117,12 @@ export function Toggle({
117
117
  return <FontAwesome5 name="check-circle" size={iconSize} color={colors.primary} />
118
118
  }
119
119
 
120
- if (iconName) return <>{renderIcon(iconName, iconSize, iconColor ?? colors.mutedForeground)}</>
120
+ if (iconName) return <>{renderIcon(iconName, iconSize, iconColor ?? colors.foregroundMuted)}</>
121
121
  const custom = renderProp(icon)
122
122
  if (custom) return <>{custom}</>
123
123
 
124
124
  // Default: empty circle to signal an action is available
125
- return <FontAwesome5 name="circle" size={iconSize} color={colors.mutedForeground} />
125
+ return <FontAwesome5 name="circle" size={iconSize} color={colors.foregroundMuted} />
126
126
  }
127
127
 
128
128
  return (
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Theme
2
2
  export { ThemeProvider, useTheme } from './theme'
3
- export type { ThemeProviderProps, ThemeColors, Theme, ColorScheme } from './theme'
4
- export { defaultLight, defaultDark } from './theme'
3
+ export type { ThemeProviderProps, ThemeColors, ResolvedColors, Theme, ColorScheme } from './theme'
4
+ export { defaultLight, defaultDark, deriveColors } from './theme'
5
5
 
6
6
  // Components
7
7
  export * from './components/Button'
@@ -37,6 +37,9 @@ export * from './components/Chip'
37
37
  export * from './components/ConfirmDialog'
38
38
  export * from './components/LabelValue'
39
39
  export * from './components/MonthPicker'
40
+ export * from './components/MediaCard'
41
+ export * from './components/CategoryStrip'
42
+ export * from './components/Pressable'
40
43
 
41
44
  // Icon utility
42
45
  export { Icon, renderIcon } from './utils/icons'
@@ -49,6 +52,7 @@ export {
49
52
  RADIUS,
50
53
  SHADOWS,
51
54
  BREAKPOINTS,
55
+ TYPOGRAPHY,
52
56
  } from './tokens'
53
57
  export type {
54
58
  Spacing,
@@ -57,4 +61,6 @@ export type {
57
61
  IconSizeKey,
58
62
  Radius,
59
63
  RadiusKey,
64
+ Typography,
65
+ TypographyKey,
60
66
  } from './tokens'