@retray-dev/ui-kit 1.7.0 → 2.3.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 (40) hide show
  1. package/COMPONENTS.md +258 -54
  2. package/README.md +6 -5
  3. package/dist/index.d.mts +113 -44
  4. package/dist/index.d.ts +113 -44
  5. package/dist/index.js +802 -324
  6. package/dist/index.mjs +794 -323
  7. package/package.json +6 -2
  8. package/src/components/Alert/Alert.tsx +24 -12
  9. package/src/components/AlertBanner/AlertBanner.tsx +83 -0
  10. package/src/components/AlertBanner/index.ts +2 -0
  11. package/src/components/Avatar/Avatar.tsx +1 -0
  12. package/src/components/Badge/Badge.tsx +44 -8
  13. package/src/components/Button/Button.tsx +12 -5
  14. package/src/components/Card/Card.tsx +86 -9
  15. package/src/components/Chip/Chip.tsx +173 -0
  16. package/src/components/Chip/index.ts +2 -0
  17. package/src/components/ConfirmDialog/ConfirmDialog.tsx +87 -0
  18. package/src/components/ConfirmDialog/index.ts +2 -0
  19. package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +4 -2
  20. package/src/components/CurrencyInput/CurrencyInput.tsx +9 -1
  21. package/src/components/EmptyState/EmptyState.tsx +42 -7
  22. package/src/components/Input/Input.tsx +102 -21
  23. package/src/components/LabelValue/LabelValue.tsx +47 -0
  24. package/src/components/LabelValue/index.ts +2 -0
  25. package/src/components/ListItem/ListItem.tsx +121 -0
  26. package/src/components/ListItem/index.ts +2 -0
  27. package/src/components/MonthPicker/MonthPicker.tsx +92 -0
  28. package/src/components/MonthPicker/index.ts +2 -0
  29. package/src/components/Select/Select.tsx +189 -125
  30. package/src/components/Slider/Slider.tsx +64 -100
  31. package/src/components/Switch/Switch.tsx +25 -21
  32. package/src/components/Textarea/Textarea.tsx +12 -2
  33. package/src/components/Toggle/Toggle.tsx +13 -6
  34. package/src/index.ts +8 -2
  35. package/src/theme/ThemeProvider.tsx +11 -8
  36. package/src/theme/colors.ts +19 -18
  37. package/src/theme/types.ts +2 -0
  38. package/src/components/Alert/index.ts +0 -2
  39. package/src/components/CurrencyInputLarge/CurrencyInputLarge.tsx +0 -66
  40. package/src/components/CurrencyInputLarge/index.ts +0 -1
@@ -1,14 +1,13 @@
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'
1
+ import React, { useRef, useState } from 'react'
2
+ import { View, Text, TouchableOpacity, Modal, Animated, StyleSheet, ViewStyle, Platform } from 'react-native'
3
+ import { Picker } from '@react-native-picker/picker'
9
4
  import * as Haptics from 'expo-haptics'
10
5
  import { useTheme } from '../../theme'
11
6
 
7
+ const isIOS = Platform.OS === 'ios'
8
+ const isAndroid = Platform.OS === 'android'
9
+ const isWeb = Platform.OS === 'web'
10
+
12
11
  export interface SelectOption {
13
12
  label: string
14
13
  value: string
@@ -19,10 +18,8 @@ export interface SelectProps {
19
18
  options: SelectOption[]
20
19
  value?: string
21
20
  onValueChange?: (value: string) => void
22
- /** Text shown when no option is selected. Defaults to `'Select an option'`. */
23
21
  placeholder?: string
24
22
  label?: string
25
- /** Red helper text; also changes trigger border to `destructive` color. */
26
23
  error?: string
27
24
  disabled?: boolean
28
25
  style?: ViewStyle
@@ -39,8 +36,10 @@ export function Select({
39
36
  style,
40
37
  }: SelectProps) {
41
38
  const { colors } = useTheme()
42
- const bottomSheetRef = useRef<BottomSheetModal>(null)
43
39
  const scale = useRef(new Animated.Value(1)).current
40
+ const [pickerVisible, setPickerVisible] = useState(false)
41
+ const [pendingValue, setPendingValue] = useState<string | undefined>(value)
42
+ const pickerRef = useRef<React.ElementRef<typeof Picker>>(null)
44
43
 
45
44
  const selected = options.find((o) => o.value === value)
46
45
 
@@ -54,109 +53,173 @@ export function Select({
54
53
  }
55
54
 
56
55
  const handleOpen = () => {
57
- if (!disabled) {
58
- Haptics.selectionAsync()
59
- bottomSheetRef.current?.present()
56
+ if (disabled) return
57
+ Haptics.selectionAsync()
58
+ if (isIOS) {
59
+ setPendingValue(value)
60
+ setPickerVisible(true)
61
+ } else if (isAndroid) {
62
+ ;(pickerRef.current as any)?.focus()
60
63
  }
61
64
  }
62
65
 
63
- const renderBackdrop = useCallback(
64
- (props: BottomSheetBackdropProps) => (
65
- <BottomSheetBackdrop
66
- {...props}
67
- disappearsOnIndex={-1}
68
- appearsOnIndex={0}
69
- pressBehavior="close"
70
- />
71
- ),
72
- []
73
- )
66
+ const handleDismiss = () => {
67
+ setPickerVisible(false)
68
+ }
69
+
70
+ const handleConfirm = () => {
71
+ if (pendingValue !== undefined && pendingValue !== '') {
72
+ Haptics.selectionAsync()
73
+ onValueChange?.(pendingValue)
74
+ }
75
+ setPickerVisible(false)
76
+ }
74
77
 
75
78
  return (
76
79
  <View style={[styles.container, style]}>
77
80
  {label ? <Text style={[styles.label, { color: colors.foreground }]}>{label}</Text> : null}
78
81
 
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
82
+ {/* Trigger button shown on iOS and Android only */}
83
+ {!isWeb ? (
84
+ <Animated.View style={{ transform: [{ scale }], opacity: disabled ? 0.45 : 1 }}>
85
+ <TouchableOpacity
86
+ style={[
87
+ styles.trigger,
88
+ {
89
+ borderColor: error ? colors.destructive : colors.border,
90
+ backgroundColor: colors.background,
91
+ },
92
+ ]}
93
+ onPress={handleOpen}
94
+ onPressIn={handlePressIn}
95
+ onPressOut={handlePressOut}
96
+ activeOpacity={1}
97
+ touchSoundDisabled={true}
98
+ >
99
+ <Text
100
+ style={[
101
+ styles.triggerText,
102
+ { color: selected ? colors.foreground : colors.mutedForeground },
103
+ ]}
104
+ numberOfLines={1}
105
+ allowFontScaling={true}
106
+ >
107
+ {selected?.label ?? placeholder}
108
+ </Text>
109
+ <Text style={[styles.chevron, { color: colors.mutedForeground }]}>▾</Text>
110
+ </TouchableOpacity>
111
+ </Animated.View>
112
+ ) : null}
113
+
114
+ {/* iOS: Modal with wheel Picker */}
115
+ {isIOS ? (
116
+ <Modal
117
+ visible={pickerVisible}
118
+ transparent
119
+ animationType="slide"
120
+ onRequestClose={handleDismiss}
121
+ >
122
+ <TouchableOpacity style={styles.iosBackdrop} activeOpacity={1} onPress={handleDismiss} />
123
+ <View style={[styles.iosSheet, { backgroundColor: colors.card }]}>
124
+ <View style={[styles.iosToolbar, { borderBottomColor: colors.border }]}>
125
+ {label ? (
126
+ <Text style={[styles.iosToolbarTitle, { color: colors.foreground }]} allowFontScaling={true}>
127
+ {label}
128
+ </Text>
129
+ ) : (
130
+ <View />
131
+ )}
132
+ <TouchableOpacity onPress={handleConfirm} style={styles.iosDoneBtn} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
133
+ <Text style={[styles.iosDoneBtnText, { color: colors.primary }]} allowFontScaling={true}>
134
+ Done
135
+ </Text>
136
+ </TouchableOpacity>
137
+ </View>
138
+ <Picker
139
+ selectedValue={pendingValue ?? ''}
140
+ onValueChange={(val) => setPendingValue(val as string)}
141
+ itemStyle={{ color: colors.foreground }}
142
+ >
143
+ {!value ? (
144
+ <Picker.Item label={placeholder} value="" color={colors.mutedForeground} enabled={false} />
145
+ ) : null}
146
+ {options.map((o) => (
147
+ <Picker.Item
148
+ key={o.value}
149
+ label={o.label}
150
+ value={o.value}
151
+ enabled={!o.disabled}
152
+ color={o.disabled ? colors.mutedForeground : colors.foreground}
153
+ />
154
+ ))}
155
+ </Picker>
156
+ </View>
157
+ </Modal>
158
+ ) : null}
159
+
160
+ {/* Android: hidden Picker opened programmatically via focus() */}
161
+ {isAndroid ? (
162
+ <Picker
163
+ ref={pickerRef}
164
+ selectedValue={value ?? ''}
165
+ onValueChange={(val) => {
166
+ if (val !== '') {
167
+ Haptics.selectionAsync()
168
+ onValueChange?.(val as string)
169
+ }
170
+ }}
171
+ mode="dialog"
172
+ enabled={!disabled}
173
+ prompt={label}
174
+ style={styles.androidHiddenPicker}
175
+ >
176
+ {!value ? <Picker.Item label={placeholder} value="" enabled={false} /> : null}
177
+ {options.map((o) => (
178
+ <Picker.Item
179
+ key={o.value}
180
+ label={o.label}
181
+ value={o.value}
182
+ enabled={!o.disabled}
183
+ />
184
+ ))}
185
+ </Picker>
186
+ ) : null}
187
+
188
+ {/* Web: Picker renders as native <select> */}
189
+ {isWeb ? (
190
+ <Picker
191
+ selectedValue={value ?? ''}
192
+ onValueChange={(val) => {
193
+ if (val !== '') {
194
+ onValueChange?.(val as string)
195
+ }
196
+ }}
197
+ enabled={!disabled}
95
198
  style={[
96
- styles.triggerText,
97
- { color: selected ? colors.foreground : colors.mutedForeground },
199
+ styles.webPicker,
200
+ {
201
+ borderColor: error ? colors.destructive : colors.border,
202
+ color: selected ? colors.foreground : colors.mutedForeground,
203
+ backgroundColor: colors.background,
204
+ opacity: disabled ? 0.45 : 1,
205
+ },
98
206
  ]}
99
- numberOfLines={1}
100
207
  >
101
- {selected?.label ?? placeholder}
102
- </Text>
103
- <Text style={[styles.chevron, { color: colors.mutedForeground }]}>▾</Text>
104
- </TouchableOpacity>
105
- </Animated.View>
208
+ <Picker.Item label={placeholder} value="" enabled={false} />
209
+ {options.map((o) => (
210
+ <Picker.Item
211
+ key={o.value}
212
+ label={o.label}
213
+ value={o.value}
214
+ enabled={!o.disabled}
215
+ />
216
+ ))}
217
+ </Picker>
218
+ ) : null}
106
219
 
107
220
  {error ? (
108
221
  <Text style={[styles.helperText, { color: colors.destructive }]}>{error}</Text>
109
222
  ) : 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
223
  </View>
161
224
  )
162
225
  }
@@ -190,43 +253,44 @@ const styles = StyleSheet.create({
190
253
  helperText: {
191
254
  fontSize: 13,
192
255
  },
193
- sheetBackground: {
256
+ iosBackdrop: {
257
+ flex: 1,
258
+ backgroundColor: 'rgba(0,0,0,0.4)',
259
+ },
260
+ iosSheet: {
194
261
  borderTopLeftRadius: 24,
195
262
  borderTopRightRadius: 24,
263
+ paddingBottom: 32,
196
264
  },
197
- sheetHandle: {
198
- width: 36,
199
- height: 4,
200
- borderRadius: 2,
201
- },
202
- sheetContent: {
265
+ iosToolbar: {
266
+ flexDirection: 'row',
267
+ alignItems: 'center',
268
+ justifyContent: 'space-between',
203
269
  paddingHorizontal: 20,
204
- paddingBottom: 36,
270
+ paddingVertical: 12,
271
+ borderBottomWidth: 1,
205
272
  },
206
- sheetTitle: {
273
+ iosToolbarTitle: {
207
274
  fontSize: 17,
208
275
  fontWeight: '600',
209
- paddingVertical: 16,
210
- paddingHorizontal: 4,
211
276
  },
212
- option: {
213
- flexDirection: 'row',
214
- alignItems: 'center',
215
- justifyContent: 'space-between',
216
- paddingHorizontal: 16,
217
- paddingVertical: 16,
218
- borderRadius: 12,
277
+ iosDoneBtn: {
278
+ padding: 4,
219
279
  },
220
- optionText: {
280
+ iosDoneBtnText: {
221
281
  fontSize: 17,
222
- flex: 1,
282
+ fontWeight: '600',
223
283
  },
224
- disabledOption: {
225
- opacity: 0.45,
284
+ androidHiddenPicker: {
285
+ height: 0,
286
+ opacity: 0,
287
+ position: 'absolute',
226
288
  },
227
- checkmark: {
228
- fontSize: 16,
229
- fontWeight: '600',
230
- marginLeft: 8,
289
+ webPicker: {
290
+ borderWidth: 1.5,
291
+ borderRadius: 14,
292
+ paddingHorizontal: 20,
293
+ paddingVertical: 16,
294
+ fontSize: 17,
231
295
  },
232
296
  })
@@ -1,19 +1,20 @@
1
- import React, { useRef, useState } from 'react'
2
- import { View, PanResponder, StyleSheet, LayoutChangeEvent, ViewStyle } from 'react-native'
1
+ import React, { useRef } from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import RNSlider from '@react-native-community/slider'
3
4
  import * as Haptics from 'expo-haptics'
4
5
  import { useTheme } from '../../theme'
5
6
 
6
7
  export interface SliderProps {
7
- /** Current value. Controlled when provided; falls back to internal state otherwise. */
8
8
  value?: number
9
9
  minimumValue?: number
10
10
  maximumValue?: number
11
- /** Snap interval. `0` (default) means continuous (no snapping). */
12
11
  step?: number
13
- /** Called on every move while dragging. */
14
12
  onValueChange?: (value: number) => void
15
- /** Called once when the user releases the thumb. */
16
13
  onSlidingComplete?: (value: number) => void
14
+ label?: string
15
+ showValue?: boolean
16
+ formatValue?: (value: number) => string
17
+ accessibilityLabel?: string
17
18
  disabled?: boolean
18
19
  style?: ViewStyle
19
20
  }
@@ -25,119 +26,82 @@ export function Slider({
25
26
  step = 0,
26
27
  onValueChange,
27
28
  onSlidingComplete,
29
+ label,
30
+ showValue = false,
31
+ formatValue = (v: number) => v.toFixed(2),
32
+ accessibilityLabel,
28
33
  disabled,
29
34
  style,
30
35
  }: SliderProps) {
31
36
  const { colors } = useTheme()
32
- const trackWidth = useRef(0)
33
37
  const lastSteppedValue = useRef(value)
34
- const [internalValue, setInternalValue] = useState(value)
35
- const currentValue = value ?? internalValue
36
38
 
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))
39
+ const handleValueChange = (v: number) => {
40
+ if (step && v !== lastSteppedValue.current) {
41
+ lastSteppedValue.current = v
42
+ Haptics.selectionAsync()
43
+ }
44
+ onValueChange?.(v)
48
45
  }
49
46
 
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
47
  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 }]}
48
+ <View style={[styles.wrapper, style]} accessibilityLabel={accessibilityLabel}>
49
+ {label || showValue ? (
50
+ <View style={styles.header}>
51
+ {label ? (
52
+ <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>
53
+ {label}
54
+ </Text>
55
+ ) : null}
56
+ {showValue ? (
57
+ <Text style={[styles.valueText, { color: colors.mutedForeground }]} allowFontScaling={true}>
58
+ {formatValue(value)}
59
+ </Text>
60
+ ) : null}
61
+ </View>
62
+ ) : null}
63
+ <View style={disabled ? styles.disabled : undefined}>
64
+ <RNSlider
65
+ value={value}
66
+ minimumValue={minimumValue}
67
+ maximumValue={maximumValue}
68
+ step={step || 0}
69
+ disabled={disabled}
70
+ onValueChange={handleValueChange}
71
+ onSlidingComplete={onSlidingComplete}
72
+ minimumTrackTintColor={colors.primary}
73
+ maximumTrackTintColor={colors.muted}
74
+ thumbTintColor={colors.primary}
75
+ style={styles.slider}
76
+ accessibilityLabel={accessibilityLabel}
94
77
  />
95
78
  </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
79
  </View>
109
80
  )
110
81
  }
111
82
 
112
83
  const styles = StyleSheet.create({
113
- container: {
114
- height: 32,
115
- justifyContent: 'center',
116
- position: 'relative',
84
+ wrapper: {
85
+ gap: 8,
117
86
  },
118
- disabled: {
119
- opacity: 0.45,
87
+ header: {
88
+ flexDirection: 'row',
89
+ justifyContent: 'space-between',
90
+ alignItems: 'center',
120
91
  },
121
- track: {
122
- height: 6,
123
- borderRadius: 3,
124
- overflow: 'hidden',
125
- width: '100%',
92
+ label: {
93
+ fontSize: 15,
94
+ fontWeight: '500',
126
95
  },
127
- range: {
128
- height: '100%',
129
- borderRadius: 3,
96
+ valueText: {
97
+ fontSize: 14,
98
+ fontWeight: '500',
130
99
  },
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,
100
+ slider: {
101
+ width: '100%',
102
+ height: 40,
103
+ },
104
+ disabled: {
105
+ opacity: 0.45,
142
106
  },
143
107
  })
@@ -1,5 +1,7 @@
1
1
  import React, { useEffect, useRef } from 'react'
2
- import { TouchableOpacity, Animated, StyleSheet, ViewStyle } from 'react-native'
2
+ import { TouchableOpacity, Animated, StyleSheet, ViewStyle, Platform, View } from 'react-native'
3
+
4
+ const nativeDriver = Platform.OS !== 'web'
3
5
  import * as Haptics from 'expo-haptics'
4
6
  import { useTheme } from '../../theme'
5
7
 
@@ -25,7 +27,7 @@ export function Switch({ checked = false, onCheckedChange, disabled, style }: Sw
25
27
  Animated.parallel([
26
28
  Animated.spring(translateX, {
27
29
  toValue: checked ? THUMB_TRAVEL : 0,
28
- useNativeDriver: true,
30
+ useNativeDriver: nativeDriver,
29
31
  bounciness: 4,
30
32
  }),
31
33
  Animated.timing(trackOpacity, {
@@ -42,25 +44,27 @@ export function Switch({ checked = false, onCheckedChange, disabled, style }: Sw
42
44
  })
43
45
 
44
46
  return (
45
- <TouchableOpacity
46
- onPress={() => {
47
- Haptics.selectionAsync()
48
- onCheckedChange?.(!checked)
49
- }}
50
- disabled={disabled}
51
- activeOpacity={0.8}
52
- touchSoundDisabled={true}
53
- style={[styles.wrapper, { opacity: disabled ? 0.45 : 1 }, style]}
54
- >
55
- <Animated.View style={[styles.track, { backgroundColor: trackColor }]}>
56
- <Animated.View
57
- style={[
58
- styles.thumb,
59
- { backgroundColor: colors.primaryForeground, transform: [{ translateX }] },
60
- ]}
61
- />
62
- </Animated.View>
63
- </TouchableOpacity>
47
+ <View style={[{ opacity: disabled ? 0.45 : 1 }, style]}>
48
+ <TouchableOpacity
49
+ onPress={() => {
50
+ Haptics.selectionAsync()
51
+ onCheckedChange?.(!checked)
52
+ }}
53
+ disabled={disabled}
54
+ activeOpacity={0.8}
55
+ touchSoundDisabled={true}
56
+ style={styles.wrapper}
57
+ >
58
+ <Animated.View style={[styles.track, { backgroundColor: trackColor }]}>
59
+ <Animated.View
60
+ style={[
61
+ styles.thumb,
62
+ { backgroundColor: colors.primaryForeground, transform: [{ translateX }] },
63
+ ]}
64
+ />
65
+ </Animated.View>
66
+ </TouchableOpacity>
67
+ </View>
64
68
  )
65
69
  }
66
70
 
@@ -1,7 +1,12 @@
1
1
  import React, { useState } from 'react'
2
- import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle } from 'react-native'
2
+ import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, Platform } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
4
 
5
+ const webInputResetStyle: any =
6
+ Platform.OS === 'web'
7
+ ? { outlineStyle: 'none', outlineWidth: 0, outlineColor: 'transparent', boxShadow: 'none' }
8
+ : {}
9
+
5
10
  export interface TextareaProps extends TextInputProps {
6
11
  label?: string
7
12
  /** Red helper text below the textarea; also changes border to `destructive` color. Takes priority over `hint`. */
@@ -38,11 +43,16 @@ export function Textarea({
38
43
  style={[
39
44
  styles.input,
40
45
  {
41
- borderColor: error ? colors.destructive : focused ? colors.ring : colors.border,
46
+ borderColor: error
47
+ ? colors.destructive
48
+ : focused
49
+ ? (colors.ring ?? colors.primary)
50
+ : colors.border,
42
51
  color: colors.foreground,
43
52
  backgroundColor: colors.background,
44
53
  minHeight: rows * 30,
45
54
  },
55
+ webInputResetStyle,
46
56
  style,
47
57
  ]}
48
58
  onFocus={(e) => {