@retray-dev/ui-kit 4.0.0 → 5.1.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.
Files changed (50) hide show
  1. package/COMPONENTS.md +1791 -663
  2. package/README.md +4 -3
  3. package/dist/index.d.mts +268 -83
  4. package/dist/index.d.ts +268 -83
  5. package/dist/index.js +1032 -309
  6. package/dist/index.mjs +1029 -311
  7. package/package.json +3 -2
  8. package/src/components/Accordion/Accordion.tsx +1 -1
  9. package/src/components/AlertBanner/AlertBanner.tsx +50 -45
  10. package/src/components/Avatar/Avatar.tsx +61 -17
  11. package/src/components/Badge/Badge.tsx +17 -15
  12. package/src/components/Button/Button.tsx +31 -42
  13. package/src/components/Card/Card.tsx +4 -4
  14. package/src/components/CategoryStrip/CategoryStrip.tsx +185 -0
  15. package/src/components/CategoryStrip/index.ts +2 -0
  16. package/src/components/Checkbox/Checkbox.tsx +44 -16
  17. package/src/components/Chip/Chip.tsx +1 -1
  18. package/src/components/ConfirmDialog/ConfirmDialog.tsx +3 -3
  19. package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +1 -0
  20. package/src/components/CurrencyInput/CurrencyInput.tsx +6 -4
  21. package/src/components/EmptyState/EmptyState.tsx +9 -9
  22. package/src/components/IconButton/IconButton.tsx +74 -34
  23. package/src/components/Input/Input.tsx +15 -13
  24. package/src/components/LabelValue/LabelValue.tsx +1 -1
  25. package/src/components/ListItem/ListItem.tsx +5 -5
  26. package/src/components/MediaCard/MediaCard.tsx +249 -0
  27. package/src/components/MediaCard/index.ts +2 -0
  28. package/src/components/Pressable/Pressable.tsx +100 -0
  29. package/src/components/Pressable/index.ts +1 -0
  30. package/src/components/Progress/Progress.tsx +14 -7
  31. package/src/components/RadioGroup/RadioGroup.tsx +1 -1
  32. package/src/components/Select/Select.tsx +5 -5
  33. package/src/components/Sheet/Sheet.tsx +2 -2
  34. package/src/components/Skeleton/Skeleton.tsx +34 -7
  35. package/src/components/Slider/Slider.tsx +2 -2
  36. package/src/components/Spinner/Spinner.tsx +1 -1
  37. package/src/components/Switch/Switch.tsx +31 -4
  38. package/src/components/Tabs/Tabs.tsx +63 -45
  39. package/src/components/Text/Text.tsx +59 -10
  40. package/src/components/Textarea/Textarea.tsx +4 -3
  41. package/src/components/Toast/Toast.tsx +77 -36
  42. package/src/components/Toggle/Toggle.tsx +3 -3
  43. package/src/index.ts +8 -2
  44. package/src/theme/ThemeProvider.tsx +11 -10
  45. package/src/theme/colorUtils.ts +80 -0
  46. package/src/theme/colors.ts +76 -35
  47. package/src/theme/index.ts +2 -2
  48. package/src/theme/types.ts +27 -13
  49. package/src/tokens.ts +150 -13
  50. package/src/utils/hover.ts +25 -0
@@ -133,7 +133,7 @@ export function ListItem({
133
133
  : leftRender ?? icon
134
134
 
135
135
  const effectiveRight: React.ReactNode | string | undefined = rightIcon
136
- ? renderIcon(rightIcon, 24, rightIconColor ?? colors.mutedForeground)
136
+ ? renderIcon(rightIcon, 24, rightIconColor ?? colors.foregroundMuted)
137
137
  : rightRender ?? trailing
138
138
 
139
139
  const cardStyle: ViewStyle =
@@ -176,7 +176,7 @@ export function ListItem({
176
176
  </Text>
177
177
  {subtitle ? (
178
178
  <Text
179
- style={[styles.subtitle, { color: colors.mutedForeground }, subtitleStyle]}
179
+ style={[styles.subtitle, { color: colors.foregroundMuted }, subtitleStyle]}
180
180
  numberOfLines={2}
181
181
  allowFontScaling={true}
182
182
  >
@@ -185,7 +185,7 @@ export function ListItem({
185
185
  ) : null}
186
186
  {caption ? (
187
187
  <Text
188
- style={[styles.caption, { color: colors.mutedForeground }, captionStyle]}
188
+ style={[styles.caption, { color: colors.foregroundMuted }, captionStyle]}
189
189
  numberOfLines={1}
190
190
  allowFontScaling={true}
191
191
  >
@@ -198,7 +198,7 @@ export function ListItem({
198
198
  <View style={styles.rightContainer}>
199
199
  {typeof effectiveRight === 'string' ? (
200
200
  <Text
201
- style={[styles.rightText, { color: colors.mutedForeground }]}
201
+ style={[styles.rightText, { color: colors.foregroundMuted }]}
202
202
  allowFontScaling={true}
203
203
  >
204
204
  {effectiveRight}
@@ -208,7 +208,7 @@ export function ListItem({
208
208
  )}
209
209
  </View>
210
210
  ) : showChevron ? (
211
- <Entypo name="chevron-with-circle-right" size={20} color={colors.mutedForeground} />
211
+ <Entypo name="chevron-with-circle-right" size={20} color={colors.foregroundMuted} />
212
212
  ) : null}
213
213
  </TouchableOpacity>
214
214
 
@@ -0,0 +1,249 @@
1
+ import React, { useRef, useState } from 'react'
2
+ import {
3
+ View,
4
+ Image,
5
+ Text,
6
+ TouchableOpacity,
7
+ Animated,
8
+ StyleSheet,
9
+ ViewStyle,
10
+ ImageSourcePropType,
11
+ Platform,
12
+ } from 'react-native'
13
+ import { impactLight } from '../../utils/haptics'
14
+ import { useTheme } from '../../theme'
15
+ import { s, vs, ms, mvs } from '../../utils/scaling'
16
+ import { renderIcon } from '../../utils/icons'
17
+ import { useHover } from '../../utils/hover'
18
+ import { RADIUS, SHADOWS } from '../../tokens'
19
+
20
+ const nativeDriver = Platform.OS !== 'web'
21
+
22
+ export type MediaCardAspectRatio = '1:1' | '4:3' | '16:9' | '4:5' | '3:2'
23
+
24
+ const aspectRatioMap: Record<MediaCardAspectRatio, number> = {
25
+ '1:1': 1,
26
+ '4:3': 3 / 4,
27
+ '16:9': 9 / 16,
28
+ '4:5': 5 / 4,
29
+ '3:2': 2 / 3,
30
+ }
31
+
32
+ export interface MediaCardProps {
33
+ /** Image source — URI string or require(). */
34
+ imageSource?: ImageSourcePropType
35
+ /** Image aspect ratio. Defaults to `'4:3'`. */
36
+ aspectRatio?: MediaCardAspectRatio
37
+ /** Badge content rendered top-left over the image (e.g. a Badge component or Text). */
38
+ badge?: React.ReactNode
39
+ /** Icon rendered in a circle button top-right over the image. Defaults to `'heart'`. */
40
+ actionIcon?: React.ReactNode
41
+ /** Icon name for the action button. Overrides `actionIcon`. */
42
+ actionIconName?: string
43
+ /** Whether the action icon is in active/filled state. */
44
+ actionActive?: boolean
45
+ /** Called when the top-right action icon is pressed. */
46
+ onActionPress?: () => void
47
+ /** Primary text below the image. */
48
+ title?: string
49
+ /** Secondary text below the title. */
50
+ subtitle?: string
51
+ /** Tertiary / caption text below subtitle. */
52
+ caption?: string
53
+ /** Called when the card body is pressed. */
54
+ onPress?: () => void
55
+ style?: ViewStyle
56
+ /** Style for the image container. */
57
+ imageStyle?: ViewStyle
58
+ /** Additional content rendered below caption. */
59
+ footer?: React.ReactNode
60
+ }
61
+
62
+ export function MediaCard({
63
+ imageSource,
64
+ aspectRatio = '4:3',
65
+ badge,
66
+ actionIcon,
67
+ actionIconName,
68
+ actionActive = false,
69
+ onActionPress,
70
+ title,
71
+ subtitle,
72
+ caption,
73
+ onPress,
74
+ style,
75
+ imageStyle,
76
+ footer,
77
+ }: MediaCardProps) {
78
+ const { colors } = useTheme()
79
+ const scale = useRef(new Animated.Value(1)).current
80
+ const { hovered, hoverHandlers } = useHover()
81
+
82
+ const handlePressIn = () => {
83
+ if (!onPress) return
84
+ Animated.spring(scale, { toValue: 0.98, useNativeDriver: nativeDriver, speed: 40, bounciness: 0 }).start()
85
+ }
86
+
87
+ const handlePressOut = () => {
88
+ if (!onPress) return
89
+ Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, speed: 40, bounciness: 4 }).start()
90
+ }
91
+
92
+ const handlePress = () => {
93
+ if (!onPress) return
94
+ impactLight()
95
+ onPress()
96
+ }
97
+
98
+ const ratio = aspectRatioMap[aspectRatio]
99
+
100
+ // Action icon: active = primary fill, inactive = foreground outline
101
+ const resolvedActionIcon = actionIconName
102
+ ? renderIcon(actionIconName, 18, actionActive ? colors.primary : colors.background)
103
+ : actionIcon ?? renderIcon('heart', 18, actionActive ? colors.primary : colors.background)
104
+
105
+ const cardContent = (
106
+ <View
107
+ style={[
108
+ styles.card,
109
+ hovered && styles.cardHovered,
110
+ style,
111
+ ]}
112
+ {...(Platform.OS === 'web' ? hoverHandlers : {})}
113
+ >
114
+ {/* Image area */}
115
+ <View style={[styles.imageContainer, imageStyle]}>
116
+ <View style={{ paddingTop: `${ratio * 100}%` as any }}>
117
+ <View style={StyleSheet.absoluteFill}>
118
+ {imageSource ? (
119
+ <Image
120
+ source={imageSource}
121
+ style={styles.image}
122
+ resizeMode="cover"
123
+ />
124
+ ) : (
125
+ <View style={[styles.imagePlaceholder, { backgroundColor: colors.surface }]} />
126
+ )}
127
+ </View>
128
+ </View>
129
+
130
+ {/* Badge — top left */}
131
+ {badge && (
132
+ <View style={styles.badgeContainer}>
133
+ {badge}
134
+ </View>
135
+ )}
136
+
137
+ {/* Action icon — top right */}
138
+ {(onActionPress || actionIcon || actionIconName) && (
139
+ <TouchableOpacity
140
+ style={[styles.actionButton, { backgroundColor: 'rgba(0,0,0,0.24)' }]}
141
+ onPress={() => { impactLight(); onActionPress?.() }}
142
+ activeOpacity={0.8}
143
+ touchSoundDisabled={true}
144
+ >
145
+ {resolvedActionIcon}
146
+ </TouchableOpacity>
147
+ )}
148
+ </View>
149
+
150
+ {/* Metadata */}
151
+ {(title || subtitle || caption || footer) && (
152
+ <View style={styles.meta}>
153
+ {title ? (
154
+ <Text style={[styles.title, { color: colors.foreground }]} numberOfLines={2} allowFontScaling={true}>
155
+ {title}
156
+ </Text>
157
+ ) : null}
158
+ {subtitle ? (
159
+ <Text style={[styles.subtitle, { color: colors.foregroundSubtle }]} numberOfLines={1} allowFontScaling={true}>
160
+ {subtitle}
161
+ </Text>
162
+ ) : null}
163
+ {caption ? (
164
+ <Text style={[styles.caption, { color: colors.foregroundMuted }]} numberOfLines={1} allowFontScaling={true}>
165
+ {caption}
166
+ </Text>
167
+ ) : null}
168
+ {footer}
169
+ </View>
170
+ )}
171
+ </View>
172
+ )
173
+
174
+ if (onPress) {
175
+ return (
176
+ <Animated.View style={{ transform: [{ scale }] }}>
177
+ <TouchableOpacity
178
+ onPress={handlePress}
179
+ onPressIn={handlePressIn}
180
+ onPressOut={handlePressOut}
181
+ activeOpacity={1}
182
+ touchSoundDisabled={true}
183
+ >
184
+ {cardContent}
185
+ </TouchableOpacity>
186
+ </Animated.View>
187
+ )
188
+ }
189
+
190
+ return cardContent
191
+ }
192
+
193
+ const styles = StyleSheet.create({
194
+ card: {
195
+ borderRadius: RADIUS.md, // 14px — Airbnb property card spec
196
+ overflow: 'hidden',
197
+ backgroundColor: 'transparent',
198
+ },
199
+ cardHovered: {
200
+ // Web hover: lift shadow
201
+ ...SHADOWS.md,
202
+ },
203
+ imageContainer: {
204
+ borderRadius: RADIUS.md,
205
+ overflow: 'hidden',
206
+ },
207
+ image: {
208
+ width: '100%',
209
+ height: '100%',
210
+ },
211
+ imagePlaceholder: {
212
+ width: '100%',
213
+ height: '100%',
214
+ },
215
+ badgeContainer: {
216
+ position: 'absolute',
217
+ top: s(8),
218
+ left: s(8),
219
+ },
220
+ actionButton: {
221
+ position: 'absolute',
222
+ top: s(8),
223
+ right: s(8),
224
+ width: s(32),
225
+ height: s(32),
226
+ borderRadius: 9999,
227
+ alignItems: 'center',
228
+ justifyContent: 'center',
229
+ },
230
+ meta: {
231
+ paddingTop: vs(8),
232
+ gap: vs(2),
233
+ },
234
+ title: {
235
+ fontFamily: 'Poppins-SemiBold',
236
+ fontSize: ms(14),
237
+ lineHeight: mvs(20),
238
+ },
239
+ subtitle: {
240
+ fontFamily: 'Poppins-Regular',
241
+ fontSize: ms(13),
242
+ lineHeight: mvs(18),
243
+ },
244
+ caption: {
245
+ fontFamily: 'Poppins-Regular',
246
+ fontSize: ms(12),
247
+ lineHeight: mvs(16),
248
+ },
249
+ })
@@ -0,0 +1,2 @@
1
+ export { MediaCard } from './MediaCard'
2
+ export type { MediaCardProps, MediaCardAspectRatio } from './MediaCard'
@@ -0,0 +1,100 @@
1
+ import React, { useRef } from 'react'
2
+ import {
3
+ TouchableOpacity,
4
+ Animated,
5
+ ViewStyle,
6
+ Platform,
7
+ TouchableOpacityProps,
8
+ } from 'react-native'
9
+ import { impactLight } from '../../utils/haptics'
10
+ import { useHover } from '../../utils/hover'
11
+
12
+ const nativeDriver = Platform.OS !== 'web'
13
+
14
+ export interface PressableProps extends Omit<TouchableOpacityProps, 'activeOpacity'> {
15
+ /** Children content to render inside the pressable. */
16
+ children: React.ReactNode
17
+ /** Called when pressed. */
18
+ onPress?: () => void
19
+ /** Scale value on press. Defaults to `0.98` (MediaCard-style). */
20
+ pressScale?: number
21
+ /** Bounciness of the spring animation on release. Defaults to `4`. */
22
+ bounciness?: number
23
+ /** Enable haptic feedback on press. Defaults to `true`. */
24
+ haptics?: boolean
25
+ /** Additional style for the Animated wrapper. */
26
+ style?: ViewStyle
27
+ /** Disable interaction. */
28
+ disabled?: boolean
29
+ /** Hover scale (web only). Defaults to `1.02`. Set to `1` to disable. */
30
+ hoverScale?: number
31
+ }
32
+
33
+ /**
34
+ * Generic pressable with beautiful spring bounce effect matching MediaCard interaction.
35
+ * Use for custom pressable content that needs consistent press feel.
36
+ */
37
+ export function Pressable({
38
+ children,
39
+ onPress,
40
+ pressScale = 0.98,
41
+ bounciness = 4,
42
+ haptics = true,
43
+ style,
44
+ disabled,
45
+ hoverScale = 1.02,
46
+ ...touchableProps
47
+ }: PressableProps) {
48
+ const scale = useRef(new Animated.Value(1)).current
49
+ const { hovered, hoverHandlers } = useHover()
50
+
51
+ const handlePressIn = () => {
52
+ if (disabled) return
53
+ Animated.spring(scale, {
54
+ toValue: pressScale,
55
+ useNativeDriver: nativeDriver,
56
+ speed: 40,
57
+ bounciness: 0,
58
+ }).start()
59
+ }
60
+
61
+ const handlePressOut = () => {
62
+ if (disabled) return
63
+ Animated.spring(scale, {
64
+ toValue: 1,
65
+ useNativeDriver: nativeDriver,
66
+ speed: 40,
67
+ bounciness,
68
+ }).start()
69
+ }
70
+
71
+ const handlePress = () => {
72
+ if (disabled || !onPress) return
73
+ if (haptics) impactLight()
74
+ onPress()
75
+ }
76
+
77
+ const hoverScaleValue = hovered && hoverScale !== 1 ? hoverScale : 1
78
+
79
+ return (
80
+ <Animated.View
81
+ style={[
82
+ { transform: [{ scale: Animated.multiply(scale, hoverScaleValue) }] },
83
+ style,
84
+ ]}
85
+ {...(Platform.OS === 'web' ? hoverHandlers : {})}
86
+ >
87
+ <TouchableOpacity
88
+ onPress={handlePress}
89
+ onPressIn={handlePressIn}
90
+ onPressOut={handlePressOut}
91
+ activeOpacity={1}
92
+ disabled={disabled}
93
+ touchSoundDisabled={true}
94
+ {...touchableProps}
95
+ >
96
+ {children}
97
+ </TouchableOpacity>
98
+ </Animated.View>
99
+ )
100
+ }
@@ -0,0 +1 @@
1
+ export * from './Pressable'
@@ -3,15 +3,16 @@ import { View, Animated, StyleSheet, ViewStyle } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
4
  import { vs } from '../../utils/scaling'
5
5
 
6
+ export type ProgressVariant = 'default' | 'success' | 'warning' | 'destructive'
7
+
6
8
  export interface ProgressProps {
7
- /** Current progress value. Clamped to `[0, max]`. Defaults to `0`. */
8
9
  value?: number
9
- /** Maximum value. Defaults to `100`. */
10
10
  max?: number
11
+ variant?: ProgressVariant
11
12
  style?: ViewStyle
12
13
  }
13
14
 
14
- export function Progress({ value = 0, max = 100, style }: ProgressProps) {
15
+ export function Progress({ value = 0, max = 100, variant = 'default', style }: ProgressProps) {
15
16
  const { colors } = useTheme()
16
17
  const percent = Math.min(Math.max((value / max) * 100, 0), 100)
17
18
  const [trackWidth, setTrackWidth] = useState(0)
@@ -27,13 +28,19 @@ export function Progress({ value = 0, max = 100, style }: ProgressProps) {
27
28
  }).start()
28
29
  }, [percent, trackWidth])
29
30
 
31
+ const indicatorColor =
32
+ variant === 'success' ? colors.success
33
+ : variant === 'warning' ? colors.warning
34
+ : variant === 'destructive' ? colors.destructive
35
+ : colors.primary
36
+
30
37
  return (
31
38
  <View
32
- style={[styles.track, { backgroundColor: colors.muted }, style]}
39
+ style={[styles.track, { backgroundColor: colors.surface }, style]}
33
40
  onLayout={(e) => setTrackWidth(e.nativeEvent.layout.width)}
34
41
  >
35
42
  <Animated.View
36
- style={[styles.indicator, { width: animatedWidth, backgroundColor: colors.primary }]}
43
+ style={[styles.indicator, { width: animatedWidth, backgroundColor: indicatorColor }]}
37
44
  />
38
45
  </View>
39
46
  )
@@ -42,12 +49,12 @@ export function Progress({ value = 0, max = 100, style }: ProgressProps) {
42
49
  const styles = StyleSheet.create({
43
50
  track: {
44
51
  height: vs(8),
45
- borderRadius: 999,
52
+ borderRadius: 9999,
46
53
  overflow: 'hidden',
47
54
  width: '100%',
48
55
  },
49
56
  indicator: {
50
57
  height: '100%',
51
- borderRadius: 999,
58
+ borderRadius: 9999,
52
59
  },
53
60
  })
@@ -71,7 +71,7 @@ function RadioItem({
71
71
  <Text
72
72
  style={[
73
73
  styles.label,
74
- { color: option.disabled ? colors.mutedForeground : colors.foreground },
74
+ { color: option.disabled ? colors.foregroundMuted : colors.foreground },
75
75
  ]}
76
76
  allowFontScaling={true}
77
77
  >
@@ -102,14 +102,14 @@ export function Select({
102
102
  <Text
103
103
  style={[
104
104
  styles.triggerText,
105
- { color: selected ? colors.foreground : colors.mutedForeground },
105
+ { color: selected ? colors.foreground : colors.foregroundMuted },
106
106
  ]}
107
107
  numberOfLines={1}
108
108
  allowFontScaling={true}
109
109
  >
110
110
  {selected?.label ?? placeholder}
111
111
  </Text>
112
- <Entypo name="chevron-with-circle-down" size={20} color={colors.mutedForeground} />
112
+ <Entypo name="chevron-with-circle-down" size={20} color={colors.foregroundMuted} />
113
113
  </TouchableOpacity>
114
114
  </Animated.View>
115
115
  ) : null}
@@ -144,7 +144,7 @@ export function Select({
144
144
  itemStyle={{ color: colors.foreground }}
145
145
  >
146
146
  {!value ? (
147
- <Picker.Item label={placeholder} value="" color={colors.mutedForeground} enabled={false} />
147
+ <Picker.Item label={placeholder} value="" color={colors.foregroundMuted} enabled={false} />
148
148
  ) : null}
149
149
  {options.map((o) => (
150
150
  <Picker.Item
@@ -152,7 +152,7 @@ export function Select({
152
152
  label={o.label}
153
153
  value={o.value}
154
154
  enabled={!o.disabled}
155
- color={o.disabled ? colors.mutedForeground : colors.foreground}
155
+ color={o.disabled ? colors.foregroundMuted : colors.foreground}
156
156
  />
157
157
  ))}
158
158
  </Picker>
@@ -202,7 +202,7 @@ export function Select({
202
202
  styles.webPicker,
203
203
  {
204
204
  borderColor: error ? colors.destructive : colors.border,
205
- color: selected ? colors.foreground : colors.mutedForeground,
205
+ color: selected ? colors.foreground : colors.foregroundMuted,
206
206
  backgroundColor: colors.background,
207
207
  opacity: disabled ? 0.45 : 1,
208
208
  },
@@ -66,10 +66,10 @@ export function Sheet({
66
66
  {title || description ? (
67
67
  <View style={styles.header}>
68
68
  {title ? (
69
- <Text style={[styles.title, { color: colors.cardForeground }]} allowFontScaling={true}>{title}</Text>
69
+ <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>{title}</Text>
70
70
  ) : null}
71
71
  {description ? (
72
- <Text style={[styles.description, { color: colors.mutedForeground }]} allowFontScaling={true}>
72
+ <Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
73
73
  {description}
74
74
  </Text>
75
75
  ) : null}
@@ -2,15 +2,30 @@ import React, { useEffect, useRef, useState } from 'react'
2
2
  import { Animated, StyleSheet, View, ViewStyle } from 'react-native'
3
3
  import { LinearGradient } from 'expo-linear-gradient'
4
4
  import { useTheme } from '../../theme'
5
+ import { s } from '../../utils/scaling'
6
+
7
+ // circle: circular avatar placeholder text: short line preset base: custom dimensions
8
+ export type SkeletonPreset = 'base' | 'circle' | 'text'
5
9
 
6
10
  export interface SkeletonProps {
7
11
  width?: number | string
8
12
  height?: number
9
13
  borderRadius?: number
14
+ /** Preset shape. `'circle'` forces width=height square with full radius. `'text'` renders a short line. */
15
+ preset?: SkeletonPreset
16
+ /** Only used with `preset='circle'` — overrides the diameter. Defaults to 40. */
17
+ diameter?: number
10
18
  style?: ViewStyle
11
19
  }
12
20
 
13
- export function Skeleton({ width = '100%', height = 16, borderRadius = 6, style }: SkeletonProps) {
21
+ export function Skeleton({
22
+ width = '100%',
23
+ height = 16,
24
+ borderRadius = 6,
25
+ preset = 'base',
26
+ diameter = 40,
27
+ style,
28
+ }: SkeletonProps) {
14
29
  const { colors, colorScheme } = useTheme()
15
30
  const shimmerAnim = useRef(new Animated.Value(0)).current
16
31
  const [containerWidth, setContainerWidth] = useState(300)
@@ -20,11 +35,7 @@ export function Skeleton({ width = '100%', height = 16, borderRadius = 6, style
20
35
 
21
36
  useEffect(() => {
22
37
  const animation = Animated.loop(
23
- Animated.timing(shimmerAnim, {
24
- toValue: 1,
25
- duration: 1200,
26
- useNativeDriver: true,
27
- })
38
+ Animated.timing(shimmerAnim, { toValue: 1, duration: 1200, useNativeDriver: true })
28
39
  )
29
40
  animation.start()
30
41
  return () => animation.stop()
@@ -35,11 +46,27 @@ export function Skeleton({ width = '100%', height = 16, borderRadius = 6, style
35
46
  outputRange: [-containerWidth, containerWidth],
36
47
  })
37
48
 
49
+ // Resolve dimensions by preset
50
+ const resolvedWidth: number | string =
51
+ preset === 'circle' ? s(diameter)
52
+ : preset === 'text' ? '60%'
53
+ : width
54
+
55
+ const resolvedHeight: number =
56
+ preset === 'circle' ? s(diameter)
57
+ : preset === 'text' ? 14
58
+ : height
59
+
60
+ const resolvedRadius: number =
61
+ preset === 'circle' ? 9999
62
+ : preset === 'text' ? 4
63
+ : borderRadius
64
+
38
65
  return (
39
66
  <View
40
67
  style={[
41
68
  styles.base,
42
- { width: width as any, height, borderRadius, backgroundColor: colors.muted },
69
+ { width: resolvedWidth as any, height: resolvedHeight, borderRadius: resolvedRadius, backgroundColor: colors.surface },
43
70
  style,
44
71
  ]}
45
72
  onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
@@ -55,7 +55,7 @@ export function Slider({
55
55
  </Text>
56
56
  ) : null}
57
57
  {showValue ? (
58
- <Text style={[styles.valueText, { color: colors.mutedForeground }]} allowFontScaling={true}>
58
+ <Text style={[styles.valueText, { color: colors.foregroundMuted }]} allowFontScaling={true}>
59
59
  {formatValue(value)}
60
60
  </Text>
61
61
  ) : null}
@@ -71,7 +71,7 @@ export function Slider({
71
71
  onValueChange={handleValueChange}
72
72
  onSlidingComplete={onSlidingComplete}
73
73
  minimumTrackTintColor={colors.primary}
74
- maximumTrackTintColor={colors.muted}
74
+ maximumTrackTintColor={colors.surface}
75
75
  thumbTintColor={colors.primary}
76
76
  style={styles.slider}
77
77
  accessibilityLabel={accessibilityLabel}
@@ -31,7 +31,7 @@ export function Spinner({ size = 'md', color, label, ...props }: SpinnerProps) {
31
31
  <View style={styles.wrapper}>
32
32
  <ActivityIndicator size={sizeMap[size]} color={color ?? colors.primary} {...props} />
33
33
  <Text
34
- style={[styles.label, { color: colors.mutedForeground, fontSize: labelFontSize[size] }]}
34
+ style={[styles.label, { color: colors.foregroundMuted, fontSize: labelFontSize[size] }]}
35
35
  allowFontScaling={true}
36
36
  >
37
37
  {label}