@retray-dev/ui-kit 2.6.0 → 2.8.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 +125 -1
- package/README.md +20 -2
- package/dist/index.d.mts +95 -1
- package/dist/index.d.ts +95 -1
- package/dist/index.js +453 -293
- package/dist/index.mjs +377 -223
- package/package.json +1 -1
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Checkbox/Checkbox.tsx +1 -0
- package/src/components/IconButton/IconButton.tsx +147 -0
- package/src/components/IconButton/index.ts +2 -0
- package/src/components/RadioGroup/RadioGroup.tsx +1 -0
- package/src/components/Select/Select.tsx +2 -2
- package/src/components/Sheet/Sheet.tsx +2 -2
- package/src/components/Tabs/Tabs.tsx +1 -0
- package/src/components/Toast/Toast.tsx +2 -2
- package/src/components/Toggle/Toggle.tsx +1 -1
- package/src/index.ts +18 -0
- package/src/tokens.ts +69 -0
- package/src/utils/haptics.ts +18 -12
- package/src/components/Alert/Alert.tsx +0 -84
package/package.json
CHANGED
|
@@ -148,7 +148,7 @@ export function Button({
|
|
|
148
148
|
) : (
|
|
149
149
|
<>
|
|
150
150
|
{effectiveIcon && iconPosition === 'left' && <>{effectiveIcon}</>}
|
|
151
|
-
<Text style={[styles.label, labelVariantStyle, labelSizeStyles[size], effectiveIcon ? styles.labelWithIcon : undefined]}>{label}</Text>
|
|
151
|
+
<Text style={[styles.label, labelVariantStyle, labelSizeStyles[size], effectiveIcon ? styles.labelWithIcon : undefined]} allowFontScaling={true}>{label}</Text>
|
|
152
152
|
{effectiveIcon && iconPosition === 'right' && <>{effectiveIcon}</>}
|
|
153
153
|
</>
|
|
154
154
|
)}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React, { useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
TouchableOpacity,
|
|
4
|
+
Animated,
|
|
5
|
+
ActivityIndicator,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
TouchableOpacityProps,
|
|
8
|
+
ViewStyle,
|
|
9
|
+
Platform,
|
|
10
|
+
} from 'react-native'
|
|
11
|
+
|
|
12
|
+
const nativeDriver = Platform.OS !== 'web'
|
|
13
|
+
import { impactLight } from '../../utils/haptics'
|
|
14
|
+
import { useTheme } from '../../theme'
|
|
15
|
+
import { s } from '../../utils/scaling'
|
|
16
|
+
import { renderIcon } from '../../utils/icons'
|
|
17
|
+
|
|
18
|
+
export type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'
|
|
19
|
+
export type IconButtonSize = 'sm' | 'md' | 'lg'
|
|
20
|
+
|
|
21
|
+
export interface IconButtonProps extends TouchableOpacityProps {
|
|
22
|
+
/**
|
|
23
|
+
* Icon name from `@expo/vector-icons` (e.g. `"home"`, `"star"`, `"plus"`).
|
|
24
|
+
* See https://icons.expo.fyi. Takes precedence over `icon` when both supplied.
|
|
25
|
+
*/
|
|
26
|
+
iconName?: string
|
|
27
|
+
/** ReactNode icon — used when `iconName` is not provided. */
|
|
28
|
+
icon?: React.ReactNode
|
|
29
|
+
/** Override the resolved icon color. Defaults to the foreground color for the active variant. */
|
|
30
|
+
iconColor?: string
|
|
31
|
+
variant?: IconButtonVariant
|
|
32
|
+
size?: IconButtonSize
|
|
33
|
+
/** Replaces icon with a spinner and forces `disabled`. */
|
|
34
|
+
loading?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sizeMap: Record<IconButtonSize, { container: number; icon: number }> = {
|
|
38
|
+
sm: { container: s(40), icon: 18 },
|
|
39
|
+
md: { container: s(44), icon: 20 },
|
|
40
|
+
lg: { container: s(52), icon: 24 },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function IconButton({
|
|
44
|
+
iconName,
|
|
45
|
+
icon,
|
|
46
|
+
iconColor,
|
|
47
|
+
variant = 'primary',
|
|
48
|
+
size = 'md',
|
|
49
|
+
loading = false,
|
|
50
|
+
disabled,
|
|
51
|
+
style,
|
|
52
|
+
onPress,
|
|
53
|
+
...props
|
|
54
|
+
}: IconButtonProps) {
|
|
55
|
+
const { colors } = useTheme()
|
|
56
|
+
const isDisabled = disabled || loading
|
|
57
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
58
|
+
|
|
59
|
+
const handlePressIn = () => {
|
|
60
|
+
if (isDisabled) return
|
|
61
|
+
Animated.spring(scale, {
|
|
62
|
+
toValue: 0.95,
|
|
63
|
+
useNativeDriver: nativeDriver,
|
|
64
|
+
speed: 40,
|
|
65
|
+
bounciness: 0,
|
|
66
|
+
}).start()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handlePressOut = () => {
|
|
70
|
+
Animated.spring(scale, {
|
|
71
|
+
toValue: 1,
|
|
72
|
+
useNativeDriver: nativeDriver,
|
|
73
|
+
speed: 40,
|
|
74
|
+
bounciness: 4,
|
|
75
|
+
}).start()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const handlePress: TouchableOpacityProps['onPress'] = (e) => {
|
|
79
|
+
impactLight()
|
|
80
|
+
onPress?.(e)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const containerVariantStyle: ViewStyle = {
|
|
84
|
+
primary: { backgroundColor: colors.primary },
|
|
85
|
+
secondary: { backgroundColor: colors.secondary },
|
|
86
|
+
outline: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.border },
|
|
87
|
+
ghost: { backgroundColor: 'transparent' },
|
|
88
|
+
destructive: { backgroundColor: colors.destructive },
|
|
89
|
+
}[variant]
|
|
90
|
+
|
|
91
|
+
const defaultIconColor: string = {
|
|
92
|
+
primary: colors.primaryForeground,
|
|
93
|
+
secondary: colors.secondaryForeground,
|
|
94
|
+
outline: colors.foreground,
|
|
95
|
+
ghost: colors.foreground,
|
|
96
|
+
destructive: colors.destructiveForeground,
|
|
97
|
+
}[variant]
|
|
98
|
+
|
|
99
|
+
const spinnerColor =
|
|
100
|
+
variant === 'destructive' ? colors.destructiveForeground
|
|
101
|
+
: variant === 'primary' || variant === 'secondary' ? colors.primaryForeground
|
|
102
|
+
: colors.foreground
|
|
103
|
+
|
|
104
|
+
const { container: containerSize, icon: iconSize } = sizeMap[size]
|
|
105
|
+
|
|
106
|
+
const resolvedIcon: React.ReactNode = iconName
|
|
107
|
+
? renderIcon(iconName, iconSize, iconColor ?? defaultIconColor)
|
|
108
|
+
: icon
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Animated.View style={{ transform: [{ scale }] }}>
|
|
112
|
+
<TouchableOpacity
|
|
113
|
+
style={[
|
|
114
|
+
styles.base,
|
|
115
|
+
containerVariantStyle,
|
|
116
|
+
{ width: containerSize, height: containerSize },
|
|
117
|
+
isDisabled && styles.disabled,
|
|
118
|
+
style,
|
|
119
|
+
]}
|
|
120
|
+
disabled={isDisabled}
|
|
121
|
+
activeOpacity={1}
|
|
122
|
+
touchSoundDisabled={true}
|
|
123
|
+
onPress={handlePress}
|
|
124
|
+
onPressIn={handlePressIn}
|
|
125
|
+
onPressOut={handlePressOut}
|
|
126
|
+
{...props}
|
|
127
|
+
>
|
|
128
|
+
{loading ? (
|
|
129
|
+
<ActivityIndicator size="small" color={spinnerColor} />
|
|
130
|
+
) : (
|
|
131
|
+
resolvedIcon
|
|
132
|
+
)}
|
|
133
|
+
</TouchableOpacity>
|
|
134
|
+
</Animated.View>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const styles = StyleSheet.create({
|
|
139
|
+
base: {
|
|
140
|
+
borderRadius: 999,
|
|
141
|
+
alignItems: 'center',
|
|
142
|
+
justifyContent: 'center',
|
|
143
|
+
},
|
|
144
|
+
disabled: {
|
|
145
|
+
opacity: 0.5,
|
|
146
|
+
},
|
|
147
|
+
})
|
|
@@ -79,7 +79,7 @@ export function Select({
|
|
|
79
79
|
|
|
80
80
|
return (
|
|
81
81
|
<View style={[styles.container, style]}>
|
|
82
|
-
{label ? <Text style={[styles.label, { color: colors.foreground }]}>{label}</Text> : null}
|
|
82
|
+
{label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
|
|
83
83
|
|
|
84
84
|
{/* Trigger button — shown on iOS and Android only */}
|
|
85
85
|
{!isWeb ? (
|
|
@@ -220,7 +220,7 @@ export function Select({
|
|
|
220
220
|
) : null}
|
|
221
221
|
|
|
222
222
|
{error ? (
|
|
223
|
-
<Text style={[styles.helperText, { color: colors.destructive }]}>{error}</Text>
|
|
223
|
+
<Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
|
|
224
224
|
) : null}
|
|
225
225
|
</View>
|
|
226
226
|
)
|
|
@@ -72,10 +72,10 @@ export function Sheet({
|
|
|
72
72
|
{title || description ? (
|
|
73
73
|
<View style={styles.header}>
|
|
74
74
|
{title ? (
|
|
75
|
-
<Text style={[styles.title, { color: colors.cardForeground }]}>{title}</Text>
|
|
75
|
+
<Text style={[styles.title, { color: colors.cardForeground }]} allowFontScaling={true}>{title}</Text>
|
|
76
76
|
) : null}
|
|
77
77
|
{description ? (
|
|
78
|
-
<Text style={[styles.description, { color: colors.mutedForeground }]}>
|
|
78
|
+
<Text style={[styles.description, { color: colors.mutedForeground }]} allowFontScaling={true}>
|
|
79
79
|
{description}
|
|
80
80
|
</Text>
|
|
81
81
|
) : null}
|
|
@@ -127,10 +127,10 @@ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: ()
|
|
|
127
127
|
<View style={styles.leftIconContainer}>{leftIcon}</View>
|
|
128
128
|
<View style={styles.toastContent}>
|
|
129
129
|
{item.title ? (
|
|
130
|
-
<Text style={[styles.toastTitle, { color: textColor }]}>{item.title}</Text>
|
|
130
|
+
<Text style={[styles.toastTitle, { color: textColor }]} allowFontScaling={true}>{item.title}</Text>
|
|
131
131
|
) : null}
|
|
132
132
|
{item.description ? (
|
|
133
|
-
<Text style={[styles.toastDescription, { color: textColor, opacity: 0.85 }]}>
|
|
133
|
+
<Text style={[styles.toastDescription, { color: textColor, opacity: 0.85 }]} allowFontScaling={true}>
|
|
134
134
|
{item.description}
|
|
135
135
|
</Text>
|
|
136
136
|
) : null}
|
|
@@ -146,7 +146,7 @@ export function Toggle({
|
|
|
146
146
|
>
|
|
147
147
|
<View style={styles.inner}>
|
|
148
148
|
<LeftIcon />
|
|
149
|
-
{label ? <Animated.Text style={[styles.label, { color: textColor }]}>{label}</Animated.Text> : null}
|
|
149
|
+
{label ? <Animated.Text style={[styles.label, { color: textColor }]} allowFontScaling={true}>{label}</Animated.Text> : null}
|
|
150
150
|
</View>
|
|
151
151
|
</Animated.View>
|
|
152
152
|
</TouchableOpacity>
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { defaultLight, defaultDark } from './theme'
|
|
|
5
5
|
|
|
6
6
|
// Components
|
|
7
7
|
export * from './components/Button'
|
|
8
|
+
export * from './components/IconButton'
|
|
8
9
|
export * from './components/Text'
|
|
9
10
|
export * from './components/Input'
|
|
10
11
|
export * from './components/Badge'
|
|
@@ -40,3 +41,20 @@ export * from './components/MonthPicker'
|
|
|
40
41
|
// Icon utility
|
|
41
42
|
export { Icon, renderIcon } from './utils/icons'
|
|
42
43
|
export type { IconProps, IconFamily } from './utils/icons'
|
|
44
|
+
|
|
45
|
+
// Design tokens
|
|
46
|
+
export {
|
|
47
|
+
SPACING,
|
|
48
|
+
ICON_SIZES,
|
|
49
|
+
RADIUS,
|
|
50
|
+
SHADOWS,
|
|
51
|
+
BREAKPOINTS,
|
|
52
|
+
} from './tokens'
|
|
53
|
+
export type {
|
|
54
|
+
Spacing,
|
|
55
|
+
SpacingKey,
|
|
56
|
+
IconSize,
|
|
57
|
+
IconSizeKey,
|
|
58
|
+
Radius,
|
|
59
|
+
RadiusKey,
|
|
60
|
+
} from './tokens'
|
package/src/tokens.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const SPACING = {
|
|
2
|
+
xs: 4,
|
|
3
|
+
sm: 8,
|
|
4
|
+
md: 12,
|
|
5
|
+
lg: 16,
|
|
6
|
+
xl: 24,
|
|
7
|
+
'2xl': 32,
|
|
8
|
+
'3xl': 48,
|
|
9
|
+
} as const
|
|
10
|
+
|
|
11
|
+
export const ICON_SIZES = {
|
|
12
|
+
sm: 14,
|
|
13
|
+
md: 18,
|
|
14
|
+
lg: 22,
|
|
15
|
+
xl: 28,
|
|
16
|
+
'2xl': 32,
|
|
17
|
+
} as const
|
|
18
|
+
|
|
19
|
+
export const RADIUS = {
|
|
20
|
+
sm: 4,
|
|
21
|
+
md: 8,
|
|
22
|
+
lg: 12,
|
|
23
|
+
xl: 16,
|
|
24
|
+
full: 9999,
|
|
25
|
+
} as const
|
|
26
|
+
|
|
27
|
+
export const SHADOWS = {
|
|
28
|
+
sm: {
|
|
29
|
+
shadowColor: '#000',
|
|
30
|
+
shadowOffset: { width: 0, height: 1 },
|
|
31
|
+
shadowOpacity: 0.08,
|
|
32
|
+
shadowRadius: 4,
|
|
33
|
+
elevation: 2,
|
|
34
|
+
},
|
|
35
|
+
md: {
|
|
36
|
+
shadowColor: '#000',
|
|
37
|
+
shadowOffset: { width: 0, height: 3 },
|
|
38
|
+
shadowOpacity: 0.12,
|
|
39
|
+
shadowRadius: 8,
|
|
40
|
+
elevation: 5,
|
|
41
|
+
},
|
|
42
|
+
lg: {
|
|
43
|
+
shadowColor: '#000',
|
|
44
|
+
shadowOffset: { width: 0, height: 6 },
|
|
45
|
+
shadowOpacity: 0.2,
|
|
46
|
+
shadowRadius: 16,
|
|
47
|
+
elevation: 10,
|
|
48
|
+
},
|
|
49
|
+
xl: {
|
|
50
|
+
shadowColor: '#000',
|
|
51
|
+
shadowOffset: { width: 0, height: 12 },
|
|
52
|
+
shadowOpacity: 0.28,
|
|
53
|
+
shadowRadius: 24,
|
|
54
|
+
elevation: 18,
|
|
55
|
+
},
|
|
56
|
+
} as const
|
|
57
|
+
|
|
58
|
+
export const BREAKPOINTS = {
|
|
59
|
+
wide: 700,
|
|
60
|
+
} as const
|
|
61
|
+
|
|
62
|
+
export type Spacing = typeof SPACING
|
|
63
|
+
export type SpacingKey = keyof Spacing
|
|
64
|
+
|
|
65
|
+
export type IconSize = typeof ICON_SIZES
|
|
66
|
+
export type IconSizeKey = keyof IconSize
|
|
67
|
+
|
|
68
|
+
export type Radius = typeof RADIUS
|
|
69
|
+
export type RadiusKey = keyof Radius
|
package/src/utils/haptics.ts
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
import { Platform } from 'react-native'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* Web-safe haptics helpers. All calls are no-ops on web since expo-haptics
|
|
5
|
-
* is a native-only module and throws on web.
|
|
6
|
-
*/
|
|
3
|
+
type HapticsModule = typeof import('expo-haptics')
|
|
7
4
|
|
|
8
|
-
let
|
|
5
|
+
let _haptics: HapticsModule | null = null
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
async function getHaptics(): Promise<HapticsModule | null> {
|
|
8
|
+
if (Platform.OS === 'web') return null
|
|
9
|
+
if (!_haptics) {
|
|
10
|
+
_haptics = await import('expo-haptics')
|
|
11
|
+
}
|
|
12
|
+
return _haptics
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function selectionAsync(): void {
|
|
15
|
-
|
|
16
|
+
if (Platform.OS === 'web') return
|
|
17
|
+
getHaptics().then(h => h?.selectionAsync())
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function impactLight(): void {
|
|
19
|
-
|
|
21
|
+
if (Platform.OS === 'web') return
|
|
22
|
+
getHaptics().then(h => h?.impactAsync(h.ImpactFeedbackStyle.Light))
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export function impactMedium(): void {
|
|
23
|
-
|
|
26
|
+
if (Platform.OS === 'web') return
|
|
27
|
+
getHaptics().then(h => h?.impactAsync(h.ImpactFeedbackStyle.Medium))
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export function notificationSuccess(): void {
|
|
27
|
-
|
|
31
|
+
if (Platform.OS === 'web') return
|
|
32
|
+
getHaptics().then(h => h?.notificationAsync(h.NotificationFeedbackType.Success))
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
export function notificationError(): void {
|
|
31
|
-
|
|
36
|
+
if (Platform.OS === 'web') return
|
|
37
|
+
getHaptics().then(h => h?.notificationAsync(h.NotificationFeedbackType.Error))
|
|
32
38
|
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
-
import { useTheme } from '../../theme'
|
|
4
|
-
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
5
|
-
|
|
6
|
-
export type AlertBannerVariant = 'default' | 'destructive' | 'success'
|
|
7
|
-
|
|
8
|
-
export interface AlertBannerProps {
|
|
9
|
-
title?: string
|
|
10
|
-
description?: string
|
|
11
|
-
variant?: AlertBannerVariant
|
|
12
|
-
icon?: React.ReactNode
|
|
13
|
-
style?: ViewStyle
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function AlertBanner({ title, description, variant = 'default', icon, style }: AlertBannerProps) {
|
|
17
|
-
const { colors } = useTheme()
|
|
18
|
-
|
|
19
|
-
const borderColor =
|
|
20
|
-
variant === 'destructive' ? colors.destructive
|
|
21
|
-
: variant === 'success' ? colors.success
|
|
22
|
-
: colors.border
|
|
23
|
-
|
|
24
|
-
const titleColor =
|
|
25
|
-
variant === 'destructive' ? colors.destructive
|
|
26
|
-
: variant === 'success' ? colors.success
|
|
27
|
-
: colors.foreground
|
|
28
|
-
|
|
29
|
-
const descColor =
|
|
30
|
-
variant === 'destructive' ? colors.destructive
|
|
31
|
-
: variant === 'success' ? colors.success
|
|
32
|
-
: colors.mutedForeground
|
|
33
|
-
|
|
34
|
-
const defaultIcon =
|
|
35
|
-
variant === 'destructive' ? '⚠' : variant === 'success' ? '✓' : 'ℹ'
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<View style={[styles.container, { backgroundColor: colors.card, borderColor }, style]}>
|
|
39
|
-
{icon ? (
|
|
40
|
-
<View style={styles.icon}>{icon}</View>
|
|
41
|
-
) : (
|
|
42
|
-
<View style={styles.icon}>
|
|
43
|
-
<Text style={[styles.defaultIcon, { color: titleColor }]} allowFontScaling={true}>{defaultIcon}</Text>
|
|
44
|
-
</View>
|
|
45
|
-
)}
|
|
46
|
-
<View style={styles.content}>
|
|
47
|
-
{title ? <Text style={[styles.title, { color: titleColor }]} allowFontScaling={true}>{title}</Text> : null}
|
|
48
|
-
{description ? (
|
|
49
|
-
<Text style={[styles.description, { color: descColor }]} allowFontScaling={true}>{description}</Text>
|
|
50
|
-
) : null}
|
|
51
|
-
</View>
|
|
52
|
-
</View>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const styles = StyleSheet.create({
|
|
57
|
-
container: {
|
|
58
|
-
flexDirection: 'row',
|
|
59
|
-
borderWidth: 1,
|
|
60
|
-
borderRadius: ms(8),
|
|
61
|
-
padding: s(16),
|
|
62
|
-
gap: s(12),
|
|
63
|
-
},
|
|
64
|
-
icon: {
|
|
65
|
-
marginTop: vs(2),
|
|
66
|
-
},
|
|
67
|
-
content: {
|
|
68
|
-
flex: 1,
|
|
69
|
-
gap: vs(4),
|
|
70
|
-
},
|
|
71
|
-
title: {
|
|
72
|
-
fontSize: ms(14),
|
|
73
|
-
fontWeight: '500',
|
|
74
|
-
lineHeight: mvs(20),
|
|
75
|
-
},
|
|
76
|
-
description: {
|
|
77
|
-
fontSize: ms(14),
|
|
78
|
-
lineHeight: mvs(20),
|
|
79
|
-
},
|
|
80
|
-
defaultIcon: {
|
|
81
|
-
fontSize: ms(18),
|
|
82
|
-
fontWeight: '700',
|
|
83
|
-
},
|
|
84
|
-
})
|