@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retray-dev/ui-kit",
3
- "version": "2.5.2",
3
+ "version": "2.7.0",
4
4
  "description": "Personal UI Kit for React Native / Expo",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -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}>{icon ?? defaultIcon}</View>
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
- export function Badge({ label, children, variant = 'default', size = 'md', icon, style }: BadgeProps) {
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
- {icon ? <View style={styles.iconContainer}>{icon}</View> : null}
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
- {icon && iconPosition === 'left' && <>{typeof icon === 'function' ? icon({ label, size, variant }) : icon}</>}
134
- <Text style={[styles.label, labelVariantStyle, labelSizeStyles[size], icon ? styles.labelWithIcon : undefined]}>{label}</Text>
135
- {icon && iconPosition === 'right' && <>{typeof icon === 'function' ? icon({ label, size, variant }) : icon}</>}
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
- {icon ? (
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
- {icon}
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
- // If type is password and no suffix is provided, add the toggle button
41
- const effectiveSuffix = isPassword && !suffix ? (
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
- ) : suffix
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
- {prefix ? (
64
- typeof prefix === 'string' ? (
84
+ {effectivePrefix ? (
85
+ typeof effectivePrefix === 'string' ? (
65
86
  <Text style={[styles.prefixText, { color: colors.mutedForeground }, prefixStyle]} allowFontScaling={true}>
66
- {prefix}
87
+ {effectivePrefix}
67
88
  </Text>
68
89
  ) : (
69
- <View style={styles.prefixContainer}>{prefix}</View>
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
- // Support legacy props
113
- const effectiveLeft = leftRender ?? icon
114
- const effectiveRight = rightRender ?? trailing
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.icon ?? defaultIcon
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={20} color={colors.primary} />
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={20} color={colors.mutedForeground} />
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
@@ -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 Haptics: typeof import('expo-haptics') | null = null
5
+ let _haptics: HapticsModule | null = null
9
6
 
10
- if (Platform.OS !== 'web') {
11
- Haptics = require('expo-haptics')
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
- Haptics?.selectionAsync()
16
+ if (Platform.OS === 'web') return
17
+ getHaptics().then(h => h?.selectionAsync())
16
18
  }
17
19
 
18
20
  export function impactLight(): void {
19
- Haptics?.impactAsync(Haptics.ImpactFeedbackStyle.Light)
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
- Haptics?.impactAsync(Haptics.ImpactFeedbackStyle.Medium)
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
- Haptics?.notificationAsync(Haptics.NotificationFeedbackType.Success)
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
- Haptics?.notificationAsync(Haptics.NotificationFeedbackType.Error)
36
+ if (Platform.OS === 'web') return
37
+ getHaptics().then(h => h?.notificationAsync(h.NotificationFeedbackType.Error))
32
38
  }