@retray-dev/ui-kit 2.5.2 → 2.7.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 +245 -5
- package/README.md +18 -0
- package/dist/index.d.mts +173 -8
- package/dist/index.d.ts +173 -8
- package/dist/index.js +319 -179
- package/dist/index.mjs +259 -132
- package/package.json +1 -1
- package/src/components/AlertBanner/AlertBanner.tsx +14 -2
- package/src/components/Badge/Badge.tsx +16 -2
- package/src/components/Button/Button.tsx +20 -3
- package/src/components/EmptyState/EmptyState.tsx +15 -3
- package/src/components/Input/Input.tsx +29 -8
- package/src/components/ListItem/ListItem.tsx +26 -3
- package/src/components/Toast/Toast.tsx +11 -1
- package/src/components/Toggle/Toggle.tsx +27 -2
- package/src/index.ts +21 -0
- package/src/tokens.ts +69 -0
- package/src/utils/haptics.ts +18 -12
- package/src/utils/icons.ts +73 -0
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
|
3
3
|
import { FontAwesome5, MaterialIcons, Entypo } from '@expo/vector-icons'
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
5
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
6
|
+
import { renderIcon } from '../../utils/icons'
|
|
6
7
|
|
|
7
8
|
export type AlertBannerVariant = 'default' | 'destructive' | 'success'
|
|
8
9
|
|
|
@@ -11,10 +12,17 @@ export interface AlertBannerProps {
|
|
|
11
12
|
description?: string
|
|
12
13
|
variant?: AlertBannerVariant
|
|
13
14
|
icon?: React.ReactNode
|
|
15
|
+
/**
|
|
16
|
+
* Icon name from `@expo/vector-icons`. See https://icons.expo.fyi.
|
|
17
|
+
* Takes precedence over `icon`. When neither is set, a default variant icon is shown.
|
|
18
|
+
*/
|
|
19
|
+
iconName?: string
|
|
20
|
+
/** Override the resolved icon color. Defaults to the variant title color. */
|
|
21
|
+
iconColor?: string
|
|
14
22
|
style?: ViewStyle
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
export function AlertBanner({ title, description, variant = 'default', icon, style }: AlertBannerProps) {
|
|
25
|
+
export function AlertBanner({ title, description, variant = 'default', icon, iconName, iconColor, style }: AlertBannerProps) {
|
|
18
26
|
const { colors } = useTheme()
|
|
19
27
|
|
|
20
28
|
const borderColor =
|
|
@@ -41,9 +49,13 @@ export function AlertBanner({ title, description, variant = 'default', icon, sty
|
|
|
41
49
|
<Entypo name="info-with-circle" size={18} color={titleColor} />
|
|
42
50
|
)
|
|
43
51
|
|
|
52
|
+
const effectiveIcon: React.ReactNode = iconName
|
|
53
|
+
? renderIcon(iconName, 18, iconColor ?? titleColor)
|
|
54
|
+
: icon ?? defaultIcon
|
|
55
|
+
|
|
44
56
|
return (
|
|
45
57
|
<View style={[styles.container, { backgroundColor: colors.card, borderColor }, style]}>
|
|
46
|
-
<View style={styles.icon}>{
|
|
58
|
+
<View style={styles.icon}>{effectiveIcon}</View>
|
|
47
59
|
<View style={styles.content}>
|
|
48
60
|
{title ? <Text style={[styles.title, { color: titleColor }]} allowFontScaling={true}>{title}</Text> : null}
|
|
49
61
|
{description ? (
|
|
@@ -2,6 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
4
|
import { s, vs, ms } from '../../utils/scaling'
|
|
5
|
+
import { renderIcon } from '../../utils/icons'
|
|
5
6
|
|
|
6
7
|
export type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline'
|
|
7
8
|
export type BadgeSize = 'sm' | 'md' | 'lg'
|
|
@@ -14,6 +15,13 @@ export interface BadgeProps {
|
|
|
14
15
|
size?: BadgeSize
|
|
15
16
|
/** Icon rendered before the label/children. */
|
|
16
17
|
icon?: React.ReactNode
|
|
18
|
+
/**
|
|
19
|
+
* Icon name from `@expo/vector-icons` rendered before the label.
|
|
20
|
+
* See https://icons.expo.fyi. Takes precedence over `icon`.
|
|
21
|
+
*/
|
|
22
|
+
iconName?: string
|
|
23
|
+
/** Override the resolved icon color. Defaults to the variant foreground color. */
|
|
24
|
+
iconColor?: string
|
|
17
25
|
style?: ViewStyle
|
|
18
26
|
}
|
|
19
27
|
|
|
@@ -35,7 +43,9 @@ const sizeIconGap: Record<BadgeSize, number> = {
|
|
|
35
43
|
lg: s(6),
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
const sizeIconSize: Record<BadgeSize, number> = { sm: 10, md: 12, lg: 14 }
|
|
47
|
+
|
|
48
|
+
export function Badge({ label, children, variant = 'default', size = 'md', icon, iconName, iconColor, style }: BadgeProps) {
|
|
39
49
|
const { colors } = useTheme()
|
|
40
50
|
|
|
41
51
|
const containerStyle: ViewStyle = {
|
|
@@ -52,11 +62,15 @@ export function Badge({ label, children, variant = 'default', size = 'md', icon,
|
|
|
52
62
|
outline: colors.foreground,
|
|
53
63
|
}[variant]
|
|
54
64
|
|
|
65
|
+
const effectiveIcon: React.ReactNode = iconName
|
|
66
|
+
? renderIcon(iconName, sizeIconSize[size], iconColor ?? textColor)
|
|
67
|
+
: icon
|
|
68
|
+
|
|
55
69
|
const content = children ?? label
|
|
56
70
|
|
|
57
71
|
return (
|
|
58
72
|
<View style={[styles.container, containerStyle, sizePadding[size], { gap: sizeIconGap[size] }, style]}>
|
|
59
|
-
{
|
|
73
|
+
{effectiveIcon ? <View style={styles.iconContainer}>{effectiveIcon}</View> : null}
|
|
60
74
|
{typeof content === 'string' ? (
|
|
61
75
|
<Text style={[styles.label, { color: textColor }, sizeFontSize[size]]} allowFontScaling={true}>
|
|
62
76
|
{content}
|
|
@@ -15,6 +15,7 @@ const nativeDriver = Platform.OS !== 'web'
|
|
|
15
15
|
import { impactLight } from '../../utils/haptics'
|
|
16
16
|
import { useTheme } from '../../theme'
|
|
17
17
|
import { s, vs, ms } from '../../utils/scaling'
|
|
18
|
+
import { renderIcon } from '../../utils/icons'
|
|
18
19
|
|
|
19
20
|
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'
|
|
20
21
|
export type ButtonSize = 'sm' | 'md' | 'lg'
|
|
@@ -34,6 +35,14 @@ export interface ButtonProps extends TouchableOpacityProps {
|
|
|
34
35
|
fullWidth?: boolean
|
|
35
36
|
/** Icon rendered alongside the label. Can be a ReactNode or a render function `(props) => ReactNode`. */
|
|
36
37
|
icon?: React.ReactNode | ((props: { label: string; size: ButtonSize; variant: ButtonVariant }) => React.ReactNode)
|
|
38
|
+
/**
|
|
39
|
+
* Icon name from `@expo/vector-icons` (e.g. `"home"`, `"star"`, `"arrow-right"`).
|
|
40
|
+
* See https://icons.expo.fyi to browse available icons.
|
|
41
|
+
* Takes precedence over `icon` when both are supplied.
|
|
42
|
+
*/
|
|
43
|
+
iconName?: string
|
|
44
|
+
/** Override the resolved icon color. Defaults to the label foreground color for the active variant. */
|
|
45
|
+
iconColor?: string
|
|
37
46
|
/** Side the icon appears on. Defaults to `'left'`. */
|
|
38
47
|
iconPosition?: 'left' | 'right'
|
|
39
48
|
}
|
|
@@ -50,6 +59,8 @@ const labelSizeStyles: Record<ButtonSize, TextStyle> = {
|
|
|
50
59
|
lg: { fontSize: ms(18) },
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
const iconSizeMap: Record<ButtonSize, number> = { sm: 16, md: 18, lg: 20 }
|
|
63
|
+
|
|
53
64
|
export function Button({
|
|
54
65
|
label,
|
|
55
66
|
variant = 'primary',
|
|
@@ -57,6 +68,8 @@ export function Button({
|
|
|
57
68
|
loading = false,
|
|
58
69
|
fullWidth = false,
|
|
59
70
|
icon,
|
|
71
|
+
iconName,
|
|
72
|
+
iconColor,
|
|
60
73
|
iconPosition = 'left',
|
|
61
74
|
disabled,
|
|
62
75
|
style,
|
|
@@ -102,6 +115,10 @@ export function Button({
|
|
|
102
115
|
destructive: { color: colors.destructiveForeground },
|
|
103
116
|
}[variant]
|
|
104
117
|
|
|
118
|
+
const effectiveIcon: React.ReactNode = iconName
|
|
119
|
+
? renderIcon(iconName, iconSizeMap[size], iconColor ?? (labelVariantStyle.color as string))
|
|
120
|
+
: typeof icon === 'function' ? icon({ label, size, variant }) : icon
|
|
121
|
+
|
|
105
122
|
const spinnerColor =
|
|
106
123
|
variant === 'destructive' ? colors.destructiveForeground
|
|
107
124
|
: variant === 'primary' || variant === 'secondary' ? colors.primaryForeground
|
|
@@ -130,9 +147,9 @@ export function Button({
|
|
|
130
147
|
<ActivityIndicator size="small" color={spinnerColor} />
|
|
131
148
|
) : (
|
|
132
149
|
<>
|
|
133
|
-
{
|
|
134
|
-
<Text style={[styles.label, labelVariantStyle, labelSizeStyles[size],
|
|
135
|
-
{
|
|
150
|
+
{effectiveIcon && iconPosition === 'left' && <>{effectiveIcon}</>}
|
|
151
|
+
<Text style={[styles.label, labelVariantStyle, labelSizeStyles[size], effectiveIcon ? styles.labelWithIcon : undefined]}>{label}</Text>
|
|
152
|
+
{effectiveIcon && iconPosition === 'right' && <>{effectiveIcon}</>}
|
|
136
153
|
</>
|
|
137
154
|
)}
|
|
138
155
|
</TouchableOpacity>
|
|
@@ -2,9 +2,17 @@ import React from 'react'
|
|
|
2
2
|
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
4
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
5
|
+
import { renderIcon } from '../../utils/icons'
|
|
5
6
|
|
|
6
7
|
export interface EmptyStateProps {
|
|
7
8
|
icon?: React.ReactNode
|
|
9
|
+
/**
|
|
10
|
+
* Icon name from `@expo/vector-icons`. See https://icons.expo.fyi.
|
|
11
|
+
* Takes precedence over `icon`. Sized automatically to fit the slot (48 default, 32 compact).
|
|
12
|
+
*/
|
|
13
|
+
iconName?: string
|
|
14
|
+
/** Override the resolved icon color. Defaults to `mutedForeground`. */
|
|
15
|
+
iconColor?: string
|
|
8
16
|
title: string
|
|
9
17
|
description?: string
|
|
10
18
|
action?: React.ReactNode
|
|
@@ -13,10 +21,14 @@ export interface EmptyStateProps {
|
|
|
13
21
|
style?: ViewStyle
|
|
14
22
|
}
|
|
15
23
|
|
|
16
|
-
export function EmptyState({ icon, title, description, action, size = 'default', style }: EmptyStateProps) {
|
|
24
|
+
export function EmptyState({ icon, iconName, iconColor, title, description, action, size = 'default', style }: EmptyStateProps) {
|
|
17
25
|
const { colors } = useTheme()
|
|
18
26
|
const isCompact = size === 'compact'
|
|
19
27
|
|
|
28
|
+
const effectiveIcon: React.ReactNode = iconName
|
|
29
|
+
? renderIcon(iconName, isCompact ? 32 : 48, iconColor ?? colors.mutedForeground)
|
|
30
|
+
: icon
|
|
31
|
+
|
|
20
32
|
return (
|
|
21
33
|
<View
|
|
22
34
|
style={[
|
|
@@ -26,7 +38,7 @@ export function EmptyState({ icon, title, description, action, size = 'default',
|
|
|
26
38
|
style,
|
|
27
39
|
]}
|
|
28
40
|
>
|
|
29
|
-
{
|
|
41
|
+
{effectiveIcon ? (
|
|
30
42
|
<View
|
|
31
43
|
style={[
|
|
32
44
|
styles.iconWrapper,
|
|
@@ -34,7 +46,7 @@ export function EmptyState({ icon, title, description, action, size = 'default',
|
|
|
34
46
|
{ backgroundColor: colors.muted },
|
|
35
47
|
]}
|
|
36
48
|
>
|
|
37
|
-
{
|
|
49
|
+
{effectiveIcon}
|
|
38
50
|
</View>
|
|
39
51
|
) : null}
|
|
40
52
|
<View style={styles.textWrapper}>
|
|
@@ -3,6 +3,7 @@ import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, TextStyle
|
|
|
3
3
|
import { AntDesign } from '@expo/vector-icons'
|
|
4
4
|
import { useTheme } from '../../theme'
|
|
5
5
|
import { s, vs, ms } from '../../utils/scaling'
|
|
6
|
+
import { renderIcon } from '../../utils/icons'
|
|
6
7
|
|
|
7
8
|
const webInputResetStyle: any =
|
|
8
9
|
Platform.OS === 'web'
|
|
@@ -23,13 +24,27 @@ export interface InputProps extends TextInputProps {
|
|
|
23
24
|
prefixStyle?: TextStyle
|
|
24
25
|
/** Style applied to suffix text if suffix is a string. */
|
|
25
26
|
suffixStyle?: TextStyle
|
|
27
|
+
/**
|
|
28
|
+
* Icon name from `@expo/vector-icons` rendered before the input text.
|
|
29
|
+
* See https://icons.expo.fyi. Takes precedence over `prefix`.
|
|
30
|
+
*/
|
|
31
|
+
prefixIcon?: string
|
|
32
|
+
/**
|
|
33
|
+
* Icon name from `@expo/vector-icons` rendered after the input text.
|
|
34
|
+
* See https://icons.expo.fyi. Takes precedence over `suffix` (unless `type="password"`).
|
|
35
|
+
*/
|
|
36
|
+
suffixIcon?: string
|
|
37
|
+
/** Override the resolved prefix icon color. Defaults to `mutedForeground`. */
|
|
38
|
+
prefixIconColor?: string
|
|
39
|
+
/** Override the resolved suffix icon color. Defaults to `mutedForeground`. */
|
|
40
|
+
suffixIconColor?: string
|
|
26
41
|
/** Input type. When set to \`'password'\`, shows a toggle button to reveal/hide text. */
|
|
27
42
|
type?: 'text' | 'password'
|
|
28
43
|
/** Style for the outer container \`View\`. Use \`style\` (from \`TextInputProps\`) to style the \`TextInput\` itself. */
|
|
29
44
|
containerStyle?: ViewStyle
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixStyle, type = 'text', containerStyle, style, onFocus, onBlur, secureTextEntry, ...props }: InputProps) {
|
|
47
|
+
export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixStyle, prefixIcon, suffixIcon, prefixIconColor, suffixIconColor, type = 'text', containerStyle, style, onFocus, onBlur, secureTextEntry, ...props }: InputProps) {
|
|
33
48
|
const { colors } = useTheme()
|
|
34
49
|
const [focused, setFocused] = useState(false)
|
|
35
50
|
const [showPassword, setShowPassword] = useState(false)
|
|
@@ -37,12 +52,18 @@ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixS
|
|
|
37
52
|
const isPassword = type === 'password'
|
|
38
53
|
const effectiveSecure = isPassword ? !showPassword : secureTextEntry
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
const effectivePrefix: React.ReactNode = prefixIcon
|
|
56
|
+
? renderIcon(prefixIcon, 20, prefixIconColor ?? colors.mutedForeground)
|
|
57
|
+
: prefix
|
|
58
|
+
|
|
59
|
+
// If type is password and no suffix override is provided, add the toggle button
|
|
60
|
+
const effectiveSuffix: React.ReactNode = isPassword && !suffix && !suffixIcon ? (
|
|
42
61
|
<TouchableOpacity onPress={() => setShowPassword(!showPassword)} style={styles.passwordToggle} activeOpacity={0.6}>
|
|
43
62
|
<AntDesign name={showPassword ? 'eye' : 'eye-invisible'} size={20} color={colors.mutedForeground} />
|
|
44
63
|
</TouchableOpacity>
|
|
45
|
-
) :
|
|
64
|
+
) : suffixIcon
|
|
65
|
+
? renderIcon(suffixIcon, 20, suffixIconColor ?? colors.mutedForeground)
|
|
66
|
+
: suffix
|
|
46
67
|
|
|
47
68
|
return (
|
|
48
69
|
<View style={[styles.container, containerStyle]}>
|
|
@@ -60,13 +81,13 @@ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixS
|
|
|
60
81
|
},
|
|
61
82
|
]}
|
|
62
83
|
>
|
|
63
|
-
{
|
|
64
|
-
typeof
|
|
84
|
+
{effectivePrefix ? (
|
|
85
|
+
typeof effectivePrefix === 'string' ? (
|
|
65
86
|
<Text style={[styles.prefixText, { color: colors.mutedForeground }, prefixStyle]} allowFontScaling={true}>
|
|
66
|
-
{
|
|
87
|
+
{effectivePrefix}
|
|
67
88
|
</Text>
|
|
68
89
|
) : (
|
|
69
|
-
<View style={styles.prefixContainer}>{
|
|
90
|
+
<View style={styles.prefixContainer}>{effectivePrefix}</View>
|
|
70
91
|
)
|
|
71
92
|
) : null}
|
|
72
93
|
<TextInput
|
|
@@ -13,6 +13,7 @@ import { Entypo } from '@expo/vector-icons'
|
|
|
13
13
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
14
14
|
import { useTheme } from '../../theme'
|
|
15
15
|
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
16
|
+
import { renderIcon } from '../../utils/icons'
|
|
16
17
|
|
|
17
18
|
const nativeDriver = Platform.OS !== 'web'
|
|
18
19
|
|
|
@@ -33,6 +34,20 @@ export interface ListItemProps {
|
|
|
33
34
|
trailing?: React.ReactNode | string
|
|
34
35
|
/** @deprecated Use `leftRender` instead. */
|
|
35
36
|
icon?: React.ReactNode
|
|
37
|
+
/**
|
|
38
|
+
* Icon name from `@expo/vector-icons` rendered in the left slot.
|
|
39
|
+
* See https://icons.expo.fyi. Takes precedence over `leftRender`.
|
|
40
|
+
*/
|
|
41
|
+
leftIcon?: string
|
|
42
|
+
/**
|
|
43
|
+
* Icon name from `@expo/vector-icons` rendered in the right slot.
|
|
44
|
+
* See https://icons.expo.fyi. Takes precedence over `rightRender`.
|
|
45
|
+
*/
|
|
46
|
+
rightIcon?: string
|
|
47
|
+
/** Override the resolved left icon color. Defaults to `foreground`. */
|
|
48
|
+
leftIconColor?: string
|
|
49
|
+
/** Override the resolved right icon color. Defaults to `mutedForeground`. */
|
|
50
|
+
rightIconColor?: string
|
|
36
51
|
|
|
37
52
|
title: string
|
|
38
53
|
/** Secondary line below the title. */
|
|
@@ -69,6 +84,10 @@ export function ListItem({
|
|
|
69
84
|
rightRender,
|
|
70
85
|
trailing,
|
|
71
86
|
icon,
|
|
87
|
+
leftIcon,
|
|
88
|
+
rightIcon,
|
|
89
|
+
leftIconColor,
|
|
90
|
+
rightIconColor,
|
|
72
91
|
title,
|
|
73
92
|
subtitle,
|
|
74
93
|
caption,
|
|
@@ -109,9 +128,13 @@ export function ListItem({
|
|
|
109
128
|
onPress?.()
|
|
110
129
|
}
|
|
111
130
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
131
|
+
const effectiveLeft: React.ReactNode = leftIcon
|
|
132
|
+
? renderIcon(leftIcon, 24, leftIconColor ?? colors.foreground)
|
|
133
|
+
: leftRender ?? icon
|
|
134
|
+
|
|
135
|
+
const effectiveRight: React.ReactNode | string | undefined = rightIcon
|
|
136
|
+
? renderIcon(rightIcon, 24, rightIconColor ?? colors.mutedForeground)
|
|
137
|
+
: rightRender ?? trailing
|
|
115
138
|
|
|
116
139
|
const cardStyle: ViewStyle =
|
|
117
140
|
variant === 'card'
|
|
@@ -14,6 +14,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
|
14
14
|
import { notificationSuccess, notificationError, impactLight } from '../../utils/haptics'
|
|
15
15
|
import { useTheme } from '../../theme'
|
|
16
16
|
import { s, vs, ms } from '../../utils/scaling'
|
|
17
|
+
import { renderIcon } from '../../utils/icons'
|
|
17
18
|
|
|
18
19
|
export type ToastVariant = 'default' | 'destructive' | 'success'
|
|
19
20
|
|
|
@@ -23,6 +24,13 @@ export interface ToastItem {
|
|
|
23
24
|
description?: string
|
|
24
25
|
variant?: ToastVariant
|
|
25
26
|
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
|
+
iconName?: string
|
|
32
|
+
/** Override the resolved icon color. Defaults to the variant text color. */
|
|
33
|
+
iconColor?: string
|
|
26
34
|
/** Auto-dismiss delay in milliseconds. Defaults to `3000`. */
|
|
27
35
|
duration?: number
|
|
28
36
|
}
|
|
@@ -109,7 +117,9 @@ function ToastNotification({ item, onDismiss }: { item: ToastItem; onDismiss: ()
|
|
|
109
117
|
<Entypo name="info-with-circle" size={22} color={textColor} />
|
|
110
118
|
)
|
|
111
119
|
|
|
112
|
-
const leftIcon = item.
|
|
120
|
+
const leftIcon: React.ReactNode = item.iconName
|
|
121
|
+
? renderIcon(item.iconName, 22, item.iconColor ?? textColor)
|
|
122
|
+
: item.icon ?? defaultIcon
|
|
113
123
|
|
|
114
124
|
return (
|
|
115
125
|
<GestureDetector gesture={panGesture}>
|
|
@@ -4,6 +4,7 @@ import { FontAwesome5 } from '@expo/vector-icons'
|
|
|
4
4
|
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
5
5
|
import { useTheme } from '../../theme'
|
|
6
6
|
import { s, vs, ms } from '../../utils/scaling'
|
|
7
|
+
import { renderIcon } from '../../utils/icons'
|
|
7
8
|
|
|
8
9
|
export type ToggleVariant = 'default' | 'outline'
|
|
9
10
|
export type ToggleSize = 'sm' | 'md' | 'lg'
|
|
@@ -18,6 +19,20 @@ export interface ToggleProps extends TouchableOpacityProps {
|
|
|
18
19
|
icon?: React.ReactNode | ((pressed: boolean) => React.ReactNode)
|
|
19
20
|
/** Icon to show when pressed/active. If omitted, a default check mark is used. */
|
|
20
21
|
activeIcon?: React.ReactNode | ((pressed: boolean) => React.ReactNode)
|
|
22
|
+
/**
|
|
23
|
+
* Icon name from `@expo/vector-icons` shown when not pressed.
|
|
24
|
+
* See https://icons.expo.fyi. Takes precedence over `icon`.
|
|
25
|
+
*/
|
|
26
|
+
iconName?: string
|
|
27
|
+
/**
|
|
28
|
+
* Icon name from `@expo/vector-icons` shown when pressed/active.
|
|
29
|
+
* See https://icons.expo.fyi. Takes precedence over `activeIcon`.
|
|
30
|
+
*/
|
|
31
|
+
activeIconName?: string
|
|
32
|
+
/** Override the resolved inactive icon color. Defaults to `mutedForeground`. */
|
|
33
|
+
iconColor?: string
|
|
34
|
+
/** Override the resolved active icon color. Defaults to `primary`. */
|
|
35
|
+
activeIconColor?: string
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
const sizeStyles: Record<ToggleSize, ViewStyle> = {
|
|
@@ -26,6 +41,8 @@ const sizeStyles: Record<ToggleSize, ViewStyle> = {
|
|
|
26
41
|
lg: { paddingHorizontal: s(20), paddingVertical: vs(14), minWidth: s(48), minHeight: vs(48) },
|
|
27
42
|
}
|
|
28
43
|
|
|
44
|
+
const iconSizeMap: Record<ToggleSize, number> = { sm: 16, md: 18, lg: 20 }
|
|
45
|
+
|
|
29
46
|
export function Toggle({
|
|
30
47
|
pressed = false,
|
|
31
48
|
onPressedChange,
|
|
@@ -34,6 +51,10 @@ export function Toggle({
|
|
|
34
51
|
label,
|
|
35
52
|
icon,
|
|
36
53
|
activeIcon,
|
|
54
|
+
iconName,
|
|
55
|
+
activeIconName,
|
|
56
|
+
iconColor,
|
|
57
|
+
activeIconColor,
|
|
37
58
|
disabled,
|
|
38
59
|
style,
|
|
39
60
|
...props
|
|
@@ -78,6 +99,8 @@ export function Toggle({
|
|
|
78
99
|
outputRange: [colors.foreground, colors.primary],
|
|
79
100
|
})
|
|
80
101
|
|
|
102
|
+
const iconSize = iconSizeMap[size]
|
|
103
|
+
|
|
81
104
|
const LeftIcon = () => {
|
|
82
105
|
const renderProp = (prop?: any) => {
|
|
83
106
|
if (!prop) return null
|
|
@@ -86,16 +109,18 @@ export function Toggle({
|
|
|
86
109
|
}
|
|
87
110
|
|
|
88
111
|
if (pressed) {
|
|
112
|
+
if (activeIconName) return <>{renderIcon(activeIconName, iconSize, activeIconColor ?? colors.primary)}</>
|
|
89
113
|
const active = renderProp(activeIcon)
|
|
90
114
|
if (active) return <>{active}</>
|
|
91
|
-
return <FontAwesome5 name="check-circle" size={
|
|
115
|
+
return <FontAwesome5 name="check-circle" size={iconSize} color={colors.primary} />
|
|
92
116
|
}
|
|
93
117
|
|
|
118
|
+
if (iconName) return <>{renderIcon(iconName, iconSize, iconColor ?? colors.mutedForeground)}</>
|
|
94
119
|
const custom = renderProp(icon)
|
|
95
120
|
if (custom) return <>{custom}</>
|
|
96
121
|
|
|
97
122
|
// Default: empty circle to signal an action is available
|
|
98
|
-
return <FontAwesome5 name="circle" size={
|
|
123
|
+
return <FontAwesome5 name="circle" size={iconSize} color={colors.mutedForeground} />
|
|
99
124
|
}
|
|
100
125
|
|
|
101
126
|
return (
|
package/src/index.ts
CHANGED
|
@@ -36,3 +36,24 @@ export * from './components/Chip'
|
|
|
36
36
|
export * from './components/ConfirmDialog'
|
|
37
37
|
export * from './components/LabelValue'
|
|
38
38
|
export * from './components/MonthPicker'
|
|
39
|
+
|
|
40
|
+
// Icon utility
|
|
41
|
+
export { Icon, renderIcon } from './utils/icons'
|
|
42
|
+
export type { IconProps, IconFamily } from './utils/icons'
|
|
43
|
+
|
|
44
|
+
// Design tokens
|
|
45
|
+
export {
|
|
46
|
+
SPACING,
|
|
47
|
+
ICON_SIZES,
|
|
48
|
+
RADIUS,
|
|
49
|
+
SHADOWS,
|
|
50
|
+
BREAKPOINTS,
|
|
51
|
+
} from './tokens'
|
|
52
|
+
export type {
|
|
53
|
+
Spacing,
|
|
54
|
+
SpacingKey,
|
|
55
|
+
IconSize,
|
|
56
|
+
IconSizeKey,
|
|
57
|
+
Radius,
|
|
58
|
+
RadiusKey,
|
|
59
|
+
} 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
|
}
|