@retray-dev/ui-kit 4.0.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.
- package/COMPONENTS.md +1791 -663
- package/README.md +4 -3
- package/dist/index.d.mts +268 -83
- package/dist/index.d.ts +268 -83
- package/dist/index.js +1032 -309
- package/dist/index.mjs +1029 -311
- package/package.json +3 -2
- package/src/components/Accordion/Accordion.tsx +1 -1
- package/src/components/AlertBanner/AlertBanner.tsx +50 -45
- package/src/components/Avatar/Avatar.tsx +61 -17
- package/src/components/Badge/Badge.tsx +17 -15
- package/src/components/Button/Button.tsx +31 -42
- package/src/components/Card/Card.tsx +4 -4
- package/src/components/CategoryStrip/CategoryStrip.tsx +185 -0
- package/src/components/CategoryStrip/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.tsx +44 -16
- package/src/components/Chip/Chip.tsx +1 -1
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +3 -3
- package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +1 -0
- package/src/components/CurrencyInput/CurrencyInput.tsx +6 -4
- package/src/components/EmptyState/EmptyState.tsx +9 -9
- package/src/components/IconButton/IconButton.tsx +74 -34
- package/src/components/Input/Input.tsx +15 -13
- package/src/components/LabelValue/LabelValue.tsx +1 -1
- package/src/components/ListItem/ListItem.tsx +5 -5
- package/src/components/MediaCard/MediaCard.tsx +249 -0
- package/src/components/MediaCard/index.ts +2 -0
- package/src/components/Pressable/Pressable.tsx +100 -0
- package/src/components/Pressable/index.ts +1 -0
- package/src/components/Progress/Progress.tsx +14 -7
- package/src/components/RadioGroup/RadioGroup.tsx +1 -1
- package/src/components/Select/Select.tsx +5 -5
- package/src/components/Sheet/Sheet.tsx +2 -2
- package/src/components/Skeleton/Skeleton.tsx +34 -7
- package/src/components/Slider/Slider.tsx +2 -2
- package/src/components/Spinner/Spinner.tsx +1 -1
- package/src/components/Switch/Switch.tsx +31 -4
- package/src/components/Tabs/Tabs.tsx +63 -45
- package/src/components/Text/Text.tsx +59 -10
- package/src/components/Textarea/Textarea.tsx +4 -3
- package/src/components/Toast/Toast.tsx +77 -36
- package/src/components/Toggle/Toggle.tsx +3 -3
- package/src/index.ts +8 -2
- package/src/theme/ThemeProvider.tsx +11 -10
- package/src/theme/colorUtils.ts +80 -0
- package/src/theme/colors.ts +76 -35
- package/src/theme/index.ts +2 -2
- package/src/theme/types.ts +27 -13
- package/src/tokens.ts +150 -13
- package/src/utils/hover.ts +25 -0
|
@@ -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
|
|
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.
|
|
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={
|
|
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.
|
|
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
|
-
|
|
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={[
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
113
|
+
success: colors.successBorder,
|
|
114
|
+
warning: colors.warningBorder,
|
|
105
115
|
}[variant]
|
|
106
116
|
|
|
107
|
-
const
|
|
108
|
-
default:
|
|
109
|
-
destructive:
|
|
110
|
-
success:
|
|
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
|
|
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={
|
|
129
|
+
<FontAwesome5 name="check-circle" size={16} color={accentColor} />
|
|
118
130
|
) : variant === 'destructive' ? (
|
|
119
|
-
<AntDesign name="exclamation-circle" size={
|
|
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={
|
|
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,
|
|
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:
|
|
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:
|
|
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={
|
|
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: '
|
|
208
|
-
borderRadius: ms(
|
|
209
|
-
borderWidth:
|
|
210
|
-
paddingHorizontal: s(
|
|
211
|
-
paddingVertical: vs(
|
|
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:
|
|
214
|
-
shadowOpacity: 0.
|
|
215
|
-
shadowRadius:
|
|
216
|
-
elevation:
|
|
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(
|
|
247
|
+
gap: vs(2),
|
|
221
248
|
},
|
|
222
249
|
leftIconContainer: {
|
|
223
|
-
|
|
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-
|
|
230
|
-
fontSize: ms(
|
|
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(
|
|
262
|
+
fontSize: ms(12),
|
|
263
|
+
lineHeight: ms(17),
|
|
264
|
+
opacity: 0.85,
|
|
235
265
|
},
|
|
236
|
-
|
|
237
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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'
|