@retray-dev/ui-kit 9.0.0 → 9.2.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 +178 -7
- package/CONSUMER.md +247 -0
- package/DESIGN.md +668 -0
- package/EXAMPLES.md +19 -12
- 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 +31 -4
- package/dist/CurrencyInput.mjs +2 -2
- 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 +27 -2
- package/dist/Input.mjs +1 -1
- package/dist/ListItem.d.mts +3 -1
- package/dist/ListItem.d.ts +3 -1
- package/dist/ListItem.js +2 -1
- 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/{chunk-M6ZXVBTK.mjs → chunk-6MKGPAR2.mjs} +21 -5
- package/dist/{chunk-7QHVVCB3.mjs → chunk-FZZLPJ6B.mjs} +3 -0
- package/dist/{chunk-MAC465BB.mjs → chunk-KNSENOV4.mjs} +5 -3
- package/dist/{chunk-756RAKE4.mjs → chunk-LVYEU5ZK.mjs} +27 -2
- package/dist/{chunk-BNP626TY.mjs → chunk-T4I5WVHA.mjs} +2 -1
- package/dist/chunk-URI2WBIV.mjs +147 -0
- package/dist/chunk-Y4GL2MHX.mjs +112 -0
- package/dist/index.d.mts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +327 -8
- package/dist/index.mjs +51 -12
- package/package.json +18 -5
- 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 +51 -23
- package/src/components/ListItem/ListItem.tsx +4 -1
- package/src/components/SheetSelect/SheetSelect.tsx +192 -0
- package/src/components/SheetSelect/index.ts +1 -0
- package/src/hooks/useConfirmDialog.ts +67 -0
- package/src/index.ts +6 -0
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, TextStyle, TouchableOpacity, Platform } from 'react-native'
|
|
3
|
+
import { BottomSheetTextInput } from '@gorhom/bottom-sheet'
|
|
3
4
|
import Animated, {
|
|
4
5
|
useAnimatedStyle,
|
|
5
6
|
interpolateColor,
|
|
@@ -33,9 +34,11 @@ export interface InputProps extends TextInputProps {
|
|
|
33
34
|
type?: 'text' | 'password'
|
|
34
35
|
containerStyle?: ViewStyle
|
|
35
36
|
inputWrapperStyle?: ViewStyle
|
|
37
|
+
/** Use inside a Sheet/BottomSheet — swaps TextInput for BottomSheetTextInput to fix keyboard handling. */
|
|
38
|
+
sheetMode?: boolean
|
|
36
39
|
}
|
|
37
40
|
|
|
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) {
|
|
41
|
+
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
42
|
const { colors } = useTheme()
|
|
40
43
|
const [focused, setFocused] = useState(false)
|
|
41
44
|
const [showPassword, setShowPassword] = useState(false)
|
|
@@ -100,28 +103,53 @@ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyl
|
|
|
100
103
|
<View style={styles.prefixContainer}>{effectivePrefix}</View>
|
|
101
104
|
)
|
|
102
105
|
) : null}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
106
|
+
{sheetMode ? (
|
|
107
|
+
<BottomSheetTextInput
|
|
108
|
+
style={[
|
|
109
|
+
styles.input,
|
|
110
|
+
{ color: colors.foreground },
|
|
111
|
+
webInputResetStyle as TextStyle,
|
|
112
|
+
style,
|
|
113
|
+
]}
|
|
114
|
+
onFocus={(e) => {
|
|
115
|
+
setFocused(true)
|
|
116
|
+
onFocus?.(e)
|
|
117
|
+
}}
|
|
118
|
+
onBlur={(e) => {
|
|
119
|
+
setFocused(false)
|
|
120
|
+
onBlur?.(e)
|
|
121
|
+
}}
|
|
122
|
+
placeholderTextColor={colors.foregroundMuted}
|
|
123
|
+
allowFontScaling={true}
|
|
124
|
+
secureTextEntry={effectiveSecure}
|
|
125
|
+
editable={isDisabled ? false : editable}
|
|
126
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
127
|
+
{...props}
|
|
128
|
+
/>
|
|
129
|
+
) : (
|
|
130
|
+
<TextInput
|
|
131
|
+
style={[
|
|
132
|
+
styles.input,
|
|
133
|
+
{ color: colors.foreground },
|
|
134
|
+
webInputResetStyle,
|
|
135
|
+
style,
|
|
136
|
+
]}
|
|
137
|
+
onFocus={(e) => {
|
|
138
|
+
setFocused(true)
|
|
139
|
+
onFocus?.(e)
|
|
140
|
+
}}
|
|
141
|
+
onBlur={(e) => {
|
|
142
|
+
setFocused(false)
|
|
143
|
+
onBlur?.(e)
|
|
144
|
+
}}
|
|
145
|
+
placeholderTextColor={colors.foregroundMuted}
|
|
146
|
+
allowFontScaling={true}
|
|
147
|
+
secureTextEntry={effectiveSecure}
|
|
148
|
+
editable={isDisabled ? false : editable}
|
|
149
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
150
|
+
{...props}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
125
153
|
{effectiveSuffix ? (
|
|
126
154
|
typeof effectiveSuffix === 'string' ? (
|
|
127
155
|
<Text style={[styles.suffixText, { color: colors.foregroundMuted }, suffixStyle]} allowFontScaling={true}>
|
|
@@ -72,6 +72,8 @@ export interface ListItemProps {
|
|
|
72
72
|
titleStyle?: TextStyle
|
|
73
73
|
/** Style applied to the subtitle Text. */
|
|
74
74
|
subtitleStyle?: TextStyle
|
|
75
|
+
/** Max lines for the subtitle. Defaults to 2. */
|
|
76
|
+
subtitleNumberOfLines?: number
|
|
75
77
|
/** Style applied to the caption Text. */
|
|
76
78
|
captionStyle?: TextStyle
|
|
77
79
|
/** Accessibility label override. Defaults to the title. */
|
|
@@ -98,6 +100,7 @@ function ListItemBase({
|
|
|
98
100
|
style,
|
|
99
101
|
titleStyle,
|
|
100
102
|
subtitleStyle,
|
|
103
|
+
subtitleNumberOfLines = 2,
|
|
101
104
|
captionStyle,
|
|
102
105
|
accessibilityLabel,
|
|
103
106
|
}: ListItemProps) {
|
|
@@ -150,7 +153,7 @@ function ListItemBase({
|
|
|
150
153
|
{subtitle ? (
|
|
151
154
|
<Text
|
|
152
155
|
style={[styles.subtitle, { color: colors.foregroundMuted }, subtitleStyle]}
|
|
153
|
-
numberOfLines={
|
|
156
|
+
numberOfLines={subtitleNumberOfLines}
|
|
154
157
|
allowFontScaling={true}
|
|
155
158
|
>
|
|
156
159
|
{subtitle}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, ScrollView, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
+
import { EaseView } from 'react-native-ease'
|
|
4
|
+
import { selectionAsync as hapticSelection } from '../../utils/haptics'
|
|
5
|
+
import { useTheme } from '../../theme'
|
|
6
|
+
import { s, vs, ms, mvs } from '../../utils/scaling'
|
|
7
|
+
import { renderIcon } from '../../utils/icons'
|
|
8
|
+
import { COLOR_TRANSITION } from '../../utils/animations'
|
|
9
|
+
import { PressableChip } from '../../utils/pressable'
|
|
10
|
+
import { RADIUS } from '../../tokens'
|
|
11
|
+
|
|
12
|
+
export interface SheetSelectOption {
|
|
13
|
+
label: string
|
|
14
|
+
value: string | number
|
|
15
|
+
iconName?: string
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SheetSelectProps {
|
|
20
|
+
options: SheetSelectOption[]
|
|
21
|
+
value?: string | number | (string | number)[]
|
|
22
|
+
onValueChange?: (value: string | number | (string | number)[]) => void
|
|
23
|
+
/** Allow multiple simultaneous selections. Defaults to false (radio). */
|
|
24
|
+
multiSelect?: boolean
|
|
25
|
+
label?: string
|
|
26
|
+
error?: string
|
|
27
|
+
/** Wrap chips into multiple rows instead of a single horizontal scroll. Defaults to false. */
|
|
28
|
+
wrap?: boolean
|
|
29
|
+
style?: ViewStyle
|
|
30
|
+
accessibilityLabel?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function SheetSelectChip({
|
|
34
|
+
option,
|
|
35
|
+
selected,
|
|
36
|
+
onPress,
|
|
37
|
+
}: {
|
|
38
|
+
option: SheetSelectOption
|
|
39
|
+
selected: boolean
|
|
40
|
+
onPress: () => void
|
|
41
|
+
}) {
|
|
42
|
+
const { colors } = useTheme()
|
|
43
|
+
|
|
44
|
+
const handlePress = () => {
|
|
45
|
+
hapticSelection()
|
|
46
|
+
onPress()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const iconColor = selected ? colors.primaryForeground : colors.foreground
|
|
50
|
+
const resolvedIcon = option.iconName ? renderIcon(option.iconName, ms(13), iconColor) : null
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<PressableChip
|
|
54
|
+
onPress={option.disabled ? undefined : handlePress}
|
|
55
|
+
rippleColor="transparent"
|
|
56
|
+
touchSoundDisabled
|
|
57
|
+
accessibilityRole="button"
|
|
58
|
+
accessibilityLabel={option.disabled ? `${option.label}, unavailable` : option.label}
|
|
59
|
+
accessibilityState={{ selected, disabled: option.disabled }}
|
|
60
|
+
>
|
|
61
|
+
<EaseView
|
|
62
|
+
style={[styles.chip, option.disabled && styles.chipDisabled]}
|
|
63
|
+
animate={{
|
|
64
|
+
backgroundColor: selected ? colors.primary : colors.surface,
|
|
65
|
+
borderColor: selected ? colors.primary : colors.border,
|
|
66
|
+
}}
|
|
67
|
+
transition={COLOR_TRANSITION}
|
|
68
|
+
>
|
|
69
|
+
{resolvedIcon ? <View style={styles.chipIcon}>{resolvedIcon}</View> : null}
|
|
70
|
+
<Text
|
|
71
|
+
style={[styles.chipLabel, { color: selected ? colors.primaryForeground : colors.foreground }]}
|
|
72
|
+
allowFontScaling={true}
|
|
73
|
+
>
|
|
74
|
+
{option.label}
|
|
75
|
+
</Text>
|
|
76
|
+
</EaseView>
|
|
77
|
+
</PressableChip>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function SheetSelect({
|
|
82
|
+
options,
|
|
83
|
+
value,
|
|
84
|
+
onValueChange,
|
|
85
|
+
multiSelect = false,
|
|
86
|
+
label,
|
|
87
|
+
error,
|
|
88
|
+
wrap = false,
|
|
89
|
+
style,
|
|
90
|
+
accessibilityLabel,
|
|
91
|
+
}: SheetSelectProps) {
|
|
92
|
+
const { colors } = useTheme()
|
|
93
|
+
|
|
94
|
+
const isSelected = (optionValue: string | number): boolean => {
|
|
95
|
+
if (Array.isArray(value)) return value.includes(optionValue)
|
|
96
|
+
return optionValue === value
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const handlePress = (optionValue: string | number) => {
|
|
100
|
+
if (!multiSelect) {
|
|
101
|
+
onValueChange?.(optionValue)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
const currentArray = Array.isArray(value) ? value : value != null ? [value] : []
|
|
105
|
+
const alreadySelected = currentArray.includes(optionValue)
|
|
106
|
+
const newArray: (string | number)[] = alreadySelected
|
|
107
|
+
? currentArray.filter((v) => v !== optionValue)
|
|
108
|
+
: [...currentArray, optionValue]
|
|
109
|
+
onValueChange?.(newArray)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const chips = options.map((opt) => (
|
|
113
|
+
<SheetSelectChip
|
|
114
|
+
key={opt.value}
|
|
115
|
+
option={opt}
|
|
116
|
+
selected={isSelected(opt.value)}
|
|
117
|
+
onPress={() => handlePress(opt.value)}
|
|
118
|
+
/>
|
|
119
|
+
))
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<View style={[styles.container, style]} accessibilityLabel={accessibilityLabel}>
|
|
123
|
+
{label ? (
|
|
124
|
+
<Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>
|
|
125
|
+
{label}
|
|
126
|
+
</Text>
|
|
127
|
+
) : null}
|
|
128
|
+
{wrap ? (
|
|
129
|
+
<View style={styles.wrapContainer}>{chips}</View>
|
|
130
|
+
) : (
|
|
131
|
+
<ScrollView
|
|
132
|
+
horizontal
|
|
133
|
+
showsHorizontalScrollIndicator={false}
|
|
134
|
+
contentContainerStyle={styles.scrollContent}
|
|
135
|
+
>
|
|
136
|
+
{chips}
|
|
137
|
+
</ScrollView>
|
|
138
|
+
)}
|
|
139
|
+
{error ? (
|
|
140
|
+
<Text style={[styles.error, { color: colors.destructive }]} allowFontScaling={true} accessibilityLiveRegion="polite">
|
|
141
|
+
{error}
|
|
142
|
+
</Text>
|
|
143
|
+
) : null}
|
|
144
|
+
</View>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const styles = StyleSheet.create({
|
|
149
|
+
container: {
|
|
150
|
+
gap: vs(8),
|
|
151
|
+
},
|
|
152
|
+
label: {
|
|
153
|
+
fontFamily: 'Sohne-Medium',
|
|
154
|
+
fontSize: ms(14),
|
|
155
|
+
},
|
|
156
|
+
scrollContent: {
|
|
157
|
+
flexDirection: 'row',
|
|
158
|
+
gap: s(8),
|
|
159
|
+
},
|
|
160
|
+
wrapContainer: {
|
|
161
|
+
flexDirection: 'row',
|
|
162
|
+
flexWrap: 'wrap',
|
|
163
|
+
gap: s(8),
|
|
164
|
+
},
|
|
165
|
+
chip: {
|
|
166
|
+
borderRadius: RADIUS.full,
|
|
167
|
+
paddingHorizontal: s(14),
|
|
168
|
+
paddingVertical: vs(10),
|
|
169
|
+
minHeight: 44,
|
|
170
|
+
borderWidth: 1,
|
|
171
|
+
alignItems: 'center',
|
|
172
|
+
justifyContent: 'center',
|
|
173
|
+
flexDirection: 'row',
|
|
174
|
+
gap: s(5),
|
|
175
|
+
},
|
|
176
|
+
chipDisabled: {
|
|
177
|
+
opacity: 0.4,
|
|
178
|
+
},
|
|
179
|
+
chipIcon: {
|
|
180
|
+
alignItems: 'center',
|
|
181
|
+
justifyContent: 'center',
|
|
182
|
+
},
|
|
183
|
+
chipLabel: {
|
|
184
|
+
fontFamily: 'Sohne-Medium',
|
|
185
|
+
fontSize: ms(13),
|
|
186
|
+
lineHeight: mvs(18),
|
|
187
|
+
},
|
|
188
|
+
error: {
|
|
189
|
+
fontFamily: 'Sohne-Regular',
|
|
190
|
+
fontSize: ms(13),
|
|
191
|
+
},
|
|
192
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SheetSelect'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface UseConfirmDialogOptions {
|
|
4
|
+
onConfirm: () => void | Promise<void>
|
|
5
|
+
onCancel?: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface UseConfirmDialogResult<T> {
|
|
9
|
+
/** Pass to ConfirmDialog `visible` prop. */
|
|
10
|
+
visible: boolean
|
|
11
|
+
/** The value passed to `open()` — available during the confirmation flow. */
|
|
12
|
+
target: T | null
|
|
13
|
+
/** Whether `onConfirm` is currently executing. Pass to ConfirmDialog `loading` prop. */
|
|
14
|
+
loading: boolean
|
|
15
|
+
/** Open the dialog, optionally with an associated value (e.g. the item to delete). */
|
|
16
|
+
open: (target?: T) => void
|
|
17
|
+
/** Handlers to pass directly to ConfirmDialog. */
|
|
18
|
+
dialogProps: {
|
|
19
|
+
visible: boolean
|
|
20
|
+
loading: boolean
|
|
21
|
+
onConfirm: () => void
|
|
22
|
+
onCancel: () => void
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useConfirmDialog<T = undefined>(
|
|
27
|
+
options: UseConfirmDialogOptions,
|
|
28
|
+
): UseConfirmDialogResult<T> {
|
|
29
|
+
const [visible, setVisible] = useState(false)
|
|
30
|
+
const [target, setTarget] = useState<T | null>(null)
|
|
31
|
+
const [loading, setLoading] = useState(false)
|
|
32
|
+
|
|
33
|
+
const open = useCallback((t?: T) => {
|
|
34
|
+
setTarget(t ?? null)
|
|
35
|
+
setVisible(true)
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
const handleConfirm = useCallback(async () => {
|
|
39
|
+
setLoading(true)
|
|
40
|
+
try {
|
|
41
|
+
await options.onConfirm()
|
|
42
|
+
} finally {
|
|
43
|
+
setLoading(false)
|
|
44
|
+
setVisible(false)
|
|
45
|
+
setTarget(null)
|
|
46
|
+
}
|
|
47
|
+
}, [options])
|
|
48
|
+
|
|
49
|
+
const handleCancel = useCallback(() => {
|
|
50
|
+
setVisible(false)
|
|
51
|
+
setTarget(null)
|
|
52
|
+
options.onCancel?.()
|
|
53
|
+
}, [options])
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
visible,
|
|
57
|
+
target,
|
|
58
|
+
loading,
|
|
59
|
+
open,
|
|
60
|
+
dialogProps: {
|
|
61
|
+
visible,
|
|
62
|
+
loading,
|
|
63
|
+
onConfirm: handleConfirm,
|
|
64
|
+
onCancel: handleCancel,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -53,6 +53,8 @@ export * from './components/SelectableGrid'
|
|
|
53
53
|
export * from './components/PricingCard'
|
|
54
54
|
export * from './components/TabBar'
|
|
55
55
|
export * from './components/ImageViewer'
|
|
56
|
+
export * from './components/SheetSelect'
|
|
57
|
+
export * from './components/ImageUpload'
|
|
56
58
|
// HolographicCard is intentionally NOT re-exported here — it depends on the
|
|
57
59
|
// optional peer @shopify/react-native-skia, so it must stay out of the main
|
|
58
60
|
// barrel's module graph. Deep-import it: '@retray-dev/ui-kit/HolographicCard'.
|
|
@@ -76,6 +78,10 @@ export {
|
|
|
76
78
|
richHaptics,
|
|
77
79
|
} from './utils/haptics'
|
|
78
80
|
|
|
81
|
+
// Hooks
|
|
82
|
+
export { useConfirmDialog } from './hooks/useConfirmDialog'
|
|
83
|
+
export type { UseConfirmDialogOptions, UseConfirmDialogResult } from './hooks/useConfirmDialog'
|
|
84
|
+
|
|
79
85
|
// Design tokens
|
|
80
86
|
export {
|
|
81
87
|
SPACING,
|