@retray-dev/ui-kit 6.1.0 → 6.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 +4 -4
- package/dist/index.d.mts +42 -21
- package/dist/index.d.ts +42 -21
- package/dist/index.js +679 -628
- package/dist/index.mjs +672 -621
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +10 -12
- package/src/components/Button/Button.tsx +20 -18
- package/src/components/Card/Card.tsx +21 -33
- package/src/components/CategoryStrip/CategoryStrip.tsx +45 -38
- package/src/components/Checkbox/Checkbox.tsx +31 -50
- package/src/components/Chip/Chip.tsx +34 -71
- package/src/components/IconButton/IconButton.tsx +20 -18
- package/src/components/Input/Input.tsx +39 -22
- package/src/components/ListItem/ListItem.tsx +22 -34
- package/src/components/MediaCard/MediaCard.tsx +24 -24
- package/src/components/MenuItem/MenuItem.tsx +22 -31
- package/src/components/MonthPicker/MonthPicker.tsx +12 -2
- package/src/components/Pressable/Pressable.tsx +27 -46
- package/src/components/Progress/Progress.tsx +21 -12
- package/src/components/RadioGroup/RadioGroup.tsx +52 -26
- package/src/components/Select/Select.tsx +17 -15
- package/src/components/Sheet/Sheet.tsx +4 -1
- package/src/components/Skeleton/Skeleton.tsx +24 -13
- package/src/components/Slider/Slider.tsx +11 -1
- package/src/components/Switch/Switch.tsx +44 -49
- package/src/components/Tabs/Tabs.tsx +39 -31
- package/src/components/Textarea/Textarea.tsx +29 -12
- package/src/components/Toggle/Toggle.tsx +39 -45
- package/src/utils/animations.ts +58 -0
- package/src/utils/useColorTransition.ts +40 -0
- package/src/utils/usePressScale.ts +73 -0
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react'
|
|
2
2
|
import {
|
|
3
3
|
TouchableOpacity,
|
|
4
|
-
Animated,
|
|
5
4
|
ActivityIndicator,
|
|
6
5
|
StyleSheet,
|
|
7
6
|
View,
|
|
8
7
|
Text,
|
|
9
8
|
TouchableOpacityProps,
|
|
10
9
|
ViewStyle,
|
|
11
|
-
Platform,
|
|
12
10
|
} from 'react-native'
|
|
13
|
-
|
|
14
|
-
const nativeDriver = Platform.OS !== 'web'
|
|
11
|
+
import Animated from 'react-native-reanimated'
|
|
15
12
|
import { impactLight } from '../../utils/haptics'
|
|
16
13
|
import { useTheme } from '../../theme'
|
|
17
14
|
import { s, ms } from '../../utils/scaling'
|
|
18
15
|
import { renderIcon } from '../../utils/icons'
|
|
16
|
+
import { usePressScale } from '../../utils/usePressScale'
|
|
17
|
+
import { PRESS_SCALE } from '../../utils/animations'
|
|
19
18
|
|
|
20
19
|
// primary: filled primary
|
|
21
20
|
// secondary: filled surface — icon on neutral bg (Airbnb icon-button-circle)
|
|
@@ -56,20 +55,16 @@ export function IconButton({
|
|
|
56
55
|
disabled,
|
|
57
56
|
style,
|
|
58
57
|
onPress,
|
|
58
|
+
accessibilityLabel,
|
|
59
|
+
accessibilityHint,
|
|
59
60
|
...props
|
|
60
61
|
}: IconButtonProps) {
|
|
61
62
|
const { colors } = useTheme()
|
|
62
63
|
const isDisabled = disabled || loading
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Animated.spring(scale, { toValue: 0.95, useNativeDriver: nativeDriver, speed: 40, bounciness: 0 }).start()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const handlePressOut = () => {
|
|
71
|
-
Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, speed: 40, bounciness: 4 }).start()
|
|
72
|
-
}
|
|
64
|
+
const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
|
|
65
|
+
pressScale: PRESS_SCALE.button,
|
|
66
|
+
disabled: isDisabled,
|
|
67
|
+
})
|
|
73
68
|
|
|
74
69
|
const handlePress: TouchableOpacityProps['onPress'] = (e) => {
|
|
75
70
|
impactLight()
|
|
@@ -109,7 +104,10 @@ export function IconButton({
|
|
|
109
104
|
const showCount = typeof badge === 'number' && badge > 0
|
|
110
105
|
|
|
111
106
|
return (
|
|
112
|
-
<Animated.View
|
|
107
|
+
<Animated.View
|
|
108
|
+
style={[styles.wrapper, animatedStyle]}
|
|
109
|
+
{...hoverHandlers}
|
|
110
|
+
>
|
|
113
111
|
<TouchableOpacity
|
|
114
112
|
style={[
|
|
115
113
|
styles.base,
|
|
@@ -122,8 +120,12 @@ export function IconButton({
|
|
|
122
120
|
activeOpacity={1}
|
|
123
121
|
touchSoundDisabled={true}
|
|
124
122
|
onPress={handlePress}
|
|
125
|
-
onPressIn={
|
|
126
|
-
onPressOut={
|
|
123
|
+
onPressIn={onPressIn}
|
|
124
|
+
onPressOut={onPressOut}
|
|
125
|
+
accessibilityRole="button"
|
|
126
|
+
accessibilityLabel={accessibilityLabel ?? iconName ?? 'icon button'}
|
|
127
|
+
accessibilityHint={accessibilityHint}
|
|
128
|
+
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
|
127
129
|
{...props}
|
|
128
130
|
>
|
|
129
131
|
{loading ? (
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import React, { useState
|
|
2
|
-
import { TextInput, View, Text,
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, TextStyle, TouchableOpacity, Platform } from 'react-native'
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
interpolateColor,
|
|
6
|
+
} from 'react-native-reanimated'
|
|
3
7
|
import { AntDesign } from '@expo/vector-icons'
|
|
4
8
|
import { useTheme } from '../../theme'
|
|
5
9
|
import { s, vs, ms } from '../../utils/scaling'
|
|
6
10
|
import { renderIcon } from '../../utils/icons'
|
|
11
|
+
import { useColorTransition } from '../../utils/useColorTransition'
|
|
12
|
+
import { TIMINGS } from '../../utils/animations'
|
|
7
13
|
|
|
8
14
|
const webInputResetStyle: any =
|
|
9
15
|
Platform.OS === 'web'
|
|
@@ -48,11 +54,15 @@ export interface InputProps extends TextInputProps {
|
|
|
48
54
|
inputWrapperStyle?: ViewStyle
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyle, suffixStyle, prefixIcon, suffixIcon, prefixIconColor, suffixIconColor, type = 'text', containerStyle, inputWrapperStyle, style, onFocus, onBlur, secureTextEntry, editable, ...props }: InputProps) {
|
|
57
|
+
export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyle, suffixStyle, prefixIcon, suffixIcon, prefixIconColor, suffixIconColor, type = 'text', containerStyle, inputWrapperStyle, style, onFocus, onBlur, secureTextEntry, editable, accessibilityLabel, ...props }: InputProps) {
|
|
52
58
|
const { colors } = useTheme()
|
|
53
59
|
const [focused, setFocused] = useState(false)
|
|
54
60
|
const [showPassword, setShowPassword] = useState(false)
|
|
55
|
-
|
|
61
|
+
|
|
62
|
+
// Asymmetric durations — focus snaps in, blurs out subtly. Runs on UI thread.
|
|
63
|
+
const focusProgress = useColorTransition(focused, {
|
|
64
|
+
duration: focused ? TIMINGS.focusIn.duration : TIMINGS.focusOut.duration,
|
|
65
|
+
})
|
|
56
66
|
|
|
57
67
|
const isDisabled = disabled || editable === false
|
|
58
68
|
const isPassword = type === 'password'
|
|
@@ -62,30 +72,34 @@ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyl
|
|
|
62
72
|
? renderIcon(prefixIcon, 20, prefixIconColor ?? colors.foregroundMuted)
|
|
63
73
|
: prefix
|
|
64
74
|
|
|
65
|
-
// If type is password and no suffix override is provided, add the toggle button
|
|
66
75
|
const effectiveSuffix: React.ReactNode = isPassword && !suffix && !suffixIcon ? (
|
|
67
|
-
<TouchableOpacity
|
|
76
|
+
<TouchableOpacity
|
|
77
|
+
onPress={() => setShowPassword(!showPassword)}
|
|
78
|
+
style={styles.passwordToggle}
|
|
79
|
+
activeOpacity={0.6}
|
|
80
|
+
accessibilityRole="button"
|
|
81
|
+
accessibilityLabel={showPassword ? 'Hide password' : 'Show password'}
|
|
82
|
+
>
|
|
68
83
|
<AntDesign name={showPassword ? 'eye' : 'eye-invisible'} size={20} color={colors.foregroundMuted} />
|
|
69
84
|
</TouchableOpacity>
|
|
70
85
|
) : suffixIcon
|
|
71
86
|
? renderIcon(suffixIcon, 20, suffixIconColor ?? colors.foregroundMuted)
|
|
72
87
|
: suffix
|
|
73
88
|
|
|
89
|
+
const borderColorStyle = useAnimatedStyle(() => ({
|
|
90
|
+
borderColor: error
|
|
91
|
+
? colors.destructive
|
|
92
|
+
: interpolateColor(focusProgress.value, [0, 1], [colors.border, colors.primary]),
|
|
93
|
+
}))
|
|
94
|
+
|
|
74
95
|
return (
|
|
75
96
|
<View style={[styles.container, isDisabled && styles.containerDisabled, containerStyle]}>
|
|
76
97
|
{label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
|
|
77
98
|
<Animated.View
|
|
78
99
|
style={[
|
|
79
100
|
styles.inputWrapper,
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
? colors.destructive
|
|
83
|
-
: focusAnim.interpolate({
|
|
84
|
-
inputRange: [0, 1],
|
|
85
|
-
outputRange: [colors.border, colors.primary],
|
|
86
|
-
}),
|
|
87
|
-
backgroundColor: isDisabled ? colors.surface : colors.background,
|
|
88
|
-
},
|
|
101
|
+
{ backgroundColor: isDisabled ? colors.surface : colors.background },
|
|
102
|
+
borderColorStyle,
|
|
89
103
|
inputWrapperStyle,
|
|
90
104
|
]}
|
|
91
105
|
>
|
|
@@ -101,26 +115,23 @@ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyl
|
|
|
101
115
|
<TextInput
|
|
102
116
|
style={[
|
|
103
117
|
styles.input,
|
|
104
|
-
{
|
|
105
|
-
color: colors.foreground,
|
|
106
|
-
},
|
|
118
|
+
{ color: colors.foreground },
|
|
107
119
|
webInputResetStyle,
|
|
108
120
|
style,
|
|
109
121
|
]}
|
|
110
122
|
onFocus={(e) => {
|
|
111
123
|
setFocused(true)
|
|
112
|
-
Animated.timing(focusAnim, { toValue: 1, duration: 120, easing: Easing.out(Easing.ease), useNativeDriver: false }).start()
|
|
113
124
|
onFocus?.(e)
|
|
114
125
|
}}
|
|
115
126
|
onBlur={(e) => {
|
|
116
127
|
setFocused(false)
|
|
117
|
-
Animated.timing(focusAnim, { toValue: 0, duration: 80, easing: Easing.out(Easing.ease), useNativeDriver: false }).start()
|
|
118
128
|
onBlur?.(e)
|
|
119
129
|
}}
|
|
120
130
|
placeholderTextColor={colors.foregroundMuted}
|
|
121
131
|
allowFontScaling={true}
|
|
122
132
|
secureTextEntry={effectiveSecure}
|
|
123
133
|
editable={isDisabled ? false : editable}
|
|
134
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
124
135
|
{...props}
|
|
125
136
|
/>
|
|
126
137
|
{effectiveSuffix ? (
|
|
@@ -134,7 +145,13 @@ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyl
|
|
|
134
145
|
) : null}
|
|
135
146
|
</Animated.View>
|
|
136
147
|
{error ? (
|
|
137
|
-
<Text
|
|
148
|
+
<Text
|
|
149
|
+
style={[styles.helperText, { color: colors.destructive }]}
|
|
150
|
+
allowFontScaling={true}
|
|
151
|
+
accessibilityLiveRegion="polite"
|
|
152
|
+
>
|
|
153
|
+
{error}
|
|
154
|
+
</Text>
|
|
138
155
|
) : null}
|
|
139
156
|
{!error && hint ? (
|
|
140
157
|
<Text style={[styles.helperText, { color: colors.foregroundMuted }]} allowFontScaling={true}>{hint}</Text>
|
|
@@ -152,7 +169,7 @@ const styles = StyleSheet.create({
|
|
|
152
169
|
},
|
|
153
170
|
label: {
|
|
154
171
|
fontFamily: 'Poppins-Medium',
|
|
155
|
-
fontSize: ms(14),
|
|
172
|
+
fontSize: ms(14),
|
|
156
173
|
},
|
|
157
174
|
inputWrapper: {
|
|
158
175
|
flexDirection: 'row',
|
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react'
|
|
2
2
|
import {
|
|
3
3
|
TouchableOpacity,
|
|
4
|
-
Animated,
|
|
5
4
|
View,
|
|
6
5
|
Text,
|
|
7
6
|
StyleSheet,
|
|
8
7
|
ViewStyle,
|
|
9
8
|
TextStyle,
|
|
10
|
-
Platform,
|
|
11
9
|
} from 'react-native'
|
|
10
|
+
import Animated from 'react-native-reanimated'
|
|
12
11
|
import { Entypo } from '@expo/vector-icons'
|
|
13
12
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
14
13
|
import { useTheme } from '../../theme'
|
|
15
14
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
16
15
|
import { renderIcon } from '../../utils/icons'
|
|
17
16
|
import { RADIUS } from '../../tokens'
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
import { usePressScale } from '../../utils/usePressScale'
|
|
18
|
+
import { SPRINGS, PRESS_SCALE } from '../../utils/animations'
|
|
20
19
|
|
|
21
20
|
export type ListItemVariant = 'plain' | 'card'
|
|
22
21
|
|
|
@@ -78,6 +77,8 @@ export interface ListItemProps {
|
|
|
78
77
|
subtitleStyle?: TextStyle
|
|
79
78
|
/** Style applied to the caption Text. */
|
|
80
79
|
captionStyle?: TextStyle
|
|
80
|
+
/** Accessibility label override. Defaults to the title. */
|
|
81
|
+
accessibilityLabel?: string
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
export function ListItem({
|
|
@@ -101,30 +102,15 @@ export function ListItem({
|
|
|
101
102
|
titleStyle,
|
|
102
103
|
subtitleStyle,
|
|
103
104
|
captionStyle,
|
|
105
|
+
accessibilityLabel,
|
|
104
106
|
}: ListItemProps) {
|
|
105
107
|
const { colors } = useTheme()
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
useNativeDriver: nativeDriver,
|
|
113
|
-
stiffness: 350,
|
|
114
|
-
damping: 28,
|
|
115
|
-
mass: 0.9,
|
|
116
|
-
}).start()
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const handlePressOut = () => {
|
|
120
|
-
Animated.spring(scale, {
|
|
121
|
-
toValue: 1,
|
|
122
|
-
useNativeDriver: nativeDriver,
|
|
123
|
-
stiffness: 220,
|
|
124
|
-
damping: 20,
|
|
125
|
-
mass: 0.9,
|
|
126
|
-
}).start()
|
|
127
|
-
}
|
|
108
|
+
const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
|
|
109
|
+
pressScale: PRESS_SCALE.row,
|
|
110
|
+
pressInSpring: SPRINGS.surfacePressIn,
|
|
111
|
+
pressOutSpring: SPRINGS.surfacePressOut,
|
|
112
|
+
disabled: !onPress || disabled,
|
|
113
|
+
})
|
|
128
114
|
|
|
129
115
|
const handlePress = () => {
|
|
130
116
|
hapticSelection()
|
|
@@ -154,16 +140,21 @@ export function ListItem({
|
|
|
154
140
|
}
|
|
155
141
|
: {}
|
|
156
142
|
|
|
143
|
+
const a11yLabel = accessibilityLabel ?? [title, subtitle, caption].filter(Boolean).join('. ')
|
|
144
|
+
|
|
157
145
|
return (
|
|
158
|
-
<Animated.View style={[
|
|
146
|
+
<Animated.View style={[animatedStyle, disabled && styles.disabled]} {...hoverHandlers}>
|
|
159
147
|
<TouchableOpacity
|
|
160
148
|
style={[styles.container, cardStyle, style]}
|
|
161
149
|
onPress={onPress ? handlePress : undefined}
|
|
162
|
-
onPressIn={
|
|
163
|
-
onPressOut={
|
|
150
|
+
onPressIn={onPressIn}
|
|
151
|
+
onPressOut={onPressOut}
|
|
164
152
|
disabled={disabled}
|
|
165
153
|
activeOpacity={1}
|
|
166
154
|
touchSoundDisabled={true}
|
|
155
|
+
accessibilityRole={onPress ? 'button' : undefined}
|
|
156
|
+
accessibilityLabel={onPress ? a11yLabel : undefined}
|
|
157
|
+
accessibilityState={onPress ? { disabled: !!disabled } : undefined}
|
|
167
158
|
>
|
|
168
159
|
{effectiveLeft ? (
|
|
169
160
|
<View style={styles.leftContainer}>{effectiveLeft}</View>
|
|
@@ -219,7 +210,7 @@ export function ListItem({
|
|
|
219
210
|
<View
|
|
220
211
|
style={[
|
|
221
212
|
styles.separator,
|
|
222
|
-
{
|
|
213
|
+
{
|
|
223
214
|
backgroundColor: colors.border,
|
|
224
215
|
marginLeft: effectiveLeft ? s(44) + s(12) : 0
|
|
225
216
|
},
|
|
@@ -275,9 +266,6 @@ const styles = StyleSheet.create({
|
|
|
275
266
|
fontFamily: 'Poppins-Regular',
|
|
276
267
|
fontSize: ms(14),
|
|
277
268
|
},
|
|
278
|
-
chevron: {
|
|
279
|
-
marginLeft: s(4),
|
|
280
|
-
},
|
|
281
269
|
separator: {
|
|
282
270
|
height: StyleSheet.hairlineWidth,
|
|
283
271
|
marginRight: 0,
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Image,
|
|
5
5
|
Text,
|
|
6
6
|
TouchableOpacity,
|
|
7
|
-
Animated,
|
|
8
7
|
StyleSheet,
|
|
9
8
|
ViewStyle,
|
|
10
9
|
ImageSourcePropType,
|
|
11
10
|
Platform,
|
|
12
11
|
} from 'react-native'
|
|
12
|
+
import Animated from 'react-native-reanimated'
|
|
13
13
|
import { impactLight } from '../../utils/haptics'
|
|
14
14
|
import { useTheme } from '../../theme'
|
|
15
15
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
16
16
|
import { renderIcon } from '../../utils/icons'
|
|
17
17
|
import { useHover } from '../../utils/hover'
|
|
18
|
+
import { usePressScale } from '../../utils/usePressScale'
|
|
19
|
+
import { SPRINGS, PRESS_SCALE } from '../../utils/animations'
|
|
18
20
|
import { RADIUS, SHADOWS } from '../../tokens'
|
|
19
21
|
|
|
20
|
-
const nativeDriver = Platform.OS !== 'web'
|
|
21
|
-
|
|
22
22
|
export type MediaCardAspectRatio = '1:1' | '4:3' | '16:9' | '4:5' | '3:2'
|
|
23
23
|
|
|
24
24
|
const aspectRatioMap: Record<MediaCardAspectRatio, number> = {
|
|
@@ -57,6 +57,8 @@ export interface MediaCardProps {
|
|
|
57
57
|
imageStyle?: ViewStyle
|
|
58
58
|
/** Additional content rendered below caption. */
|
|
59
59
|
footer?: React.ReactNode
|
|
60
|
+
/** Accessibility label override. Defaults to title (and subtitle if present). */
|
|
61
|
+
accessibilityLabel?: string
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export function MediaCard({
|
|
@@ -74,20 +76,16 @@ export function MediaCard({
|
|
|
74
76
|
style,
|
|
75
77
|
imageStyle,
|
|
76
78
|
footer,
|
|
79
|
+
accessibilityLabel,
|
|
77
80
|
}: MediaCardProps) {
|
|
78
81
|
const { colors } = useTheme()
|
|
79
|
-
const scale = useRef(new Animated.Value(1)).current
|
|
80
82
|
const { hovered, hoverHandlers } = useHover()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const handlePressOut = () => {
|
|
88
|
-
if (!onPress) return
|
|
89
|
-
Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, speed: 40, bounciness: 4 }).start()
|
|
90
|
-
}
|
|
83
|
+
const { animatedStyle, onPressIn, onPressOut } = usePressScale({
|
|
84
|
+
pressScale: PRESS_SCALE.card,
|
|
85
|
+
pressInSpring: SPRINGS.surfacePressIn,
|
|
86
|
+
pressOutSpring: SPRINGS.surfacePressOut,
|
|
87
|
+
disabled: !onPress,
|
|
88
|
+
})
|
|
91
89
|
|
|
92
90
|
const handlePress = () => {
|
|
93
91
|
if (!onPress) return
|
|
@@ -102,6 +100,8 @@ export function MediaCard({
|
|
|
102
100
|
? renderIcon(actionIconName, 18, actionActive ? colors.primary : colors.background)
|
|
103
101
|
: actionIcon ?? renderIcon('heart', 18, actionActive ? colors.primary : colors.background)
|
|
104
102
|
|
|
103
|
+
const a11yLabel = accessibilityLabel ?? [title, subtitle].filter(Boolean).join('. ')
|
|
104
|
+
|
|
105
105
|
const cardContent = (
|
|
106
106
|
<View
|
|
107
107
|
style={[
|
|
@@ -111,7 +111,6 @@ export function MediaCard({
|
|
|
111
111
|
]}
|
|
112
112
|
{...(Platform.OS === 'web' ? hoverHandlers : {})}
|
|
113
113
|
>
|
|
114
|
-
{/* Image area */}
|
|
115
114
|
<View style={[styles.imageContainer, imageStyle]}>
|
|
116
115
|
<View style={{ paddingTop: `${ratio * 100}%` as any }}>
|
|
117
116
|
<View style={StyleSheet.absoluteFill}>
|
|
@@ -127,27 +126,27 @@ export function MediaCard({
|
|
|
127
126
|
</View>
|
|
128
127
|
</View>
|
|
129
128
|
|
|
130
|
-
{/* Badge — top left */}
|
|
131
129
|
{badge && (
|
|
132
130
|
<View style={styles.badgeContainer}>
|
|
133
131
|
{badge}
|
|
134
132
|
</View>
|
|
135
133
|
)}
|
|
136
134
|
|
|
137
|
-
{/* Action icon — top right */}
|
|
138
135
|
{(onActionPress || actionIcon || actionIconName) && (
|
|
139
136
|
<TouchableOpacity
|
|
140
137
|
style={[styles.actionButton, { backgroundColor: 'rgba(0,0,0,0.24)' }]}
|
|
141
138
|
onPress={() => { impactLight(); onActionPress?.() }}
|
|
142
139
|
activeOpacity={0.8}
|
|
143
140
|
touchSoundDisabled={true}
|
|
141
|
+
accessibilityRole="button"
|
|
142
|
+
accessibilityLabel={actionIconName ?? 'action'}
|
|
143
|
+
accessibilityState={{ selected: actionActive }}
|
|
144
144
|
>
|
|
145
145
|
{resolvedActionIcon}
|
|
146
146
|
</TouchableOpacity>
|
|
147
147
|
)}
|
|
148
148
|
</View>
|
|
149
149
|
|
|
150
|
-
{/* Metadata */}
|
|
151
150
|
{(title || subtitle || caption || footer) && (
|
|
152
151
|
<View style={styles.meta}>
|
|
153
152
|
{title ? (
|
|
@@ -173,13 +172,15 @@ export function MediaCard({
|
|
|
173
172
|
|
|
174
173
|
if (onPress) {
|
|
175
174
|
return (
|
|
176
|
-
<Animated.View style={
|
|
175
|
+
<Animated.View style={animatedStyle}>
|
|
177
176
|
<TouchableOpacity
|
|
178
177
|
onPress={handlePress}
|
|
179
|
-
onPressIn={
|
|
180
|
-
onPressOut={
|
|
178
|
+
onPressIn={onPressIn}
|
|
179
|
+
onPressOut={onPressOut}
|
|
181
180
|
activeOpacity={1}
|
|
182
181
|
touchSoundDisabled={true}
|
|
182
|
+
accessibilityRole="button"
|
|
183
|
+
accessibilityLabel={a11yLabel}
|
|
183
184
|
>
|
|
184
185
|
{cardContent}
|
|
185
186
|
</TouchableOpacity>
|
|
@@ -192,12 +193,11 @@ export function MediaCard({
|
|
|
192
193
|
|
|
193
194
|
const styles = StyleSheet.create({
|
|
194
195
|
card: {
|
|
195
|
-
borderRadius: RADIUS.md,
|
|
196
|
+
borderRadius: RADIUS.md,
|
|
196
197
|
overflow: 'hidden',
|
|
197
198
|
backgroundColor: 'transparent',
|
|
198
199
|
},
|
|
199
200
|
cardHovered: {
|
|
200
|
-
// Web hover: lift shadow
|
|
201
201
|
...SHADOWS.md,
|
|
202
202
|
},
|
|
203
203
|
imageContainer: {
|
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react'
|
|
2
2
|
import {
|
|
3
3
|
TouchableOpacity,
|
|
4
|
-
Animated,
|
|
5
4
|
View,
|
|
6
5
|
Text,
|
|
7
6
|
StyleSheet,
|
|
8
7
|
ViewStyle,
|
|
9
8
|
TextStyle,
|
|
10
|
-
Platform,
|
|
11
9
|
} from 'react-native'
|
|
10
|
+
import Animated from 'react-native-reanimated'
|
|
12
11
|
import { Entypo } from '@expo/vector-icons'
|
|
13
12
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
14
13
|
import { useTheme } from '../../theme'
|
|
15
14
|
import { s, vs, ms } from '../../utils/scaling'
|
|
16
15
|
import { renderIcon } from '../../utils/icons'
|
|
17
16
|
import { RADIUS } from '../../tokens'
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
import { usePressScale } from '../../utils/usePressScale'
|
|
18
|
+
import { SPRINGS, PRESS_SCALE } from '../../utils/animations'
|
|
20
19
|
|
|
21
20
|
export type MenuItemVariant = 'plain' | 'card'
|
|
22
21
|
|
|
@@ -57,6 +56,8 @@ export interface MenuItemProps {
|
|
|
57
56
|
style?: ViewStyle
|
|
58
57
|
/** Style applied to the label Text. */
|
|
59
58
|
labelStyle?: TextStyle
|
|
59
|
+
/** Accessibility label override. Defaults to label. */
|
|
60
|
+
accessibilityLabel?: string
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export function MenuItem({
|
|
@@ -73,30 +74,15 @@ export function MenuItem({
|
|
|
73
74
|
showSeparator = false,
|
|
74
75
|
style,
|
|
75
76
|
labelStyle,
|
|
77
|
+
accessibilityLabel,
|
|
76
78
|
}: MenuItemProps) {
|
|
77
79
|
const { colors } = useTheme()
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
useNativeDriver: nativeDriver,
|
|
85
|
-
stiffness: 350,
|
|
86
|
-
damping: 28,
|
|
87
|
-
mass: 0.9,
|
|
88
|
-
}).start()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const handlePressOut = () => {
|
|
92
|
-
Animated.spring(scale, {
|
|
93
|
-
toValue: 1,
|
|
94
|
-
useNativeDriver: nativeDriver,
|
|
95
|
-
stiffness: 220,
|
|
96
|
-
damping: 20,
|
|
97
|
-
mass: 0.9,
|
|
98
|
-
}).start()
|
|
99
|
-
}
|
|
80
|
+
const { animatedStyle, onPressIn, onPressOut, hoverHandlers } = usePressScale({
|
|
81
|
+
pressScale: PRESS_SCALE.row,
|
|
82
|
+
pressInSpring: SPRINGS.surfacePressIn,
|
|
83
|
+
pressOutSpring: SPRINGS.surfacePressOut,
|
|
84
|
+
disabled,
|
|
85
|
+
})
|
|
100
86
|
|
|
101
87
|
const handlePress = () => {
|
|
102
88
|
hapticSelection()
|
|
@@ -122,16 +108,21 @@ export function MenuItem({
|
|
|
122
108
|
}
|
|
123
109
|
: {}
|
|
124
110
|
|
|
111
|
+
const a11yLabel = accessibilityLabel ?? (subtitle ? `${label}. ${subtitle}` : label)
|
|
112
|
+
|
|
125
113
|
return (
|
|
126
|
-
<Animated.View style={[
|
|
114
|
+
<Animated.View style={[animatedStyle, disabled && styles.disabled]} {...hoverHandlers}>
|
|
127
115
|
<TouchableOpacity
|
|
128
116
|
style={[styles.container, cardStyle, style]}
|
|
129
117
|
onPress={handlePress}
|
|
130
|
-
onPressIn={
|
|
131
|
-
onPressOut={
|
|
118
|
+
onPressIn={onPressIn}
|
|
119
|
+
onPressOut={onPressOut}
|
|
132
120
|
disabled={disabled}
|
|
133
121
|
activeOpacity={1}
|
|
134
122
|
touchSoundDisabled={true}
|
|
123
|
+
accessibilityRole="button"
|
|
124
|
+
accessibilityLabel={a11yLabel}
|
|
125
|
+
accessibilityState={{ disabled }}
|
|
135
126
|
>
|
|
136
127
|
{resolvedIcon ? (
|
|
137
128
|
<View style={styles.iconContainer}>{resolvedIcon}</View>
|
|
@@ -157,7 +148,7 @@ export function MenuItem({
|
|
|
157
148
|
</View>
|
|
158
149
|
|
|
159
150
|
{rightRender !== undefined ? (
|
|
160
|
-
<View
|
|
151
|
+
<View
|
|
161
152
|
style={styles.rightContainer}
|
|
162
153
|
onStartShouldSetResponder={() => true}
|
|
163
154
|
onResponderRelease={() => {}}
|
|
@@ -56,16 +56,23 @@ export function MonthPicker({ value, onChange, locale = 'en', formatLabel, style
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
return (
|
|
59
|
-
<View style={[styles.container, style]}>
|
|
59
|
+
<View style={[styles.container, style]} accessibilityRole="adjustable" accessibilityLabel={getLabel()}>
|
|
60
60
|
<TouchableOpacity
|
|
61
61
|
style={styles.arrow}
|
|
62
62
|
onPress={handlePrev}
|
|
63
63
|
activeOpacity={0.6}
|
|
64
64
|
touchSoundDisabled={true}
|
|
65
|
+
accessibilityRole="button"
|
|
66
|
+
accessibilityLabel="Previous month"
|
|
67
|
+
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
65
68
|
>
|
|
66
69
|
<Entypo name="chevron-left" size={22} color={colors.foreground} />
|
|
67
70
|
</TouchableOpacity>
|
|
68
|
-
<Text
|
|
71
|
+
<Text
|
|
72
|
+
style={[styles.label, { color: colors.foreground }]}
|
|
73
|
+
allowFontScaling={true}
|
|
74
|
+
accessibilityLiveRegion="polite"
|
|
75
|
+
>
|
|
69
76
|
{getLabel()}
|
|
70
77
|
</Text>
|
|
71
78
|
<TouchableOpacity
|
|
@@ -73,6 +80,9 @@ export function MonthPicker({ value, onChange, locale = 'en', formatLabel, style
|
|
|
73
80
|
onPress={handleNext}
|
|
74
81
|
activeOpacity={0.6}
|
|
75
82
|
touchSoundDisabled={true}
|
|
83
|
+
accessibilityRole="button"
|
|
84
|
+
accessibilityLabel="Next month"
|
|
85
|
+
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
76
86
|
>
|
|
77
87
|
<Entypo name="chevron-right" size={22} color={colors.foreground} />
|
|
78
88
|
</TouchableOpacity>
|