@retray-dev/ui-kit 2.5.1 → 2.6.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 +153 -6
- package/dist/index.d.mts +98 -8
- package/dist/index.d.ts +98 -8
- package/dist/index.js +591 -505
- package/dist/index.mjs +533 -436
- package/package.json +23 -21
- package/src/components/Accordion/Accordion.tsx +61 -57
- package/src/components/Alert/Alert.tsx +11 -10
- package/src/components/AlertBanner/AlertBanner.tsx +23 -10
- package/src/components/Avatar/Avatar.tsx +9 -8
- package/src/components/Badge/Badge.tsx +27 -12
- package/src/components/Button/Button.tsx +30 -12
- package/src/components/Card/Card.tsx +12 -11
- package/src/components/Checkbox/Checkbox.tsx +16 -13
- package/src/components/Chip/Chip.tsx +8 -7
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +12 -11
- package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +2 -1
- package/src/components/CurrencyInput/CurrencyInput.tsx +2 -1
- package/src/components/EmptyState/EmptyState.tsx +34 -21
- package/src/components/Input/Input.tsx +44 -22
- package/src/components/LabelValue/LabelValue.tsx +6 -5
- package/src/components/ListItem/ListItem.tsx +46 -22
- package/src/components/MonthPicker/MonthPicker.tsx +9 -8
- package/src/components/Progress/Progress.tsx +2 -1
- package/src/components/RadioGroup/RadioGroup.tsx +18 -15
- package/src/components/Select/Select.tsx +25 -24
- package/src/components/Sheet/Sheet.tsx +15 -14
- package/src/components/Slider/Slider.tsx +7 -6
- package/src/components/Switch/Switch.tsx +7 -6
- package/src/components/Tabs/Tabs.tsx +17 -14
- package/src/components/Text/Text.tsx +7 -6
- package/src/components/Textarea/Textarea.tsx +9 -8
- package/src/components/Toast/Toast.tsx +30 -19
- package/src/components/Toggle/Toggle.tsx +36 -10
- package/src/index.ts +4 -0
- package/src/utils/haptics.ts +32 -0
- package/src/utils/icons.ts +73 -0
- package/src/utils/scaling.ts +26 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React, { useRef, useEffect } from 'react'
|
|
2
2
|
import { TouchableOpacity, Animated, StyleSheet, TouchableOpacityProps, ViewStyle, View, Easing } from 'react-native'
|
|
3
3
|
import { FontAwesome5 } from '@expo/vector-icons'
|
|
4
|
-
import
|
|
4
|
+
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
5
5
|
import { useTheme } from '../../theme'
|
|
6
|
+
import { s, vs, ms } from '../../utils/scaling'
|
|
7
|
+
import { renderIcon } from '../../utils/icons'
|
|
6
8
|
|
|
7
9
|
export type ToggleVariant = 'default' | 'outline'
|
|
8
10
|
export type ToggleSize = 'sm' | 'md' | 'lg'
|
|
@@ -17,14 +19,30 @@ export interface ToggleProps extends TouchableOpacityProps {
|
|
|
17
19
|
icon?: React.ReactNode | ((pressed: boolean) => React.ReactNode)
|
|
18
20
|
/** Icon to show when pressed/active. If omitted, a default check mark is used. */
|
|
19
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
|
|
20
36
|
}
|
|
21
37
|
|
|
22
38
|
const sizeStyles: Record<ToggleSize, ViewStyle> = {
|
|
23
|
-
sm: { paddingHorizontal: 12, paddingVertical: 8, minWidth: 40, minHeight: 40 },
|
|
24
|
-
md: { paddingHorizontal: 16, paddingVertical: 12, minWidth: 44, minHeight: 44 },
|
|
25
|
-
lg: { paddingHorizontal: 20, paddingVertical: 14, minWidth: 48, minHeight: 48 },
|
|
39
|
+
sm: { paddingHorizontal: s(12), paddingVertical: vs(8), minWidth: s(40), minHeight: vs(40) },
|
|
40
|
+
md: { paddingHorizontal: s(16), paddingVertical: vs(12), minWidth: s(44), minHeight: vs(44) },
|
|
41
|
+
lg: { paddingHorizontal: s(20), paddingVertical: vs(14), minWidth: s(48), minHeight: vs(48) },
|
|
26
42
|
}
|
|
27
43
|
|
|
44
|
+
const iconSizeMap: Record<ToggleSize, number> = { sm: 16, md: 18, lg: 20 }
|
|
45
|
+
|
|
28
46
|
export function Toggle({
|
|
29
47
|
pressed = false,
|
|
30
48
|
onPressedChange,
|
|
@@ -33,6 +51,10 @@ export function Toggle({
|
|
|
33
51
|
label,
|
|
34
52
|
icon,
|
|
35
53
|
activeIcon,
|
|
54
|
+
iconName,
|
|
55
|
+
activeIconName,
|
|
56
|
+
iconColor,
|
|
57
|
+
activeIconColor,
|
|
36
58
|
disabled,
|
|
37
59
|
style,
|
|
38
60
|
...props
|
|
@@ -77,6 +99,8 @@ export function Toggle({
|
|
|
77
99
|
outputRange: [colors.foreground, colors.primary],
|
|
78
100
|
})
|
|
79
101
|
|
|
102
|
+
const iconSize = iconSizeMap[size]
|
|
103
|
+
|
|
80
104
|
const LeftIcon = () => {
|
|
81
105
|
const renderProp = (prop?: any) => {
|
|
82
106
|
if (!prop) return null
|
|
@@ -85,23 +109,25 @@ export function Toggle({
|
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
if (pressed) {
|
|
112
|
+
if (activeIconName) return <>{renderIcon(activeIconName, iconSize, activeIconColor ?? colors.primary)}</>
|
|
88
113
|
const active = renderProp(activeIcon)
|
|
89
114
|
if (active) return <>{active}</>
|
|
90
|
-
return <FontAwesome5 name="check-circle" size={
|
|
115
|
+
return <FontAwesome5 name="check-circle" size={iconSize} color={colors.primary} />
|
|
91
116
|
}
|
|
92
117
|
|
|
118
|
+
if (iconName) return <>{renderIcon(iconName, iconSize, iconColor ?? colors.mutedForeground)}</>
|
|
93
119
|
const custom = renderProp(icon)
|
|
94
120
|
if (custom) return <>{custom}</>
|
|
95
121
|
|
|
96
122
|
// Default: empty circle to signal an action is available
|
|
97
|
-
return <FontAwesome5 name="circle" size={
|
|
123
|
+
return <FontAwesome5 name="circle" size={iconSize} color={colors.mutedForeground} />
|
|
98
124
|
}
|
|
99
125
|
|
|
100
126
|
return (
|
|
101
127
|
<Animated.View style={[{ transform: [{ scale }] }, disabled && styles.disabled, style]}>
|
|
102
128
|
<TouchableOpacity
|
|
103
129
|
onPress={() => {
|
|
104
|
-
|
|
130
|
+
hapticSelection()
|
|
105
131
|
onPressedChange?.(!pressed)
|
|
106
132
|
}}
|
|
107
133
|
onPressIn={handlePressIn}
|
|
@@ -130,19 +156,19 @@ export function Toggle({
|
|
|
130
156
|
|
|
131
157
|
const styles = StyleSheet.create({
|
|
132
158
|
base: {
|
|
133
|
-
borderRadius: 8,
|
|
159
|
+
borderRadius: ms(8),
|
|
134
160
|
},
|
|
135
161
|
inner: {
|
|
136
162
|
flexDirection: 'row',
|
|
137
163
|
alignItems: 'center',
|
|
138
164
|
justifyContent: 'center',
|
|
139
|
-
gap: 8,
|
|
165
|
+
gap: s(8),
|
|
140
166
|
},
|
|
141
167
|
disabled: {
|
|
142
168
|
opacity: 0.45,
|
|
143
169
|
},
|
|
144
170
|
label: {
|
|
145
|
-
fontSize: 14,
|
|
171
|
+
fontSize: ms(14),
|
|
146
172
|
fontWeight: '500',
|
|
147
173
|
},
|
|
148
174
|
})
|
package/src/index.ts
CHANGED
|
@@ -36,3 +36,7 @@ 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'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Platform } from 'react-native'
|
|
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
|
+
*/
|
|
7
|
+
|
|
8
|
+
let Haptics: typeof import('expo-haptics') | null = null
|
|
9
|
+
|
|
10
|
+
if (Platform.OS !== 'web') {
|
|
11
|
+
Haptics = require('expo-haptics')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function selectionAsync(): void {
|
|
15
|
+
Haptics?.selectionAsync()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function impactLight(): void {
|
|
19
|
+
Haptics?.impactAsync(Haptics.ImpactFeedbackStyle.Light)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function impactMedium(): void {
|
|
23
|
+
Haptics?.impactAsync(Haptics.ImpactFeedbackStyle.Medium)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function notificationSuccess(): void {
|
|
27
|
+
Haptics?.notificationAsync(Haptics.NotificationFeedbackType.Success)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function notificationError(): void {
|
|
31
|
+
Haptics?.notificationAsync(Haptics.NotificationFeedbackType.Error)
|
|
32
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import AntDesign from '@expo/vector-icons/AntDesign'
|
|
3
|
+
import Entypo from '@expo/vector-icons/Entypo'
|
|
4
|
+
import Feather from '@expo/vector-icons/Feather'
|
|
5
|
+
import FontAwesome5 from '@expo/vector-icons/FontAwesome5'
|
|
6
|
+
import MaterialIcons from '@expo/vector-icons/MaterialIcons'
|
|
7
|
+
import Ionicons from '@expo/vector-icons/Ionicons'
|
|
8
|
+
|
|
9
|
+
export type IconFamily = 'Feather' | 'AntDesign' | 'Entypo' | 'FontAwesome5' | 'MaterialIcons' | 'Ionicons'
|
|
10
|
+
|
|
11
|
+
export interface IconProps {
|
|
12
|
+
/** Icon name from any supported @expo/vector-icons family. See https://icons.expo.fyi */
|
|
13
|
+
name: string
|
|
14
|
+
size: number
|
|
15
|
+
color: string
|
|
16
|
+
/** Override the resolved family when the same name exists in multiple families. */
|
|
17
|
+
family?: IconFamily
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type ResolvedFamily = {
|
|
21
|
+
name: IconFamily
|
|
22
|
+
component: React.ComponentType<{ name: any; size: number; color: string }>
|
|
23
|
+
glyphMap: Record<string, number>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Priority order: highest-priority family listed last so it overwrites lower-priority entries in the cache
|
|
27
|
+
const ICON_FAMILIES: ResolvedFamily[] = [
|
|
28
|
+
{ name: 'Ionicons', component: Ionicons as any, glyphMap: (Ionicons as any).glyphMap },
|
|
29
|
+
{ name: 'MaterialIcons', component: MaterialIcons as any, glyphMap: (MaterialIcons as any).glyphMap },
|
|
30
|
+
{ name: 'FontAwesome5', component: FontAwesome5 as any, glyphMap: (FontAwesome5 as any).glyphMap },
|
|
31
|
+
{ name: 'Entypo', component: Entypo as any, glyphMap: (Entypo as any).glyphMap },
|
|
32
|
+
{ name: 'AntDesign', component: AntDesign as any, glyphMap: (AntDesign as any).glyphMap },
|
|
33
|
+
{ name: 'Feather', component: Feather as any, glyphMap: (Feather as any).glyphMap },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
let resolvedCache: Map<string, ResolvedFamily> | null = null
|
|
37
|
+
|
|
38
|
+
function buildCache(): Map<string, ResolvedFamily> {
|
|
39
|
+
const cache = new Map<string, ResolvedFamily>()
|
|
40
|
+
for (const family of ICON_FAMILIES) {
|
|
41
|
+
if (!family.glyphMap) continue
|
|
42
|
+
for (const iconName of Object.keys(family.glyphMap)) {
|
|
43
|
+
cache.set(iconName, family)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return cache
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveFamily(name: string): ResolvedFamily | null {
|
|
50
|
+
if (!resolvedCache) {
|
|
51
|
+
resolvedCache = buildCache()
|
|
52
|
+
}
|
|
53
|
+
return resolvedCache.get(name) ?? null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function Icon({ name, size, color, family }: IconProps): React.ReactElement | null {
|
|
57
|
+
let resolved: ResolvedFamily | null = null
|
|
58
|
+
|
|
59
|
+
if (family) {
|
|
60
|
+
resolved = ICON_FAMILIES.find((f) => f.name === family) ?? null
|
|
61
|
+
} else {
|
|
62
|
+
resolved = resolveFamily(name)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!resolved) return null
|
|
66
|
+
|
|
67
|
+
const Component = resolved.component
|
|
68
|
+
return React.createElement(Component, { name, size, color })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function renderIcon(name: string, size: number, color: string): React.ReactElement | null {
|
|
72
|
+
return React.createElement(Icon, { name, size, color })
|
|
73
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Platform } from 'react-native'
|
|
2
|
+
import {
|
|
3
|
+
scale,
|
|
4
|
+
verticalScale,
|
|
5
|
+
moderateScale,
|
|
6
|
+
moderateVerticalScale,
|
|
7
|
+
} from 'react-native-size-matters'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scaling utilities wrapping react-native-size-matters.
|
|
11
|
+
*
|
|
12
|
+
* On native: scales relative to guideline base 350×680 (~5" mobile).
|
|
13
|
+
* On web: identity functions — no scaling, values used as-is (px in web = pt on a standard display).
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* s(n) — scale horizontal values (paddingHorizontal, width, gap)
|
|
17
|
+
* vs(n) — scale vertical values (paddingVertical, height, minHeight)
|
|
18
|
+
* ms(n) — moderate scale (fontSize, borderRadius)
|
|
19
|
+
* mvs(n) — moderate vertical scale (lineHeight)
|
|
20
|
+
*/
|
|
21
|
+
const isWeb = Platform.OS === 'web'
|
|
22
|
+
|
|
23
|
+
export const s = isWeb ? (n: number) => n : scale
|
|
24
|
+
export const vs = isWeb ? (n: number) => n : verticalScale
|
|
25
|
+
export const ms = isWeb ? (n: number, _factor?: number) => n : moderateScale
|
|
26
|
+
export const mvs = isWeb ? (n: number, _factor?: number) => n : moderateVerticalScale
|