@retray-dev/ui-kit 2.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retray-dev/ui-kit",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Personal UI Kit for React Native / Expo",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -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
  )}
@@ -64,6 +64,7 @@ export function Checkbox({
64
64
  {label ? (
65
65
  <Text
66
66
  style={[styles.label, { color: disabled ? colors.mutedForeground : colors.foreground }]}
67
+ allowFontScaling={true}
67
68
  >
68
69
  {label}
69
70
  </Text>
@@ -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
+ })
@@ -0,0 +1,2 @@
1
+ export { IconButton } from './IconButton'
2
+ export type { IconButtonProps, IconButtonVariant, IconButtonSize } from './IconButton'
@@ -73,6 +73,7 @@ function RadioItem({
73
73
  styles.label,
74
74
  { color: option.disabled ? colors.mutedForeground : colors.foreground },
75
75
  ]}
76
+ allowFontScaling={true}
76
77
  >
77
78
  {option.label}
78
79
  </Text>
@@ -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}
@@ -76,6 +76,7 @@ function TabTrigger({
76
76
  { color: isActive ? colors.foreground : colors.mutedForeground },
77
77
  isActive && styles.activeTriggerLabel,
78
78
  ]}
79
+ allowFontScaling={true}
79
80
  >
80
81
  {tab.label}
81
82
  </Text>
@@ -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'
@@ -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
- })