@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.
- package/COMPONENTS.md +166 -4
- package/CONSUMER.md +247 -0
- package/DESIGN.md +668 -0
- package/FONTS.md +107 -0
- package/README.md +3 -3
- package/dist/AlertBanner.d.mts +3 -1
- package/dist/AlertBanner.d.ts +3 -1
- package/dist/AlertBanner.js +18 -2
- package/dist/AlertBanner.mjs +1 -1
- package/dist/ConfirmDialog.d.mts +3 -1
- package/dist/ConfirmDialog.d.ts +3 -1
- package/dist/ConfirmDialog.js +3 -0
- package/dist/ConfirmDialog.mjs +1 -1
- package/dist/CurrencyInput.d.mts +3 -1
- package/dist/CurrencyInput.d.ts +3 -1
- package/dist/CurrencyInput.js +52 -39
- package/dist/CurrencyInput.mjs +2 -3
- package/dist/ImageUpload.d.mts +27 -0
- package/dist/ImageUpload.d.ts +27 -0
- package/dist/ImageUpload.js +399 -0
- package/dist/ImageUpload.mjs +9 -0
- package/dist/Input.d.mts +3 -1
- package/dist/Input.d.ts +3 -1
- package/dist/Input.js +48 -37
- package/dist/Input.mjs +1 -2
- package/dist/ListItem.d.mts +9 -2
- package/dist/ListItem.d.ts +9 -2
- package/dist/ListItem.js +9 -2
- package/dist/ListItem.mjs +1 -1
- package/dist/SheetSelect.d.mts +25 -0
- package/dist/SheetSelect.d.ts +25 -0
- package/dist/SheetSelect.js +440 -0
- package/dist/SheetSelect.mjs +9 -0
- package/dist/Textarea.mjs +1 -2
- package/dist/{chunk-M6ZXVBTK.mjs → chunk-6MKGPAR2.mjs} +21 -5
- package/dist/{chunk-EH745HE5.mjs → chunk-CZCQZHG6.mjs} +13 -4
- package/dist/{chunk-7QHVVCB3.mjs → chunk-FZZLPJ6B.mjs} +3 -0
- package/dist/{chunk-MAC465BB.mjs → chunk-JUXSWN54.mjs} +5 -3
- package/dist/{chunk-BNP626TY.mjs → chunk-OHBNABL5.mjs} +10 -3
- package/dist/chunk-URI2WBIV.mjs +147 -0
- package/dist/chunk-Y4GL2MHX.mjs +112 -0
- package/dist/{chunk-756RAKE4.mjs → chunk-ZUR7AU5R.mjs} +38 -20
- package/dist/fonts.d.mts +32 -0
- package/dist/fonts.d.ts +32 -0
- package/dist/fonts.js +44 -0
- package/dist/fonts.mjs +37 -0
- package/dist/index.d.mts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +425 -106
- package/dist/index.mjs +55 -17
- package/package.json +23 -6
- package/src/components/AlertBanner/AlertBanner.tsx +21 -3
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +5 -0
- package/src/components/CurrencyInput/CurrencyInput.tsx +4 -0
- package/src/components/ImageUpload/ImageUpload.tsx +158 -0
- package/src/components/ImageUpload/index.ts +1 -0
- package/src/components/Input/Input.tsx +64 -53
- package/src/components/ListItem/ListItem.tsx +23 -4
- package/src/components/SheetSelect/SheetSelect.tsx +192 -0
- package/src/components/SheetSelect/index.ts +1 -0
- package/src/fonts.ts +30 -29
- package/src/hooks/useConfirmDialog.ts +67 -0
- package/src/index.ts +6 -0
- 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 {
|
|
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 {
|
|
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-
|
|
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 {
|
|
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 {
|
|
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-
|
|
42
|
+
export { ConfirmDialog } from './chunk-FZZLPJ6B.mjs';
|
|
39
43
|
export { CurrencyDisplay } from './chunk-BRKYVJVV.mjs';
|
|
40
|
-
export { CurrencyInput } from './chunk-
|
|
41
|
-
export { Input } from './chunk-
|
|
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-
|
|
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.
|
|
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":
|
|
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
|
-
"
|
|
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.
|
|
72
|
-
"sonner-native": ">=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
|
|
4
|
-
|
|
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 {
|
|
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
|
|
72
|
-
//
|
|
73
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
borderRadius: 8,
|
|
184
|
+
inputWrapperError: {
|
|
185
|
+
borderWidth: 2,
|
|
175
186
|
},
|
|
176
187
|
input: {
|
|
177
188
|
fontFamily: 'Sohne-Regular',
|