@retray-dev/ui-kit 9.1.0 → 9.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 (64) hide show
  1. package/COMPONENTS.md +166 -4
  2. package/CONSUMER.md +247 -0
  3. package/DESIGN.md +668 -0
  4. package/FONTS.md +107 -0
  5. package/README.md +3 -3
  6. package/dist/AlertBanner.d.mts +3 -1
  7. package/dist/AlertBanner.d.ts +3 -1
  8. package/dist/AlertBanner.js +18 -2
  9. package/dist/AlertBanner.mjs +1 -1
  10. package/dist/ConfirmDialog.d.mts +3 -1
  11. package/dist/ConfirmDialog.d.ts +3 -1
  12. package/dist/ConfirmDialog.js +3 -0
  13. package/dist/ConfirmDialog.mjs +1 -1
  14. package/dist/CurrencyInput.d.mts +3 -1
  15. package/dist/CurrencyInput.d.ts +3 -1
  16. package/dist/CurrencyInput.js +52 -39
  17. package/dist/CurrencyInput.mjs +2 -3
  18. package/dist/ImageUpload.d.mts +27 -0
  19. package/dist/ImageUpload.d.ts +27 -0
  20. package/dist/ImageUpload.js +399 -0
  21. package/dist/ImageUpload.mjs +9 -0
  22. package/dist/Input.d.mts +3 -1
  23. package/dist/Input.d.ts +3 -1
  24. package/dist/Input.js +48 -37
  25. package/dist/Input.mjs +1 -2
  26. package/dist/ListItem.d.mts +9 -2
  27. package/dist/ListItem.d.ts +9 -2
  28. package/dist/ListItem.js +9 -2
  29. package/dist/ListItem.mjs +1 -1
  30. package/dist/SheetSelect.d.mts +25 -0
  31. package/dist/SheetSelect.d.ts +25 -0
  32. package/dist/SheetSelect.js +440 -0
  33. package/dist/SheetSelect.mjs +9 -0
  34. package/dist/Textarea.mjs +1 -2
  35. package/dist/{chunk-M6ZXVBTK.mjs → chunk-6MKGPAR2.mjs} +21 -5
  36. package/dist/{chunk-EH745HE5.mjs → chunk-CZCQZHG6.mjs} +13 -4
  37. package/dist/{chunk-7QHVVCB3.mjs → chunk-FZZLPJ6B.mjs} +3 -0
  38. package/dist/{chunk-MAC465BB.mjs → chunk-JUXSWN54.mjs} +5 -3
  39. package/dist/{chunk-BNP626TY.mjs → chunk-OHBNABL5.mjs} +10 -3
  40. package/dist/chunk-URI2WBIV.mjs +147 -0
  41. package/dist/chunk-Y4GL2MHX.mjs +112 -0
  42. package/dist/{chunk-756RAKE4.mjs → chunk-ZUR7AU5R.mjs} +38 -20
  43. package/dist/fonts.d.mts +32 -0
  44. package/dist/fonts.d.ts +32 -0
  45. package/dist/fonts.js +44 -0
  46. package/dist/fonts.mjs +37 -0
  47. package/dist/index.d.mts +26 -1
  48. package/dist/index.d.ts +26 -1
  49. package/dist/index.js +425 -106
  50. package/dist/index.mjs +55 -17
  51. package/package.json +23 -6
  52. package/src/components/AlertBanner/AlertBanner.tsx +21 -3
  53. package/src/components/ConfirmDialog/ConfirmDialog.tsx +5 -0
  54. package/src/components/CurrencyInput/CurrencyInput.tsx +4 -0
  55. package/src/components/ImageUpload/ImageUpload.tsx +158 -0
  56. package/src/components/ImageUpload/index.ts +1 -0
  57. package/src/components/Input/Input.tsx +64 -53
  58. package/src/components/ListItem/ListItem.tsx +23 -4
  59. package/src/components/SheetSelect/SheetSelect.tsx +192 -0
  60. package/src/components/SheetSelect/index.ts +1 -0
  61. package/src/fonts.ts +30 -29
  62. package/src/hooks/useConfirmDialog.ts +67 -0
  63. package/src/index.ts +6 -0
  64. package/dist/chunk-26BCI223.mjs +0 -14
package/dist/index.mjs CHANGED
@@ -1,13 +1,16 @@
1
+ export { Tabs, TabsContent } from './chunk-GQYFLP3D.mjs';
2
+ export { Text } from './chunk-WJLKJMKR.mjs';
3
+ export { Textarea } from './chunk-CZCQZHG6.mjs';
1
4
  export { Toggle } from './chunk-KIHCWCWL.mjs';
2
5
  export { VirtualList } from './chunk-NC5ZTR2Y.mjs';
6
+ export { Separator } from './chunk-MX6HRKMI.mjs';
7
+ export { BottomSheetModalProvider, Sheet, BottomSheetTextInput as SheetTextInput } from './chunk-PFZTM6D5.mjs';
8
+ export { SheetSelect } from './chunk-URI2WBIV.mjs';
3
9
  export { Skeleton } from './chunk-AJ7ZDNBT.mjs';
4
10
  export { Slider } from './chunk-JMOZEC77.mjs';
5
- export { Spinner } from './chunk-WBOOUHSS.mjs';
6
11
  export { Switch } from './chunk-QKH5ZOD5.mjs';
7
12
  export { TabBar } from './chunk-MLF3EZFW.mjs';
8
- export { Tabs, TabsContent } from './chunk-GQYFLP3D.mjs';
9
- export { Text } from './chunk-WJLKJMKR.mjs';
10
- export { Textarea } from './chunk-EH745HE5.mjs';
13
+ export { Pressable } from './chunk-MBMXYJJV.mjs';
11
14
  export { PricingCard } from './chunk-4I7D47FH.mjs';
12
15
  export { Progress } from './chunk-OB4JUQ3O.mjs';
13
16
  export { RadioGroup } from './chunk-X4G6APW6.mjs';
@@ -15,34 +18,33 @@ export { RetrayProvider } from './chunk-YFZ3ELX5.mjs';
15
18
  export { ToastProvider, sonnerToast as toast, useToast } from './chunk-2UYENBLV.mjs';
16
19
  export { Select } from './chunk-A3A6KNQN.mjs';
17
20
  export { SelectableGrid } from './chunk-NA7PARID.mjs';
18
- export { Separator } from './chunk-MX6HRKMI.mjs';
19
- export { BottomSheetModalProvider, Sheet, BottomSheetTextInput as SheetTextInput } from './chunk-PFZTM6D5.mjs';
21
+ export { LabelValue } from './chunk-A4MDAP7G.mjs';
20
22
  export { ListGroup, ListGroupFooter, ListGroupHeader } from './chunk-SOA2Z4RB.mjs';
21
- export { ListItem } from './chunk-BNP626TY.mjs';
23
+ export { ListItem } from './chunk-OHBNABL5.mjs';
22
24
  export { MediaCard } from './chunk-VGTDN7SW.mjs';
23
25
  export { MenuGroup, MenuGroupFooter, MenuGroupHeader } from './chunk-IRRY3CRZ.mjs';
24
26
  export { MenuItem } from './chunk-ZJKGQMYH.mjs';
25
27
  export { MonthPicker, dateToMonthPickerValue, monthPickerValueToDate } from './chunk-GD6KXMG5.mjs';
26
- export { Pressable } from './chunk-MBMXYJJV.mjs';
28
+ export { DetailRow } from './chunk-JB67UOB5.mjs';
27
29
  export { EmptyState } from './chunk-6OAZJ577.mjs';
28
30
  export { ErrorBoundary } from './chunk-LXJIIOYQ.mjs';
29
31
  export { Form, FormField, FormFooter, FormSection } from './chunk-6Q64UFIA.mjs';
32
+ export { ImageUpload } from './chunk-Y4GL2MHX.mjs';
33
+ export { Spinner } from './chunk-WBOOUHSS.mjs';
30
34
  export { ImageViewer } from './chunk-Z4BVUWW6.mjs';
31
35
  export { PagerDots } from './chunk-4K625MVM.mjs';
32
- export { LabelValue } from './chunk-A4MDAP7G.mjs';
36
+ export { ButtonGroup } from './chunk-3BBOZ3OQ.mjs';
33
37
  export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './chunk-ID72TK46.mjs';
34
38
  export { CategoryStrip } from './chunk-VQ57HWPL.mjs';
35
39
  import './chunk-YNROWHQJ.mjs';
36
40
  export { Checkbox } from './chunk-AV4EMIRH.mjs';
37
41
  export { Chip, ChipGroup } from './chunk-UREA2GYY.mjs';
38
- export { ConfirmDialog } from './chunk-7QHVVCB3.mjs';
42
+ export { ConfirmDialog } from './chunk-FZZLPJ6B.mjs';
39
43
  export { CurrencyDisplay } from './chunk-BRKYVJVV.mjs';
40
- export { CurrencyInput } from './chunk-MAC465BB.mjs';
41
- export { Input } from './chunk-756RAKE4.mjs';
42
- import './chunk-26BCI223.mjs';
43
- export { DetailRow } from './chunk-JB67UOB5.mjs';
44
+ export { CurrencyInput } from './chunk-JUXSWN54.mjs';
45
+ export { Input } from './chunk-ZUR7AU5R.mjs';
44
46
  export { Accordion } from './chunk-O3HA6TYM.mjs';
45
- export { AlertBanner } from './chunk-M6ZXVBTK.mjs';
47
+ export { AlertBanner } from './chunk-6MKGPAR2.mjs';
46
48
  export { AppHeader } from './chunk-AZJF2BLK.mjs';
47
49
  export { IconButton } from './chunk-3U4SSNWP.mjs';
48
50
  export { Avatar } from './chunk-JT7HKXRB.mjs';
@@ -54,9 +56,9 @@ import './chunk-DVK4G2GT.mjs';
54
56
  export { BREAKPOINTS, ICON_SIZES, RADIUS, SHADOWS, SPACING, TYPOGRAPHY } from './chunk-QY3X2UYR.mjs';
55
57
  export { Icon, configureIconFamilies, renderIcon } from './chunk-T7XZ7H7Y.mjs';
56
58
  export { ThemeProvider, defaultDark, defaultLight, deriveColors, useTheme } from './chunk-SOYNZDVY.mjs';
57
- export { ButtonGroup } from './chunk-3BBOZ3OQ.mjs';
58
59
  import './chunk-2CE3TQVY.mjs';
59
60
  import './chunk-Y6FXYEAI.mjs';
61
+ import { useState, useCallback } from 'react';
60
62
 
61
63
  // src/utils/typography.ts
62
64
  function getResponsiveFontSize(text, maxSize, steps = [
@@ -71,5 +73,41 @@ function getResponsiveFontSize(text, maxSize, steps = [
71
73
  }
72
74
  return maxSize - 8;
73
75
  }
76
+ function useConfirmDialog(options) {
77
+ const [visible, setVisible] = useState(false);
78
+ const [target, setTarget] = useState(null);
79
+ const [loading, setLoading] = useState(false);
80
+ const open = useCallback((t) => {
81
+ setTarget(t ?? null);
82
+ setVisible(true);
83
+ }, []);
84
+ const handleConfirm = useCallback(async () => {
85
+ setLoading(true);
86
+ try {
87
+ await options.onConfirm();
88
+ } finally {
89
+ setLoading(false);
90
+ setVisible(false);
91
+ setTarget(null);
92
+ }
93
+ }, [options]);
94
+ const handleCancel = useCallback(() => {
95
+ setVisible(false);
96
+ setTarget(null);
97
+ options.onCancel?.();
98
+ }, [options]);
99
+ return {
100
+ visible,
101
+ target,
102
+ loading,
103
+ open,
104
+ dialogProps: {
105
+ visible,
106
+ loading,
107
+ onConfirm: handleConfirm,
108
+ onCancel: handleCancel
109
+ }
110
+ };
111
+ }
74
112
 
75
- export { getResponsiveFontSize };
113
+ export { getResponsiveFontSize, useConfirmDialog };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retray-dev/ui-kit",
3
- "version": "9.1.0",
3
+ "version": "9.3.0",
4
4
  "description": "Personal UI Kit for React Native / Expo",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -11,7 +11,11 @@
11
11
  "import": "./dist/index.mjs",
12
12
  "require": "./dist/index.js"
13
13
  },
14
- "./fonts": "./src/fonts.ts",
14
+ "./fonts": {
15
+ "types": "./dist/fonts.d.ts",
16
+ "import": "./dist/fonts.mjs",
17
+ "require": "./dist/fonts.js"
18
+ },
15
19
  "./*": {
16
20
  "types": "./dist/*.d.ts",
17
21
  "import": "./dist/*.mjs",
@@ -22,7 +26,10 @@
22
26
  "dist",
23
27
  "src",
24
28
  "COMPONENTS.md",
25
- "EXAMPLES.md"
29
+ "CONSUMER.md",
30
+ "EXAMPLES.md",
31
+ "FONTS.md",
32
+ "DESIGN.md"
26
33
  ],
27
34
  "scripts": {
28
35
  "build": "tsup",
@@ -55,6 +62,7 @@
55
62
  "@shopify/react-native-skia": ">=1.0.0",
56
63
  "expo-font": ">=14.0.0",
57
64
  "expo-haptics": ">=14.0.0",
65
+ "expo-image-picker": ">=15.0.0",
58
66
  "expo-linear-gradient": ">=13.0.0",
59
67
  "expo-sensors": ">=13.0.0",
60
68
  "pressto": ">=0.6.0",
@@ -63,13 +71,13 @@
63
71
  "react-native-ease": ">=0.7.0",
64
72
  "react-native-gesture-handler": ">=2.0.0",
65
73
  "react-native-pulsar": ">=1.6.0",
66
- "react-native-reanimated": ">=4.0.0",
74
+ "react-native-reanimated": ">=4.0.0 <5.0.0",
67
75
  "react-native-safe-area-context": ">=4.0.0",
68
76
  "react-native-screens": ">=3.0.0",
69
77
  "react-native-size-matters": ">=0.4.0",
70
78
  "react-native-svg": ">=15.0.0",
71
- "react-native-worklets": ">=0.5.0",
72
- "sonner-native": ">=0.20.0"
79
+ "react-native-worklets": ">=0.5.1",
80
+ "sonner-native": ">=0.22.0"
73
81
  },
74
82
  "peerDependenciesMeta": {
75
83
  "@shopify/react-native-skia": {
@@ -80,6 +88,15 @@
80
88
  },
81
89
  "react-native-pulsar": {
82
90
  "optional": true
91
+ },
92
+ "expo-image-picker": {
93
+ "optional": true
94
+ },
95
+ "react-native-ease": {
96
+ "optional": false
97
+ },
98
+ "pressto": {
99
+ "optional": false
83
100
  }
84
101
  },
85
102
  "pnpm": {
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
- import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
- import { FontAwesome5, MaterialIcons, Entypo } from '@expo/vector-icons'
2
+ import { View, Text, StyleSheet, ViewStyle, TouchableOpacity } from 'react-native'
3
+ import { FontAwesome5, MaterialIcons, Entypo, Feather } from '@expo/vector-icons'
4
4
  import { useTheme } from '../../theme'
5
5
  import { s, vs, ms } from '../../utils/scaling'
6
6
  import { renderIcon } from '../../utils/icons'
@@ -15,10 +15,12 @@ export interface AlertBannerProps {
15
15
  icon?: React.ReactNode
16
16
  iconName?: string
17
17
  iconColor?: string
18
+ /** Called when the user taps the dismiss (×) button. If omitted, no button is shown. */
19
+ onDismiss?: () => void
18
20
  style?: ViewStyle
19
21
  }
20
22
 
21
- export function AlertBanner({ title, description, variant = 'default', icon, iconName, iconColor, style }: AlertBannerProps) {
23
+ export function AlertBanner({ title, description, variant = 'default', icon, iconName, iconColor, onDismiss, style }: AlertBannerProps) {
22
24
  const { colors, colorScheme } = useTheme()
23
25
 
24
26
  // Match Toast richColors appearance — saturated semantic colors
@@ -81,6 +83,17 @@ export function AlertBanner({ title, description, variant = 'default', icon, ico
81
83
  <Text style={[styles.description, { color: colors.foreground, opacity: 0.85 }]} allowFontScaling={true}>{description}</Text>
82
84
  ) : null}
83
85
  </View>
86
+ {onDismiss ? (
87
+ <TouchableOpacity
88
+ onPress={onDismiss}
89
+ style={styles.dismissButton}
90
+ activeOpacity={0.6}
91
+ accessibilityRole="button"
92
+ accessibilityLabel="Dismiss"
93
+ >
94
+ <Feather name="x" size={ms(16)} color={colors.foregroundMuted} />
95
+ </TouchableOpacity>
96
+ ) : null}
84
97
  </View>
85
98
  )
86
99
  }
@@ -109,4 +122,9 @@ const styles = StyleSheet.create({
109
122
  fontFamily: 'Sohne-Regular',
110
123
  fontSize: ms(12),
111
124
  },
125
+ dismissButton: {
126
+ padding: s(4),
127
+ marginTop: vs(-2),
128
+ marginRight: -s(4),
129
+ },
112
130
  })
@@ -19,6 +19,8 @@ export interface ConfirmDialogProps {
19
19
  confirmLabel?: string
20
20
  cancelLabel?: string
21
21
  confirmVariant?: 'primary' | 'destructive'
22
+ /** Show a loading spinner in the confirm button (e.g. while async action completes). */
23
+ loading?: boolean
22
24
  onConfirm: () => void
23
25
  onCancel: () => void
24
26
  }
@@ -30,6 +32,7 @@ export function ConfirmDialog({
30
32
  confirmLabel = 'Confirm',
31
33
  cancelLabel = 'Cancel',
32
34
  confirmVariant = 'primary',
35
+ loading = false,
33
36
  onConfirm,
34
37
  onCancel,
35
38
  }: ConfirmDialogProps) {
@@ -78,6 +81,8 @@ export function ConfirmDialog({
78
81
  label={confirmLabel}
79
82
  variant={confirmVariant}
80
83
  fullWidth
84
+ loading={loading}
85
+ disabled={loading}
81
86
  onPress={() => { notificationSuccess(); onConfirm() }}
82
87
  icon={
83
88
  <Feather
@@ -22,6 +22,8 @@ export interface CurrencyInputProps {
22
22
  editable?: boolean
23
23
  containerStyle?: ViewStyle
24
24
  style?: TextStyle
25
+ /** Use inside a Sheet/BottomSheet — passes sheetMode to the underlying Input. */
26
+ sheetMode?: boolean
25
27
  }
26
28
 
27
29
  function formatCurrency(raw: string, separator: '.' | ','): string {
@@ -44,6 +46,7 @@ export function CurrencyInput({
44
46
  editable,
45
47
  containerStyle,
46
48
  style,
49
+ sheetMode,
47
50
  }: CurrencyInputProps) {
48
51
  const handleChange = (text: string) => {
49
52
  const withoutPrefix = prefix && text.startsWith(prefix) ? text.slice(prefix.length) : text
@@ -83,6 +86,7 @@ export function CurrencyInput({
83
86
  containerStyle={containerStyle}
84
87
  inputWrapperStyle={isLarge ? { paddingVertical: vs(16), minHeight: 72 } : undefined}
85
88
  style={[inputStyle, style]}
89
+ sheetMode={sheetMode}
86
90
  />
87
91
  )
88
92
  }
@@ -0,0 +1,158 @@
1
+ import React from 'react'
2
+ import { View, Text, Image, StyleSheet, ViewStyle, Platform } from 'react-native'
3
+ import { Feather } from '@expo/vector-icons'
4
+ import { impactLight } from '../../utils/haptics'
5
+ import { useTheme } from '../../theme'
6
+ import { s, vs, ms } from '../../utils/scaling'
7
+ import { RADIUS } from '../../tokens'
8
+ import { Spinner } from '../Spinner'
9
+ import { PressableCard } from '../../utils/pressable'
10
+
11
+ export interface ImageUploadProps {
12
+ /** Current image URI. Pass null/undefined to show placeholder. */
13
+ value?: string | null
14
+ /** Called with the selected image URI. Receives null when image is cleared. */
15
+ onChange?: (uri: string | null) => void
16
+ /** Show a loading spinner over the image (e.g. while uploading). */
17
+ loading?: boolean
18
+ /** Text shown when no image is selected. */
19
+ placeholder?: string
20
+ /** Width of the upload area. Defaults to full width (undefined). */
21
+ width?: number
22
+ /** Height of the upload area. Defaults to 200. */
23
+ height?: number
24
+ /** Border radius of the upload area. Defaults to RADIUS.lg. */
25
+ borderRadius?: number
26
+ /** Aspect ratio for the selected image. Defaults to 'cover'. */
27
+ resizeMode?: 'cover' | 'contain' | 'stretch'
28
+ disabled?: boolean
29
+ style?: ViewStyle
30
+ accessibilityLabel?: string
31
+ }
32
+
33
+ export function ImageUpload({
34
+ value,
35
+ onChange,
36
+ loading = false,
37
+ placeholder = 'Tap to add image',
38
+ width,
39
+ height = 200,
40
+ borderRadius = RADIUS.lg,
41
+ resizeMode = 'cover',
42
+ disabled = false,
43
+ style,
44
+ accessibilityLabel,
45
+ }: ImageUploadProps) {
46
+ const { colors } = useTheme()
47
+
48
+ const handlePress = async () => {
49
+ if (disabled || loading) return
50
+ impactLight()
51
+
52
+ // Dynamic import so expo-image-picker is optional at module load time.
53
+ // Consumers who don't use ImageUpload never pull this dep.
54
+ let ImagePicker: typeof import('expo-image-picker')
55
+ try {
56
+ ImagePicker = await import('expo-image-picker')
57
+ } catch {
58
+ if (__DEV__) console.warn('[ImageUpload] expo-image-picker not installed. Add it as a dependency.')
59
+ return
60
+ }
61
+
62
+ if (Platform.OS !== 'web') {
63
+ const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync()
64
+ if (status !== 'granted') return
65
+ }
66
+
67
+ const result = await ImagePicker.launchImageLibraryAsync({
68
+ mediaTypes: ['images'],
69
+ allowsEditing: true,
70
+ quality: 0.8,
71
+ })
72
+
73
+ if (!result.canceled && result.assets[0]) {
74
+ onChange?.(result.assets[0].uri)
75
+ }
76
+ }
77
+
78
+ const containerStyle: ViewStyle = {
79
+ width,
80
+ height,
81
+ borderRadius,
82
+ borderWidth: value ? 0 : 1,
83
+ borderStyle: 'dashed',
84
+ borderColor: colors.border,
85
+ backgroundColor: value ? 'transparent' : colors.surface,
86
+ overflow: 'hidden',
87
+ }
88
+
89
+ return (
90
+ <PressableCard
91
+ onPress={handlePress}
92
+ enabled={!disabled && !loading}
93
+ rippleColor="transparent"
94
+ touchSoundDisabled
95
+ accessibilityRole="button"
96
+ accessibilityLabel={accessibilityLabel ?? (value ? 'Change image' : placeholder)}
97
+ accessibilityState={{ disabled: disabled || loading }}
98
+ style={[containerStyle, style]}
99
+ >
100
+ {value ? (
101
+ <Image
102
+ source={{ uri: value }}
103
+ style={[StyleSheet.absoluteFillObject, { borderRadius }]}
104
+ resizeMode={resizeMode}
105
+ />
106
+ ) : (
107
+ <View style={styles.placeholder}>
108
+ <Feather name="image" size={ms(28)} color={colors.foregroundMuted} />
109
+ <Text style={[styles.placeholderText, { color: colors.foregroundMuted }]} allowFontScaling={true}>
110
+ {placeholder}
111
+ </Text>
112
+ </View>
113
+ )}
114
+ {loading ? (
115
+ <View style={[styles.loadingOverlay, { backgroundColor: colors.overlay }]}>
116
+ <Spinner size="md" />
117
+ </View>
118
+ ) : null}
119
+ {value && !loading ? (
120
+ <View style={styles.editBadge} pointerEvents="none">
121
+ <View style={[styles.editBadgeInner, { backgroundColor: colors.overlay }]}>
122
+ <Feather name="edit-2" size={ms(12)} color="#fff" />
123
+ </View>
124
+ </View>
125
+ ) : null}
126
+ </PressableCard>
127
+ )
128
+ }
129
+
130
+ const styles = StyleSheet.create({
131
+ placeholder: {
132
+ flex: 1,
133
+ alignItems: 'center',
134
+ justifyContent: 'center',
135
+ gap: vs(8),
136
+ },
137
+ placeholderText: {
138
+ fontFamily: 'Sohne-Regular',
139
+ fontSize: ms(13),
140
+ },
141
+ loadingOverlay: {
142
+ ...StyleSheet.absoluteFillObject,
143
+ alignItems: 'center',
144
+ justifyContent: 'center',
145
+ },
146
+ editBadge: {
147
+ position: 'absolute',
148
+ bottom: vs(8),
149
+ right: s(8),
150
+ },
151
+ editBadgeInner: {
152
+ width: s(28),
153
+ height: s(28),
154
+ borderRadius: 999,
155
+ alignItems: 'center',
156
+ justifyContent: 'center',
157
+ },
158
+ })
@@ -0,0 +1 @@
1
+ export * from './ImageUpload'
@@ -1,16 +1,12 @@
1
1
  import React, { useState } from 'react'
2
2
  import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, TextStyle, TouchableOpacity, Platform } from 'react-native'
3
- import Animated, {
4
- useAnimatedStyle,
5
- interpolateColor,
6
- interpolate,
7
- } from 'react-native-reanimated'
3
+ import { BottomSheetTextInput } from '@gorhom/bottom-sheet'
4
+ import { EaseView } from 'react-native-ease'
8
5
  import { AntDesign } from '@expo/vector-icons'
9
6
  import { useTheme } from '../../theme'
10
7
  import { s, vs, ms } from '../../utils/scaling'
11
8
  import { renderIcon } from '../../utils/icons'
12
- import { useColorTransition } from '../../utils/useColorTransition'
13
- import { TIMINGS } from '../../utils/animations'
9
+ import { COLOR_TRANSITION } from '../../utils/animations'
14
10
 
15
11
  const webInputResetStyle: Record<string, unknown> =
16
12
  Platform.OS === 'web'
@@ -33,17 +29,15 @@ export interface InputProps extends TextInputProps {
33
29
  type?: 'text' | 'password'
34
30
  containerStyle?: ViewStyle
35
31
  inputWrapperStyle?: ViewStyle
32
+ /** Use inside a Sheet/BottomSheet — swaps TextInput for BottomSheetTextInput to fix keyboard handling. */
33
+ sheetMode?: boolean
36
34
  }
37
35
 
38
- export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyle, suffixStyle, prefixIcon, suffixIcon, prefixIconColor, suffixIconColor, type = 'text', containerStyle, inputWrapperStyle, style, onFocus, onBlur, secureTextEntry, editable, accessibilityLabel, ...props }: InputProps) {
36
+ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyle, suffixStyle, prefixIcon, suffixIcon, prefixIconColor, suffixIconColor, type = 'text', containerStyle, inputWrapperStyle, sheetMode = false, style, onFocus, onBlur, secureTextEntry, editable, accessibilityLabel, ...props }: InputProps) {
39
37
  const { colors } = useTheme()
40
38
  const [focused, setFocused] = useState(false)
41
39
  const [showPassword, setShowPassword] = useState(false)
42
40
 
43
- const focusProgress = useColorTransition(focused, {
44
- duration: focused ? TIMINGS.focusIn.duration : TIMINGS.focusOut.duration,
45
- })
46
-
47
41
  const isDisabled = disabled || editable === false
48
42
  const isPassword = type === 'password'
49
43
  const effectiveSecure = isPassword ? !showPassword : secureTextEntry
@@ -68,29 +62,23 @@ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyl
68
62
  ? renderIcon(suffixIcon, 20, suffixIconColor ?? colors.foregroundMuted)
69
63
  : suffix
70
64
 
71
- // Border drawn on an absolute overlay so the 1px→2px weight change never
72
- // resizes the layout box (which would reflow content / shift the interface).
73
- // Wrapper keeps borderWidth: 0; overlay grows inward and is non-interactive.
74
- const borderAnimStyle = useAnimatedStyle(() => ({
75
- borderColor: error
76
- ? colors.destructive
77
- : interpolateColor(focusProgress.value, [0, 1], [colors.border, colors.primary]),
78
- borderWidth: error
79
- ? 2
80
- : interpolate(focusProgress.value, [0, 1], [1, 2]),
81
- }))
65
+ // Border color animated via EaseView. Width is static (1px normal, 2px error)
66
+ // to avoid worklets 0.9.x interpolation issues with borderWidth.
67
+ const borderColor = error ? colors.destructive : (focused ? colors.primary : colors.border)
82
68
 
83
69
  return (
84
70
  <View style={[styles.container, isDisabled && styles.containerDisabled, containerStyle]}>
85
71
  {label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
86
- <Animated.View
72
+ <EaseView
87
73
  style={[
88
74
  styles.inputWrapper,
89
75
  { backgroundColor: isDisabled ? colors.surface : colors.background },
76
+ error && styles.inputWrapperError,
90
77
  inputWrapperStyle,
91
78
  ]}
79
+ animate={{ borderColor }}
80
+ transition={COLOR_TRANSITION}
92
81
  >
93
- <Animated.View style={[styles.borderOverlay, borderAnimStyle]} pointerEvents="none" />
94
82
  {effectivePrefix ? (
95
83
  typeof effectivePrefix === 'string' ? (
96
84
  <Text style={[styles.prefixText, { color: colors.foregroundMuted }, prefixStyle]} allowFontScaling={true}>
@@ -100,28 +88,53 @@ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyl
100
88
  <View style={styles.prefixContainer}>{effectivePrefix}</View>
101
89
  )
102
90
  ) : null}
103
- <TextInput
104
- style={[
105
- styles.input,
106
- { color: colors.foreground },
107
- webInputResetStyle,
108
- style,
109
- ]}
110
- onFocus={(e) => {
111
- setFocused(true)
112
- onFocus?.(e)
113
- }}
114
- onBlur={(e) => {
115
- setFocused(false)
116
- onBlur?.(e)
117
- }}
118
- placeholderTextColor={colors.foregroundMuted}
119
- allowFontScaling={true}
120
- secureTextEntry={effectiveSecure}
121
- editable={isDisabled ? false : editable}
122
- accessibilityLabel={accessibilityLabel ?? label}
123
- {...props}
124
- />
91
+ {sheetMode ? (
92
+ <BottomSheetTextInput
93
+ style={[
94
+ styles.input,
95
+ { color: colors.foreground },
96
+ webInputResetStyle as TextStyle,
97
+ style,
98
+ ]}
99
+ onFocus={(e) => {
100
+ setFocused(true)
101
+ onFocus?.(e)
102
+ }}
103
+ onBlur={(e) => {
104
+ setFocused(false)
105
+ onBlur?.(e)
106
+ }}
107
+ placeholderTextColor={colors.foregroundMuted}
108
+ allowFontScaling={true}
109
+ secureTextEntry={effectiveSecure}
110
+ editable={isDisabled ? false : editable}
111
+ accessibilityLabel={accessibilityLabel ?? label}
112
+ {...props}
113
+ />
114
+ ) : (
115
+ <TextInput
116
+ style={[
117
+ styles.input,
118
+ { color: colors.foreground },
119
+ webInputResetStyle,
120
+ style,
121
+ ]}
122
+ onFocus={(e) => {
123
+ setFocused(true)
124
+ onFocus?.(e)
125
+ }}
126
+ onBlur={(e) => {
127
+ setFocused(false)
128
+ onBlur?.(e)
129
+ }}
130
+ placeholderTextColor={colors.foregroundMuted}
131
+ allowFontScaling={true}
132
+ secureTextEntry={effectiveSecure}
133
+ editable={isDisabled ? false : editable}
134
+ accessibilityLabel={accessibilityLabel ?? label}
135
+ {...props}
136
+ />
137
+ )}
125
138
  {effectiveSuffix ? (
126
139
  typeof effectiveSuffix === 'string' ? (
127
140
  <Text style={[styles.suffixText, { color: colors.foregroundMuted }, suffixStyle]} allowFontScaling={true}>
@@ -131,7 +144,7 @@ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyl
131
144
  <View style={styles.suffixContainer}>{effectiveSuffix}</View>
132
145
  )
133
146
  ) : null}
134
- </Animated.View>
147
+ </EaseView>
135
148
  {error ? (
136
149
  <Text
137
150
  style={[styles.helperText, { color: colors.destructive }]}
@@ -162,16 +175,14 @@ const styles = StyleSheet.create({
162
175
  inputWrapper: {
163
176
  flexDirection: 'row',
164
177
  alignItems: 'center',
165
- // Border lives on borderOverlay (absolute) so its 1px→2px focus change
166
- // never resizes this box. Wrapper itself carries no border.
167
178
  borderRadius: 8,
179
+ borderWidth: 1,
168
180
  paddingHorizontal: s(14),
169
181
  paddingVertical: vs(11),
170
182
  minHeight: 48,
171
183
  },
172
- borderOverlay: {
173
- ...StyleSheet.absoluteFillObject,
174
- borderRadius: 8,
184
+ inputWrapperError: {
185
+ borderWidth: 2,
175
186
  },
176
187
  input: {
177
188
  fontFamily: 'Sohne-Regular',