@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.
- package/COMPONENTS.md +1806 -663
- package/README.md +14 -10
- package/dist/index.d.mts +274 -85
- package/dist/index.d.ts +274 -85
- package/dist/index.js +1048 -321
- package/dist/index.mjs +1046 -324
- 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 +9 -9
- 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 +35 -15
- 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
|
@@ -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
|
|
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={
|
|
66
|
-
{
|
|
67
|
-
<
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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({
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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',
|