cdslibrary 1.2.88 → 1.2.90
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/CDSBottomSheet.jsx +64 -26
- package/components/CDSButton.jsx +4 -33
- package/components/CDSButtonGroup.jsx +1 -1
- package/components/CDSCarousel.jsx +1 -1
- package/components/CDSImageButtonGroup.jsx +2 -2
- package/components/CDSInput.jsx +29 -10
- package/components/CDSLoader.jsx +2 -2
- package/components/CDSSelect.jsx +108 -109
- package/package.json +1 -1
|
@@ -26,8 +26,10 @@ const bottomSheetRender = ({
|
|
|
26
26
|
customSlot,
|
|
27
27
|
primaryButtonLabel = "Aceptar",
|
|
28
28
|
primaryButtonOnPress,
|
|
29
|
+
secondaryButtonOnPress,
|
|
29
30
|
secondaryButtonLabel,
|
|
30
31
|
onFinish,
|
|
32
|
+
closeOnSecondaryPress = true,
|
|
31
33
|
}, ref) => {
|
|
32
34
|
const { theme } = useTheme();
|
|
33
35
|
const isMobile = theme.isMobile;
|
|
@@ -36,16 +38,28 @@ const bottomSheetRender = ({
|
|
|
36
38
|
const translation = useSharedValue(startPos);
|
|
37
39
|
const opacity = useSharedValue(0);
|
|
38
40
|
|
|
39
|
-
const
|
|
41
|
+
const [contentHeight, setContentHeight] = React.useState(0);
|
|
42
|
+
const [layoutHeight, setLayoutHeight] = React.useState(0);
|
|
43
|
+
|
|
44
|
+
// Comprobamos si el contenido desborda el contenedor
|
|
45
|
+
const isScrollable = contentHeight > layoutHeight;
|
|
46
|
+
|
|
47
|
+
const triggerOnFinish = useCallback((wasActionAlreadyDone = false) => {
|
|
40
48
|
if (onFinish) onFinish();
|
|
41
|
-
if (primaryButtonOnPress) primaryButtonOnPress();
|
|
42
|
-
}, [onFinish, primaryButtonOnPress]);
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
// SI NO se ha ejecutado la acción (se cerró por X, gesto o backdrop)
|
|
51
|
+
// Y SI NO hay botón secundario (es un modal de botón único)
|
|
52
|
+
if (!wasActionAlreadyDone && !secondaryButtonLabel && primaryButtonOnPress) {
|
|
53
|
+
primaryButtonOnPress();
|
|
54
|
+
}
|
|
55
|
+
}, [onFinish, secondaryButtonLabel, primaryButtonOnPress]);
|
|
56
|
+
|
|
57
|
+
// 2. handleClose recibe el parámetro y lo pasa al JS
|
|
58
|
+
const handleClose = useCallback((wasActionAlreadyDone = false) => {
|
|
45
59
|
"worklet";
|
|
46
60
|
translation.value = withTiming(startPos, { duration: 300 }, (finished) => {
|
|
47
61
|
if (finished) {
|
|
48
|
-
runOnJS(triggerOnFinish)();
|
|
62
|
+
runOnJS(triggerOnFinish)(wasActionAlreadyDone);
|
|
49
63
|
}
|
|
50
64
|
});
|
|
51
65
|
opacity.value = withTiming(0, { duration: 300 });
|
|
@@ -86,7 +100,7 @@ const bottomSheetRender = ({
|
|
|
86
100
|
}
|
|
87
101
|
}, [isVisible, startPos]); // Asegúrate de incluir startPos
|
|
88
102
|
|
|
89
|
-
const animatedStyle = useAnimatedStyle(() => {
|
|
103
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
90
104
|
return {
|
|
91
105
|
opacity: opacity.value,
|
|
92
106
|
// Ocultamos el layout completamente si terminó de salir
|
|
@@ -120,11 +134,11 @@ const animatedStyle = useAnimatedStyle(() => {
|
|
|
120
134
|
<Animated.View
|
|
121
135
|
style={[
|
|
122
136
|
styles.overlay,
|
|
123
|
-
{pointerEvents: isVisible ? "auto" : "none", backgroundColor: theme.surface.special.overlay },
|
|
137
|
+
{ pointerEvents: isVisible ? "auto" : "none", backgroundColor: theme.surface.special.overlay },
|
|
124
138
|
backdropStyle
|
|
125
139
|
]}
|
|
126
140
|
>
|
|
127
|
-
<Pressable onPress={() => runOnJS(handleClose)()} style={{ flex: 1 }} />
|
|
141
|
+
<Pressable onPress={() => runOnJS(handleClose)(false)} style={{ flex: 1 }} />
|
|
128
142
|
</Animated.View>
|
|
129
143
|
<Animated.View
|
|
130
144
|
style={[
|
|
@@ -141,7 +155,6 @@ const animatedStyle = useAnimatedStyle(() => {
|
|
|
141
155
|
} : {
|
|
142
156
|
borderBottomLeftRadius: theme.radius.lg,
|
|
143
157
|
borderTopLeftRadius: theme.radius.lg,
|
|
144
|
-
paddingBottom: theme.space.md,
|
|
145
158
|
})
|
|
146
159
|
}
|
|
147
160
|
]}
|
|
@@ -166,42 +179,65 @@ const animatedStyle = useAnimatedStyle(() => {
|
|
|
166
179
|
|
|
167
180
|
{!!title && <Text style={[theme.typography.h3, { marginHorizontal: theme.space.md }]}>{title}</Text>}
|
|
168
181
|
|
|
169
|
-
<View style={styles.scrollWrapper}>
|
|
182
|
+
<View style={styles.scrollWrapper} onLayout={(e) => setLayoutHeight(e.nativeEvent.layout.height)}>
|
|
170
183
|
<GestureDetector gesture={Gesture.Simultaneous(nativeGesture)}>
|
|
171
184
|
<ScrollView
|
|
172
185
|
ref={ref}
|
|
173
186
|
style={styles.scrollArea}
|
|
174
|
-
|
|
187
|
+
scrollEnabled={isScrollable} // Desactiva el scroll si no es necesario
|
|
188
|
+
onContentSizeChange={(_, h) => setContentHeight(h)}
|
|
189
|
+
contentContainerStyle={[styles.scrollContent, {}]} // Tu padding original
|
|
175
190
|
showsVerticalScrollIndicator={true}
|
|
176
191
|
bounces={true}
|
|
177
192
|
nestedScrollEnabled={true}
|
|
178
193
|
scrollEventThrottle={16}
|
|
179
194
|
>
|
|
180
195
|
{!!description && (
|
|
181
|
-
<Text style={[theme.typography.regular.md, {
|
|
196
|
+
<Text style={[theme.typography.regular.md, { paddingHorizontal: theme.space.md }]}>
|
|
182
197
|
{description}
|
|
183
198
|
</Text>
|
|
184
199
|
)}
|
|
185
200
|
{customSlot}
|
|
186
201
|
</ScrollView>
|
|
187
202
|
</GestureDetector>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
203
|
+
{isScrollable && (
|
|
204
|
+
<LinearGradient
|
|
205
|
+
colors={['transparent', theme.surface.neutral.primary]}
|
|
206
|
+
style={[styles.fadeGradient, {
|
|
207
|
+
pointerEvents: 'none',
|
|
208
|
+
marginLeft: !isMobile && theme.space.md,
|
|
209
|
+
borderBottomLeftRadius: !isMobile && 2
|
|
210
|
+
}]}
|
|
211
|
+
/>
|
|
212
|
+
)}
|
|
192
213
|
</View>
|
|
193
214
|
|
|
194
215
|
{type !== "informative" && (
|
|
195
|
-
<View style={[isMobile ? styles.actionsContainer.typeBottomSheet : styles.actionsContainer.typeDrawer, {
|
|
216
|
+
<View style={[isMobile ? styles.actionsContainer.typeBottomSheet : styles.actionsContainer.typeDrawer, { margin: theme.space.md }]}>
|
|
217
|
+
{/* BOTÓN PRIMARIO */}
|
|
196
218
|
<CDSButton
|
|
197
219
|
label={primaryButtonLabel}
|
|
198
|
-
onPress={() =>
|
|
220
|
+
onPress={() => {
|
|
221
|
+
if (primaryButtonOnPress) primaryButtonOnPress();
|
|
222
|
+
runOnJS(handleClose)(true); // <--- TRUE: Acción ya hecha
|
|
223
|
+
}}
|
|
199
224
|
/>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
225
|
+
|
|
226
|
+
{/* BOTÓN SECUNDARIO (si existe) */}
|
|
227
|
+
{secondaryButtonLabel && (
|
|
228
|
+
<CDSButton
|
|
229
|
+
label={secondaryButtonLabel}
|
|
230
|
+
type="ghost"
|
|
231
|
+
onPress={() => {
|
|
232
|
+
if (secondaryButtonOnPress) secondaryButtonOnPress();
|
|
233
|
+
|
|
234
|
+
// Solo cerramos si no nos dijeron lo contrario
|
|
235
|
+
if (closeOnSecondaryPress) {
|
|
236
|
+
runOnJS(handleClose)(true);
|
|
237
|
+
}
|
|
238
|
+
}}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
205
241
|
</View>
|
|
206
242
|
)}
|
|
207
243
|
</Animated.View>
|
|
@@ -249,27 +285,29 @@ const styles = StyleSheet.create({
|
|
|
249
285
|
width: '100%',
|
|
250
286
|
},
|
|
251
287
|
scrollArea: {
|
|
288
|
+
|
|
252
289
|
width: '100%',
|
|
253
290
|
},
|
|
254
291
|
scrollContent: {
|
|
255
292
|
gap: 16,
|
|
293
|
+
|
|
256
294
|
},
|
|
257
295
|
fadeGradient: {
|
|
258
296
|
position: 'absolute',
|
|
259
297
|
bottom: 0,
|
|
260
298
|
left: 0,
|
|
261
299
|
right: 0,
|
|
262
|
-
height:
|
|
300
|
+
height: 32,
|
|
263
301
|
},
|
|
264
302
|
actionsContainer: {
|
|
265
303
|
typeBottomSheet: {
|
|
266
304
|
flexDirection: "column",
|
|
267
|
-
width: "
|
|
305
|
+
width: "auto",
|
|
268
306
|
gap: 8,
|
|
269
307
|
},
|
|
270
308
|
typeDrawer: {
|
|
271
309
|
flexDirection: "row-reverse",
|
|
272
|
-
width: "
|
|
310
|
+
width: "auto",
|
|
273
311
|
gap: 8,
|
|
274
312
|
marginTop: 'auto',
|
|
275
313
|
},
|
package/components/CDSButton.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useRef, useEffect } from "react";
|
|
2
|
-
import { Text, StyleSheet, TouchableOpacity, Animated } from "react-native";
|
|
2
|
+
import { Text, StyleSheet, TouchableOpacity, Animated, View } from "react-native";
|
|
3
3
|
import { MaterialIcons } from "@expo/vector-icons";
|
|
4
4
|
import { useTheme } from "../context/CDSThemeContext";
|
|
5
5
|
|
|
@@ -15,34 +15,6 @@ export const CDSButton = ({
|
|
|
15
15
|
const { theme } = useTheme();
|
|
16
16
|
const isMobile = theme.isMobile;
|
|
17
17
|
|
|
18
|
-
// 1. Referencia de animación
|
|
19
|
-
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
20
|
-
|
|
21
|
-
// 2. Disparar entrada al montar
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
Animated.timing(animatedValue, {
|
|
24
|
-
toValue: 1,
|
|
25
|
-
duration: 400,
|
|
26
|
-
useNativeDriver: false // Para manejar maxHeight y otros layouts
|
|
27
|
-
}).start();
|
|
28
|
-
}, []);
|
|
29
|
-
|
|
30
|
-
// 3. Estructura de animatedLayout unificada
|
|
31
|
-
const animatedLayout = {
|
|
32
|
-
opacity: animatedValue,
|
|
33
|
-
transform: [{
|
|
34
|
-
scale: animatedValue.interpolate({
|
|
35
|
-
inputRange: [0, 1],
|
|
36
|
-
outputRange: [0.95, 1]
|
|
37
|
-
})
|
|
38
|
-
}],
|
|
39
|
-
// Permite que el botón aparezca empujando suavemente
|
|
40
|
-
maxHeight: animatedValue.interpolate({
|
|
41
|
-
inputRange: [0, 1],
|
|
42
|
-
outputRange: [0, (variant === "fill" || isMobile ? 48 : 56)]
|
|
43
|
-
}),
|
|
44
|
-
};
|
|
45
|
-
|
|
46
18
|
const backgroundColor =
|
|
47
19
|
type === "disabled"
|
|
48
20
|
? theme.surface.action.disabled
|
|
@@ -58,11 +30,10 @@ export const CDSButton = ({
|
|
|
58
30
|
: theme.text.neutral.contrast;
|
|
59
31
|
|
|
60
32
|
return (
|
|
61
|
-
<
|
|
62
|
-
animatedLayout,
|
|
33
|
+
<View style={[
|
|
63
34
|
{
|
|
64
35
|
overflow: 'hidden',
|
|
65
|
-
alignSelf: (variant === "fill" || isMobile) ? "stretch" : "
|
|
36
|
+
alignSelf: (variant === "fill" || isMobile) ? "stretch" : "",
|
|
66
37
|
flexGrow: (variant === "fill" || isMobile ? 1 : 0),
|
|
67
38
|
}
|
|
68
39
|
]}>
|
|
@@ -113,7 +84,7 @@ export const CDSButton = ({
|
|
|
113
84
|
/>
|
|
114
85
|
)}
|
|
115
86
|
</TouchableOpacity>
|
|
116
|
-
</
|
|
87
|
+
</View>
|
|
117
88
|
);
|
|
118
89
|
};
|
|
119
90
|
|
|
@@ -24,7 +24,7 @@ export const CDSButtonGroup = ({ options, onSelect, label = 'Label', selected =
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<View style={[styles.container, { gap: theme.space.sm }]}>
|
|
27
|
+
<View style={[styles.container, { gap: theme.space.sm, }]}>
|
|
28
28
|
<Text style={[theme.typography.label]}>{label}</Text>
|
|
29
29
|
<View style={[styles.actionsContainer, { gap: theme.space.sm }]}>
|
|
30
30
|
{options.map((option, index) => (
|
|
@@ -29,7 +29,7 @@ export const CDSCarousel = ({ activeIndex, scrollViewRef, onScroll, images }) =>
|
|
|
29
29
|
onLayout={onLayout}
|
|
30
30
|
>
|
|
31
31
|
{images.map((image, index) => (
|
|
32
|
-
<View key={index} style={{ width: scrollViewWidth, justifyContent: 'center', alignItems: 'center',
|
|
32
|
+
<View key={index} style={{ width: scrollViewWidth, justifyContent: 'center', alignItems: 'center',}}>
|
|
33
33
|
<Image
|
|
34
34
|
source={image}
|
|
35
35
|
style={{
|
|
@@ -35,7 +35,7 @@ export const CDSImageButtonGroup = ({ array, onSelect, isCentered }) => {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
|
-
<
|
|
38
|
+
<View style={[
|
|
39
39
|
animatedLayout,
|
|
40
40
|
{ width: '100%' } // Asegura que el contenedor ocupe todo el ancho disponible
|
|
41
41
|
]}>
|
|
@@ -65,7 +65,7 @@ export const CDSImageButtonGroup = ({ array, onSelect, isCentered }) => {
|
|
|
65
65
|
/>
|
|
66
66
|
))}
|
|
67
67
|
</ScrollView>
|
|
68
|
-
</
|
|
68
|
+
</View>
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
|
package/components/CDSInput.jsx
CHANGED
|
@@ -23,11 +23,33 @@ export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeTe
|
|
|
23
23
|
const isReadOnly = type === 'readOnly';
|
|
24
24
|
|
|
25
25
|
// --- Colores de seguridad ---
|
|
26
|
-
const colorBase = theme?.text?.neutral?.primary
|
|
27
|
-
const colorReadOnlyText = theme?.text?.neutral?.secondary
|
|
28
|
-
const colorFocus = theme?.
|
|
29
|
-
const colorTertiary = theme?.outline?.neutral?.tertiaryVariant
|
|
30
|
-
const colorPrimaryOutline = theme?.outline?.neutral?.primary
|
|
26
|
+
const colorBase = theme?.text?.neutral?.primary
|
|
27
|
+
const colorReadOnlyText = theme?.text?.neutral?.secondary
|
|
28
|
+
const colorFocus = theme?.outline?.neutral?.focus
|
|
29
|
+
const colorTertiary = theme?.outline?.neutral?.tertiaryVariant
|
|
30
|
+
const colorPrimaryOutline = theme?.outline?.neutral?.primary
|
|
31
|
+
|
|
32
|
+
const handleTextChange = (text) => {
|
|
33
|
+
let filteredText = text;
|
|
34
|
+
|
|
35
|
+
if (keyboard === 'numeric') {
|
|
36
|
+
// Elimina cualquier cosa que no sea un número del 0 al 9
|
|
37
|
+
filteredText = text.replace(/[^0-9]/g, '');
|
|
38
|
+
}
|
|
39
|
+
else if (keyboard === 'decimal-pad') {
|
|
40
|
+
// 1. Permite solo números y puntos
|
|
41
|
+
filteredText = text.replace(/[^0-9.]/g, '');
|
|
42
|
+
|
|
43
|
+
// 2. Evita que haya más de un punto decimal
|
|
44
|
+
const parts = filteredText.split('.');
|
|
45
|
+
if (parts.length > 2) {
|
|
46
|
+
filteredText = parts[0] + '.' + parts.slice(1).join('');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
isInternalChange.current = true;
|
|
51
|
+
onChangeText && onChangeText(filteredText);
|
|
52
|
+
};
|
|
31
53
|
|
|
32
54
|
useEffect(() => {
|
|
33
55
|
// ELIMINAMOS la restricción de !isReadOnly para que se anime siempre
|
|
@@ -58,7 +80,7 @@ export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeTe
|
|
|
58
80
|
return {
|
|
59
81
|
borderColor,
|
|
60
82
|
borderWidth: 1 + (flashValue.value * 2),
|
|
61
|
-
transform: [{ scale: 1 + (flashValue.value * 0.
|
|
83
|
+
transform: [{ scale: 1 + (flashValue.value * 0.02) }]
|
|
62
84
|
};
|
|
63
85
|
});
|
|
64
86
|
|
|
@@ -109,10 +131,7 @@ export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeTe
|
|
|
109
131
|
placeholder={placeholder}
|
|
110
132
|
placeholderTextColor={theme.text.neutral.placeholder}
|
|
111
133
|
keyboardType={keyboard}
|
|
112
|
-
onChangeText={
|
|
113
|
-
isInternalChange.current = true;
|
|
114
|
-
onChangeText && onChangeText(txt);
|
|
115
|
-
}}
|
|
134
|
+
onChangeText={handleTextChange}
|
|
116
135
|
value={value}
|
|
117
136
|
editable={!isReadOnly}
|
|
118
137
|
secureTextEntry={type === "password"}
|
package/components/CDSLoader.jsx
CHANGED
|
@@ -21,8 +21,8 @@ export const CDSLoader = ({ visible = false, message }) => {
|
|
|
21
21
|
opacity.value = withTiming(1, { duration: 300 });
|
|
22
22
|
} else {
|
|
23
23
|
opacity.value = withDelay(
|
|
24
|
-
|
|
25
|
-
withTiming(0, { duration:
|
|
24
|
+
200,
|
|
25
|
+
withTiming(0, { duration: 200 }, (isFinished) => {
|
|
26
26
|
if (isFinished) {
|
|
27
27
|
runOnJS(setShouldRender)(false);
|
|
28
28
|
}
|
package/components/CDSSelect.jsx
CHANGED
|
@@ -1,151 +1,122 @@
|
|
|
1
|
-
import React, { useState,
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View, Text, TouchableOpacity, StyleSheet, ScrollView,
|
|
4
|
-
|
|
4
|
+
Pressable, Modal, SafeAreaView
|
|
5
5
|
} from "react-native";
|
|
6
6
|
import { MaterialIcons } from "@expo/vector-icons";
|
|
7
7
|
import { useTheme } from "../context/CDSThemeContext";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const SelectOption = ({ item, isSelected, onSelect, theme }) => {
|
|
11
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<Pressable
|
|
15
|
-
onPress={() => onSelect(item)}
|
|
16
|
-
onMouseEnter={() => Platform.OS === 'web' && setIsHovered(true)}
|
|
17
|
-
onMouseLeave={() => Platform.OS === 'web' && setIsHovered(false)}
|
|
18
|
-
style={({ pressed }) => [
|
|
19
|
-
styles.optionItem,
|
|
20
|
-
{
|
|
21
|
-
paddingHorizontal: theme.space.sm,
|
|
22
|
-
backgroundColor: (pressed || isHovered)
|
|
23
|
-
? theme.surface.neutral.secondary
|
|
24
|
-
: isSelected ? theme.surface.neutral.tertiary : 'transparent'
|
|
25
|
-
}
|
|
26
|
-
]}
|
|
27
|
-
>
|
|
28
|
-
<View style={styles.optionContent}>
|
|
29
|
-
<Text style={[
|
|
30
|
-
theme.typography.regular.sm,
|
|
31
|
-
{
|
|
32
|
-
color: theme.text.neutral.primary,
|
|
33
|
-
fontWeight: isSelected ? '700' : '400'
|
|
34
|
-
}
|
|
35
|
-
]}>
|
|
36
|
-
{item.label}
|
|
37
|
-
</Text>
|
|
38
|
-
{isSelected && <MaterialIcons name="check" size={18} color={theme.text.neutral.primary} />}
|
|
39
|
-
</View>
|
|
40
|
-
</Pressable>
|
|
41
|
-
);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const CDSSelect = ({ label, options = [], onSelect, selectedValue = null }) => {
|
|
9
|
+
export const CDSSelect = ({ label, options = [], onSelect, selectedValue = null, placeholder = 'Selecciona una opción' }) => {
|
|
45
10
|
const { theme } = useTheme();
|
|
46
|
-
const [
|
|
47
|
-
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
48
|
-
const containerRef = useRef(null);
|
|
49
|
-
|
|
11
|
+
const [modalVisible, setModalVisible] = useState(false);
|
|
50
12
|
const [selectedItem, setSelectedItem] = useState(
|
|
51
13
|
options.find(opt => opt.value === selectedValue) || null
|
|
52
14
|
);
|
|
53
15
|
|
|
16
|
+
// Altura fija por cada fila (puedes ajustarla según tu diseño)
|
|
17
|
+
const ITEM_HEIGHT = 52;
|
|
18
|
+
// Mostramos 7.5 opciones para que la última se vea cortada y sugiera scroll
|
|
19
|
+
const MAX_VISIBLE_OPTIONS = 7.5;
|
|
20
|
+
const MAX_HEIGHT = ITEM_HEIGHT * MAX_VISIBLE_OPTIONS;
|
|
21
|
+
|
|
54
22
|
useEffect(() => {
|
|
55
23
|
const found = options.find(opt => opt.value === selectedValue);
|
|
56
24
|
setSelectedItem(found || null);
|
|
57
25
|
}, [selectedValue, options]);
|
|
58
26
|
|
|
59
|
-
const toggleDropdown = () => {
|
|
60
|
-
if (isOpen) {
|
|
61
|
-
Animated.timing(animatedValue, {
|
|
62
|
-
toValue: 0,
|
|
63
|
-
duration: 250,
|
|
64
|
-
useNativeDriver: false
|
|
65
|
-
}).start(() => setIsOpen(false));
|
|
66
|
-
} else {
|
|
67
|
-
setIsOpen(true);
|
|
68
|
-
requestAnimationFrame(() => {
|
|
69
|
-
Animated.timing(animatedValue, {
|
|
70
|
-
toValue: 1,
|
|
71
|
-
duration: 300,
|
|
72
|
-
useNativeDriver: false
|
|
73
|
-
}).start();
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
27
|
const handleSelect = (item) => {
|
|
79
28
|
setSelectedItem(item);
|
|
80
|
-
|
|
29
|
+
setModalVisible(false);
|
|
81
30
|
if (onSelect) onSelect(item.value);
|
|
82
31
|
};
|
|
83
32
|
|
|
84
|
-
const animatedLayout = {
|
|
85
|
-
maxHeight: animatedValue.interpolate({
|
|
86
|
-
inputRange: [0, 1],
|
|
87
|
-
outputRange: [0, 240]
|
|
88
|
-
}),
|
|
89
|
-
opacity: animatedValue,
|
|
90
|
-
marginTop: animatedValue.interpolate({
|
|
91
|
-
inputRange: [0, 1],
|
|
92
|
-
outputRange: [0, theme.space.sm]
|
|
93
|
-
}),
|
|
94
|
-
};
|
|
95
|
-
|
|
96
33
|
return (
|
|
97
|
-
<View style={styles.mainContainer}
|
|
34
|
+
<View style={styles.mainContainer}>
|
|
98
35
|
{label && (
|
|
99
36
|
<Text style={[theme.typography.label, { color: theme.text.neutral.primary, marginBottom: theme.space.sm }]}>
|
|
100
37
|
{label}
|
|
101
38
|
</Text>
|
|
102
39
|
)}
|
|
103
40
|
|
|
41
|
+
{/* Trigger del Select */}
|
|
104
42
|
<TouchableOpacity
|
|
105
|
-
activeOpacity={0.
|
|
43
|
+
activeOpacity={0.7}
|
|
106
44
|
style={[
|
|
107
45
|
styles.inputContainer,
|
|
108
46
|
{
|
|
109
47
|
backgroundColor: theme.surface.neutral.primary,
|
|
110
|
-
borderColor:
|
|
48
|
+
borderColor: theme.outline.neutral.primary,
|
|
111
49
|
borderRadius: theme.radius.sm,
|
|
112
50
|
paddingHorizontal: theme.space.sm
|
|
113
51
|
}
|
|
114
52
|
]}
|
|
115
|
-
onPress={
|
|
53
|
+
onPress={() => setModalVisible(true)}
|
|
116
54
|
>
|
|
117
55
|
<Text style={selectedItem ? theme.typography.inputText.value : theme.typography.inputText.placeholder}>
|
|
118
|
-
{selectedItem ? selectedItem.label :
|
|
56
|
+
{selectedItem ? selectedItem.label : placeholder}
|
|
119
57
|
</Text>
|
|
120
|
-
<MaterialIcons name=
|
|
58
|
+
<MaterialIcons name="expand-more" size={24} color={theme.text.neutral.primary} />
|
|
121
59
|
</TouchableOpacity>
|
|
122
60
|
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
]}
|
|
61
|
+
{/* Selector Nativo / Modal */}
|
|
62
|
+
<Modal
|
|
63
|
+
visible={modalVisible}
|
|
64
|
+
transparent={true}
|
|
65
|
+
animationType="fade"
|
|
66
|
+
onRequestClose={() => setModalVisible(false)}
|
|
67
|
+
>
|
|
68
|
+
<Pressable
|
|
69
|
+
style={styles.modalOverlay}
|
|
70
|
+
onPress={() => setModalVisible(false)}
|
|
135
71
|
>
|
|
136
|
-
<
|
|
137
|
-
{
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
72
|
+
<SafeAreaView style={styles.modalContent}>
|
|
73
|
+
<View style={[
|
|
74
|
+
styles.optionsContainer,
|
|
75
|
+
{
|
|
76
|
+
backgroundColor: theme.surface.neutral.primary,
|
|
77
|
+
borderRadius: theme.radius.md,
|
|
78
|
+
maxHeight: MAX_HEIGHT + 60 // +60 para compensar el header del modal
|
|
79
|
+
}
|
|
80
|
+
]}>
|
|
81
|
+
{/* Header del Modal */}
|
|
82
|
+
<View style={[styles.modalHeader, { borderBottomColor: theme.outline.neutral.primary }]}>
|
|
83
|
+
<Text style={theme.typography.semiBold.md}>{'Selecciona una opción'}</Text>
|
|
84
|
+
<TouchableOpacity onPress={() => setModalVisible(false)} hitSlop={{top: 10, bottom: 10, left: 10, right: 10}}>
|
|
85
|
+
<MaterialIcons name="close" size={24} color={theme.text.neutral.primary} />
|
|
86
|
+
</TouchableOpacity>
|
|
87
|
+
</View>
|
|
88
|
+
|
|
89
|
+
{/* Lista Scrolleable */}
|
|
90
|
+
<ScrollView
|
|
91
|
+
bounces={false}
|
|
92
|
+
showsVerticalScrollIndicator={true}
|
|
93
|
+
nestedScrollEnabled={true}
|
|
94
|
+
>
|
|
95
|
+
{options.map((item, index) => (
|
|
96
|
+
<TouchableOpacity
|
|
97
|
+
key={item.value?.toString() || index.toString()}
|
|
98
|
+
onPress={() => handleSelect(item)}
|
|
99
|
+
style={[
|
|
100
|
+
styles.optionItem,
|
|
101
|
+
{
|
|
102
|
+
height: ITEM_HEIGHT,
|
|
103
|
+
backgroundColor: selectedItem?.value === item.value ? theme.surface.neutral.secondary : 'transparent'
|
|
104
|
+
}
|
|
105
|
+
]}
|
|
106
|
+
>
|
|
107
|
+
<Text style={[theme.typography.regular.md, { color: theme.text.neutral.primary }]}>
|
|
108
|
+
{item.label}
|
|
109
|
+
</Text>
|
|
110
|
+
{selectedItem?.value === item.value && (
|
|
111
|
+
<MaterialIcons name="check" size={20} color={theme.text.neutral.primary} />
|
|
112
|
+
)}
|
|
113
|
+
</TouchableOpacity>
|
|
114
|
+
))}
|
|
115
|
+
</ScrollView>
|
|
116
|
+
</View>
|
|
117
|
+
</SafeAreaView>
|
|
118
|
+
</Pressable>
|
|
119
|
+
</Modal>
|
|
149
120
|
</View>
|
|
150
121
|
);
|
|
151
122
|
};
|
|
@@ -153,9 +124,37 @@ export const CDSSelect = ({ label, options = [], onSelect, selectedValue = null
|
|
|
153
124
|
const styles = StyleSheet.create({
|
|
154
125
|
mainContainer: { width: '100%' },
|
|
155
126
|
inputContainer: { width: '100%', height: 48, borderWidth: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
156
|
-
|
|
157
|
-
|
|
127
|
+
modalOverlay: {
|
|
128
|
+
flex: 1,
|
|
129
|
+
backgroundColor: 'rgba(0,0,0,0.6)', // Un poco más oscuro para mejor enfoque
|
|
130
|
+
justifyContent: 'center',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
padding: 24
|
|
133
|
+
},
|
|
134
|
+
modalContent: {
|
|
135
|
+
width: '100%',
|
|
136
|
+
maxWidth: 400,
|
|
137
|
+
},
|
|
138
|
+
optionsContainer: {
|
|
139
|
+
width: '100%',
|
|
140
|
+
overflow: 'hidden',
|
|
141
|
+
elevation: 5,
|
|
142
|
+
shadowColor: '#000',
|
|
143
|
+
shadowOffset: { width: 0, height: 4 },
|
|
144
|
+
shadowOpacity: 0.3,
|
|
145
|
+
shadowRadius: 4.65,
|
|
146
|
+
},
|
|
147
|
+
modalHeader: {
|
|
148
|
+
flexDirection: 'row',
|
|
149
|
+
justifyContent: 'space-between',
|
|
150
|
+
alignItems: 'center',
|
|
151
|
+
padding: 16,
|
|
152
|
+
borderBottomWidth: 1,
|
|
158
153
|
},
|
|
159
|
-
optionItem: {
|
|
160
|
-
|
|
154
|
+
optionItem: {
|
|
155
|
+
paddingHorizontal: 16,
|
|
156
|
+
flexDirection: 'row',
|
|
157
|
+
justifyContent: 'space-between',
|
|
158
|
+
alignItems: 'center',
|
|
159
|
+
}
|
|
161
160
|
});
|