@retray-dev/ui-kit 0.1.0 → 1.5.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 (56) hide show
  1. package/COMPONENTS.md +710 -0
  2. package/LICENSE +21 -0
  3. package/README.md +150 -0
  4. package/dist/index.d.mts +345 -4
  5. package/dist/index.d.ts +345 -4
  6. package/dist/index.js +1644 -58
  7. package/dist/index.mjs +1590 -58
  8. package/package.json +44 -5
  9. package/src/components/Accordion/Accordion.tsx +173 -0
  10. package/src/components/Accordion/index.ts +2 -0
  11. package/src/components/Alert/Alert.tsx +59 -0
  12. package/src/components/Alert/index.ts +2 -0
  13. package/src/components/Avatar/Avatar.tsx +71 -0
  14. package/src/components/Avatar/index.ts +2 -0
  15. package/src/components/Badge/Badge.tsx +48 -0
  16. package/src/components/Badge/index.ts +2 -0
  17. package/src/components/Button/Button.tsx +94 -45
  18. package/src/components/Card/Card.tsx +103 -0
  19. package/src/components/Card/index.ts +9 -0
  20. package/src/components/Checkbox/Checkbox.tsx +98 -0
  21. package/src/components/Checkbox/index.ts +2 -0
  22. package/src/components/EmptyState/EmptyState.tsx +67 -0
  23. package/src/components/EmptyState/index.ts +2 -0
  24. package/src/components/Input/Input.tsx +28 -35
  25. package/src/components/Progress/Progress.tsx +52 -0
  26. package/src/components/Progress/index.ts +2 -0
  27. package/src/components/RadioGroup/RadioGroup.tsx +132 -0
  28. package/src/components/RadioGroup/index.ts +2 -0
  29. package/src/components/Select/Select.tsx +232 -0
  30. package/src/components/Select/index.ts +2 -0
  31. package/src/components/Separator/Separator.tsx +33 -0
  32. package/src/components/Separator/index.ts +2 -0
  33. package/src/components/Sheet/Sheet.tsx +115 -0
  34. package/src/components/Sheet/index.ts +2 -0
  35. package/src/components/Skeleton/Skeleton.tsx +63 -0
  36. package/src/components/Skeleton/index.ts +2 -0
  37. package/src/components/Slider/Slider.tsx +143 -0
  38. package/src/components/Slider/index.ts +2 -0
  39. package/src/components/Spinner/Spinner.tsx +21 -0
  40. package/src/components/Spinner/index.ts +2 -0
  41. package/src/components/Switch/Switch.tsx +86 -0
  42. package/src/components/Switch/index.ts +2 -0
  43. package/src/components/Tabs/Tabs.tsx +196 -0
  44. package/src/components/Tabs/index.ts +2 -0
  45. package/src/components/Text/Text.tsx +10 -4
  46. package/src/components/Textarea/Textarea.tsx +89 -0
  47. package/src/components/Textarea/index.ts +2 -0
  48. package/src/components/Toast/Toast.tsx +200 -0
  49. package/src/components/Toast/index.ts +2 -0
  50. package/src/components/Toggle/Toggle.tsx +92 -0
  51. package/src/components/Toggle/index.ts +2 -0
  52. package/src/index.ts +26 -0
  53. package/src/theme/ThemeProvider.tsx +47 -0
  54. package/src/theme/colors.ts +45 -0
  55. package/src/theme/index.ts +4 -0
  56. package/src/theme/types.ts +33 -0
@@ -0,0 +1,232 @@
1
+ import React, { useRef, useCallback } from 'react'
2
+ import { View, Text, TouchableOpacity, Animated, StyleSheet, ViewStyle } from 'react-native'
3
+ import {
4
+ BottomSheetModal,
5
+ BottomSheetView,
6
+ BottomSheetBackdrop,
7
+ type BottomSheetBackdropProps,
8
+ } from '@gorhom/bottom-sheet'
9
+ import * as Haptics from 'expo-haptics'
10
+ import { useTheme } from '../../theme'
11
+
12
+ export interface SelectOption {
13
+ label: string
14
+ value: string
15
+ disabled?: boolean
16
+ }
17
+
18
+ export interface SelectProps {
19
+ options: SelectOption[]
20
+ value?: string
21
+ onValueChange?: (value: string) => void
22
+ /** Text shown when no option is selected. Defaults to `'Select an option'`. */
23
+ placeholder?: string
24
+ label?: string
25
+ /** Red helper text; also changes trigger border to `destructive` color. */
26
+ error?: string
27
+ disabled?: boolean
28
+ style?: ViewStyle
29
+ }
30
+
31
+ export function Select({
32
+ options,
33
+ value,
34
+ onValueChange,
35
+ placeholder = 'Select an option',
36
+ label,
37
+ error,
38
+ disabled,
39
+ style,
40
+ }: SelectProps) {
41
+ const { colors } = useTheme()
42
+ const bottomSheetRef = useRef<BottomSheetModal>(null)
43
+ const scale = useRef(new Animated.Value(1)).current
44
+
45
+ const selected = options.find((o) => o.value === value)
46
+
47
+ const handlePressIn = () => {
48
+ if (disabled) return
49
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
50
+ }
51
+
52
+ const handlePressOut = () => {
53
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
54
+ }
55
+
56
+ const handleOpen = () => {
57
+ if (!disabled) {
58
+ Haptics.selectionAsync()
59
+ bottomSheetRef.current?.present()
60
+ }
61
+ }
62
+
63
+ const renderBackdrop = useCallback(
64
+ (props: BottomSheetBackdropProps) => (
65
+ <BottomSheetBackdrop
66
+ {...props}
67
+ disappearsOnIndex={-1}
68
+ appearsOnIndex={0}
69
+ pressBehavior="close"
70
+ />
71
+ ),
72
+ []
73
+ )
74
+
75
+ return (
76
+ <View style={[styles.container, style]}>
77
+ {label ? <Text style={[styles.label, { color: colors.foreground }]}>{label}</Text> : null}
78
+
79
+ <Animated.View style={{ transform: [{ scale }], opacity: disabled ? 0.45 : 1 }}>
80
+ <TouchableOpacity
81
+ style={[
82
+ styles.trigger,
83
+ {
84
+ borderColor: error ? colors.destructive : colors.border,
85
+ backgroundColor: colors.background,
86
+ },
87
+ ]}
88
+ onPress={handleOpen}
89
+ onPressIn={handlePressIn}
90
+ onPressOut={handlePressOut}
91
+ activeOpacity={1}
92
+ touchSoundDisabled={true}
93
+ >
94
+ <Text
95
+ style={[
96
+ styles.triggerText,
97
+ { color: selected ? colors.foreground : colors.mutedForeground },
98
+ ]}
99
+ numberOfLines={1}
100
+ >
101
+ {selected?.label ?? placeholder}
102
+ </Text>
103
+ <Text style={[styles.chevron, { color: colors.mutedForeground }]}>▾</Text>
104
+ </TouchableOpacity>
105
+ </Animated.View>
106
+
107
+ {error ? (
108
+ <Text style={[styles.helperText, { color: colors.destructive }]}>{error}</Text>
109
+ ) : null}
110
+
111
+ <BottomSheetModal
112
+ ref={bottomSheetRef}
113
+ enableDynamicSizing
114
+ enablePanDownToClose
115
+ backdropComponent={renderBackdrop}
116
+ backgroundStyle={[styles.sheetBackground, { backgroundColor: colors.card }]}
117
+ handleIndicatorStyle={[styles.sheetHandle, { backgroundColor: colors.border }]}
118
+ >
119
+ <BottomSheetView style={styles.sheetContent}>
120
+ {label ? (
121
+ <Text style={[styles.sheetTitle, { color: colors.foreground }]}>{label}</Text>
122
+ ) : null}
123
+ {options.map((item) => {
124
+ const isSelected = item.value === value
125
+ return (
126
+ <TouchableOpacity
127
+ key={item.value}
128
+ style={[
129
+ styles.option,
130
+ isSelected && { backgroundColor: colors.accent },
131
+ item.disabled && styles.disabledOption,
132
+ ]}
133
+ onPress={() => {
134
+ if (!item.disabled) {
135
+ Haptics.selectionAsync()
136
+ onValueChange?.(item.value)
137
+ bottomSheetRef.current?.dismiss()
138
+ }
139
+ }}
140
+ activeOpacity={0.7}
141
+ touchSoundDisabled={true}
142
+ >
143
+ <Text
144
+ style={[
145
+ styles.optionText,
146
+ { color: item.disabled ? colors.mutedForeground : colors.foreground },
147
+ isSelected && { fontWeight: '500' },
148
+ ]}
149
+ >
150
+ {item.label}
151
+ </Text>
152
+ {isSelected ? (
153
+ <Text style={[styles.checkmark, { color: colors.primary }]}>✓</Text>
154
+ ) : null}
155
+ </TouchableOpacity>
156
+ )
157
+ })}
158
+ </BottomSheetView>
159
+ </BottomSheetModal>
160
+ </View>
161
+ )
162
+ }
163
+
164
+ const styles = StyleSheet.create({
165
+ container: {
166
+ gap: 4,
167
+ },
168
+ label: {
169
+ fontSize: 14,
170
+ fontWeight: '500',
171
+ marginBottom: 2,
172
+ },
173
+ trigger: {
174
+ flexDirection: 'row',
175
+ alignItems: 'center',
176
+ justifyContent: 'space-between',
177
+ borderWidth: 1.5,
178
+ borderRadius: 8,
179
+ paddingHorizontal: 16,
180
+ paddingVertical: 14,
181
+ },
182
+ triggerText: {
183
+ fontSize: 16,
184
+ flex: 1,
185
+ },
186
+ chevron: {
187
+ fontSize: 14,
188
+ marginLeft: 8,
189
+ },
190
+ helperText: {
191
+ fontSize: 12,
192
+ },
193
+ sheetBackground: {
194
+ borderTopLeftRadius: 16,
195
+ borderTopRightRadius: 16,
196
+ },
197
+ sheetHandle: {
198
+ width: 36,
199
+ height: 4,
200
+ borderRadius: 2,
201
+ },
202
+ sheetContent: {
203
+ paddingHorizontal: 16,
204
+ paddingBottom: 32,
205
+ },
206
+ sheetTitle: {
207
+ fontSize: 16,
208
+ fontWeight: '600',
209
+ paddingVertical: 12,
210
+ paddingHorizontal: 4,
211
+ },
212
+ option: {
213
+ flexDirection: 'row',
214
+ alignItems: 'center',
215
+ justifyContent: 'space-between',
216
+ paddingHorizontal: 12,
217
+ paddingVertical: 14,
218
+ borderRadius: 8,
219
+ },
220
+ optionText: {
221
+ fontSize: 15,
222
+ flex: 1,
223
+ },
224
+ disabledOption: {
225
+ opacity: 0.45,
226
+ },
227
+ checkmark: {
228
+ fontSize: 14,
229
+ fontWeight: '600',
230
+ marginLeft: 8,
231
+ },
232
+ })
@@ -0,0 +1,2 @@
1
+ export { Select } from './Select'
2
+ export type { SelectProps, SelectOption } from './Select'
@@ -0,0 +1,33 @@
1
+ import React from 'react'
2
+ import { View, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export interface SeparatorProps {
6
+ orientation?: 'horizontal' | 'vertical'
7
+ style?: ViewStyle
8
+ }
9
+
10
+ export function Separator({ orientation = 'horizontal', style }: SeparatorProps) {
11
+ const { colors } = useTheme()
12
+
13
+ return (
14
+ <View
15
+ style={[
16
+ orientation === 'horizontal' ? styles.horizontal : styles.vertical,
17
+ { backgroundColor: colors.border },
18
+ style,
19
+ ]}
20
+ />
21
+ )
22
+ }
23
+
24
+ const styles = StyleSheet.create({
25
+ horizontal: {
26
+ height: 1,
27
+ width: '100%',
28
+ },
29
+ vertical: {
30
+ width: 1,
31
+ height: '100%',
32
+ },
33
+ })
@@ -0,0 +1,2 @@
1
+ export { Separator } from './Separator'
2
+ export type { SeparatorProps } from './Separator'
@@ -0,0 +1,115 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import {
4
+ BottomSheetModal,
5
+ BottomSheetView,
6
+ BottomSheetBackdrop,
7
+ BottomSheetModalProvider,
8
+ type BottomSheetBackdropProps,
9
+ } from '@gorhom/bottom-sheet'
10
+ import * as Haptics from 'expo-haptics'
11
+ import { useTheme } from '../../theme'
12
+
13
+ export { BottomSheetModalProvider }
14
+
15
+ export interface SheetProps {
16
+ open: boolean
17
+ onClose: () => void
18
+ title?: string
19
+ description?: string
20
+ children?: React.ReactNode
21
+ /**
22
+ * Heights the sheet can snap to. Accepts percentage strings (`'50%'`) or
23
+ * absolute point values (`300`). Defaults to `['50%']`.
24
+ */
25
+ snapPoints?: (string | number)[]
26
+ /** Style for the inner `BottomSheetView` content container. */
27
+ style?: ViewStyle
28
+ }
29
+
30
+ export function Sheet({
31
+ open,
32
+ onClose,
33
+ title,
34
+ description,
35
+ children,
36
+ snapPoints = ['50%'],
37
+ style,
38
+ }: SheetProps) {
39
+ const { colors } = useTheme()
40
+ const ref = useRef<BottomSheetModal>(null)
41
+
42
+ useEffect(() => {
43
+ if (open) {
44
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
45
+ ref.current?.present()
46
+ } else {
47
+ ref.current?.dismiss()
48
+ }
49
+ }, [open])
50
+
51
+ const renderBackdrop = (props: BottomSheetBackdropProps) => (
52
+ <BottomSheetBackdrop
53
+ {...props}
54
+ disappearsOnIndex={-1}
55
+ appearsOnIndex={0}
56
+ pressBehavior="close"
57
+ />
58
+ )
59
+
60
+ return (
61
+ <BottomSheetModal
62
+ ref={ref}
63
+ snapPoints={snapPoints}
64
+ onDismiss={onClose}
65
+ backdropComponent={renderBackdrop}
66
+ backgroundStyle={[styles.background, { backgroundColor: colors.card }]}
67
+ handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
68
+ enablePanDownToClose
69
+ >
70
+ <BottomSheetView style={[styles.content, style]}>
71
+ {title || description ? (
72
+ <View style={styles.header}>
73
+ {title ? (
74
+ <Text style={[styles.title, { color: colors.cardForeground }]}>{title}</Text>
75
+ ) : null}
76
+ {description ? (
77
+ <Text style={[styles.description, { color: colors.mutedForeground }]}>
78
+ {description}
79
+ </Text>
80
+ ) : null}
81
+ </View>
82
+ ) : null}
83
+ {children}
84
+ </BottomSheetView>
85
+ </BottomSheetModal>
86
+ )
87
+ }
88
+
89
+ const styles = StyleSheet.create({
90
+ background: {
91
+ borderTopLeftRadius: 16,
92
+ borderTopRightRadius: 16,
93
+ },
94
+ handle: {
95
+ width: 36,
96
+ height: 4,
97
+ borderRadius: 2,
98
+ },
99
+ content: {
100
+ paddingHorizontal: 24,
101
+ paddingBottom: 32,
102
+ },
103
+ header: {
104
+ gap: 8,
105
+ marginBottom: 16,
106
+ },
107
+ title: {
108
+ fontSize: 18,
109
+ fontWeight: '600',
110
+ },
111
+ description: {
112
+ fontSize: 14,
113
+ lineHeight: 20,
114
+ },
115
+ })
@@ -0,0 +1,2 @@
1
+ export { Sheet, BottomSheetModalProvider } from './Sheet'
2
+ export type { SheetProps } from './Sheet'
@@ -0,0 +1,63 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import { Animated, StyleSheet, View, ViewStyle } from 'react-native'
3
+ import { LinearGradient } from 'expo-linear-gradient'
4
+ import { useTheme } from '../../theme'
5
+
6
+ export interface SkeletonProps {
7
+ width?: number | string
8
+ height?: number
9
+ borderRadius?: number
10
+ style?: ViewStyle
11
+ }
12
+
13
+ export function Skeleton({ width = '100%', height = 16, borderRadius = 6, style }: SkeletonProps) {
14
+ const { colors, colorScheme } = useTheme()
15
+ const shimmerAnim = useRef(new Animated.Value(0)).current
16
+ const [containerWidth, setContainerWidth] = useState(300)
17
+
18
+ const shimmerHighlight =
19
+ colorScheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'
20
+
21
+ useEffect(() => {
22
+ const animation = Animated.loop(
23
+ Animated.timing(shimmerAnim, {
24
+ toValue: 1,
25
+ duration: 1200,
26
+ useNativeDriver: true,
27
+ })
28
+ )
29
+ animation.start()
30
+ return () => animation.stop()
31
+ }, [shimmerAnim])
32
+
33
+ const translateX = shimmerAnim.interpolate({
34
+ inputRange: [0, 1],
35
+ outputRange: [-containerWidth, containerWidth],
36
+ })
37
+
38
+ return (
39
+ <View
40
+ style={[
41
+ styles.base,
42
+ { width: width as any, height, borderRadius, backgroundColor: colors.muted },
43
+ style,
44
+ ]}
45
+ onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
46
+ >
47
+ <Animated.View style={[StyleSheet.absoluteFill, { transform: [{ translateX }] }]}>
48
+ <LinearGradient
49
+ colors={['transparent', shimmerHighlight, 'transparent']}
50
+ start={{ x: 0, y: 0 }}
51
+ end={{ x: 1, y: 0 }}
52
+ style={StyleSheet.absoluteFill}
53
+ />
54
+ </Animated.View>
55
+ </View>
56
+ )
57
+ }
58
+
59
+ const styles = StyleSheet.create({
60
+ base: {
61
+ overflow: 'hidden',
62
+ },
63
+ })
@@ -0,0 +1,2 @@
1
+ export { Skeleton } from './Skeleton'
2
+ export type { SkeletonProps } from './Skeleton'
@@ -0,0 +1,143 @@
1
+ import React, { useRef, useState } from 'react'
2
+ import { View, PanResponder, StyleSheet, LayoutChangeEvent, ViewStyle } from 'react-native'
3
+ import * as Haptics from 'expo-haptics'
4
+ import { useTheme } from '../../theme'
5
+
6
+ export interface SliderProps {
7
+ /** Current value. Controlled when provided; falls back to internal state otherwise. */
8
+ value?: number
9
+ minimumValue?: number
10
+ maximumValue?: number
11
+ /** Snap interval. `0` (default) means continuous (no snapping). */
12
+ step?: number
13
+ /** Called on every move while dragging. */
14
+ onValueChange?: (value: number) => void
15
+ /** Called once when the user releases the thumb. */
16
+ onSlidingComplete?: (value: number) => void
17
+ disabled?: boolean
18
+ style?: ViewStyle
19
+ }
20
+
21
+ export function Slider({
22
+ value = 0,
23
+ minimumValue = 0,
24
+ maximumValue = 1,
25
+ step = 0,
26
+ onValueChange,
27
+ onSlidingComplete,
28
+ disabled,
29
+ style,
30
+ }: SliderProps) {
31
+ const { colors } = useTheme()
32
+ const trackWidth = useRef(0)
33
+ const lastSteppedValue = useRef(value)
34
+ const [internalValue, setInternalValue] = useState(value)
35
+ const currentValue = value ?? internalValue
36
+
37
+ const clamp = (v: number) => Math.min(Math.max(v, minimumValue), maximumValue)
38
+
39
+ const snapToStep = (v: number) => {
40
+ if (!step) return v
41
+ return Math.round((v - minimumValue) / step) * step + minimumValue
42
+ }
43
+
44
+ const xToValue = (x: number): number => {
45
+ const ratio = Math.min(Math.max(x / trackWidth.current, 0), 1)
46
+ const raw = ratio * (maximumValue - minimumValue) + minimumValue
47
+ return clamp(snapToStep(raw))
48
+ }
49
+
50
+ const panResponder = useRef(
51
+ PanResponder.create({
52
+ onStartShouldSetPanResponder: () => !disabled,
53
+ onMoveShouldSetPanResponder: () => !disabled,
54
+ onPanResponderGrant: (e) => {
55
+ const x = e.nativeEvent.locationX
56
+ const newValue = xToValue(x)
57
+ setInternalValue(newValue)
58
+ onValueChange?.(newValue)
59
+ },
60
+ onPanResponderMove: (e) => {
61
+ const x = e.nativeEvent.locationX
62
+ const newValue = xToValue(x)
63
+ if (newValue !== lastSteppedValue.current) {
64
+ lastSteppedValue.current = newValue
65
+ Haptics.selectionAsync()
66
+ }
67
+ setInternalValue(newValue)
68
+ onValueChange?.(newValue)
69
+ },
70
+ onPanResponderRelease: (e) => {
71
+ const x = e.nativeEvent.locationX
72
+ const newValue = xToValue(x)
73
+ setInternalValue(newValue)
74
+ onSlidingComplete?.(newValue)
75
+ },
76
+ })
77
+ ).current
78
+
79
+ const onLayout = (e: LayoutChangeEvent) => {
80
+ trackWidth.current = e.nativeEvent.layout.width
81
+ }
82
+
83
+ const percent = ((currentValue - minimumValue) / (maximumValue - minimumValue)) * 100
84
+
85
+ return (
86
+ <View
87
+ style={[styles.container, disabled && styles.disabled, style]}
88
+ {...panResponder.panHandlers}
89
+ onLayout={onLayout}
90
+ >
91
+ <View style={[styles.track, { backgroundColor: colors.muted }]}>
92
+ <View
93
+ style={[styles.range, { width: `${percent}%` as any, backgroundColor: colors.primary }]}
94
+ />
95
+ </View>
96
+ <View
97
+ style={[
98
+ styles.thumb,
99
+ {
100
+ left: `${percent}%` as any,
101
+ backgroundColor: colors.primary,
102
+ borderColor: colors.background,
103
+ transform: [{ translateX: -14 }],
104
+ },
105
+ ]}
106
+ pointerEvents="none"
107
+ />
108
+ </View>
109
+ )
110
+ }
111
+
112
+ const styles = StyleSheet.create({
113
+ container: {
114
+ height: 32,
115
+ justifyContent: 'center',
116
+ position: 'relative',
117
+ },
118
+ disabled: {
119
+ opacity: 0.45,
120
+ },
121
+ track: {
122
+ height: 6,
123
+ borderRadius: 3,
124
+ overflow: 'hidden',
125
+ width: '100%',
126
+ },
127
+ range: {
128
+ height: '100%',
129
+ borderRadius: 3,
130
+ },
131
+ thumb: {
132
+ position: 'absolute',
133
+ width: 28,
134
+ height: 28,
135
+ borderRadius: 14,
136
+ borderWidth: 2,
137
+ shadowColor: '#000',
138
+ shadowOffset: { width: 0, height: 1 },
139
+ shadowOpacity: 0.2,
140
+ shadowRadius: 2,
141
+ elevation: 2,
142
+ },
143
+ })
@@ -0,0 +1,2 @@
1
+ export { Slider } from './Slider'
2
+ export type { SliderProps } from './Slider'
@@ -0,0 +1,21 @@
1
+ import React from 'react'
2
+ import { ActivityIndicator, ActivityIndicatorProps } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export type SpinnerSize = 'sm' | 'md' | 'lg'
6
+
7
+ export interface SpinnerProps extends Omit<ActivityIndicatorProps, 'size'> {
8
+ size?: SpinnerSize
9
+ color?: string
10
+ }
11
+
12
+ const sizeMap: Record<SpinnerSize, 'small' | 'large'> = {
13
+ sm: 'small',
14
+ md: 'small',
15
+ lg: 'large',
16
+ }
17
+
18
+ export function Spinner({ size = 'md', color, ...props }: SpinnerProps) {
19
+ const { colors } = useTheme()
20
+ return <ActivityIndicator size={sizeMap[size]} color={color ?? colors.primary} {...props} />
21
+ }
@@ -0,0 +1,2 @@
1
+ export { Spinner } from './Spinner'
2
+ export type { SpinnerProps, SpinnerSize } from './Spinner'