cdslibrary 1.2.87 → 1.2.89
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 +65 -28
- package/components/CDSButton.jsx +4 -33
- package/components/CDSButtonGroup.jsx +1 -1
- package/components/CDSCarousel.jsx +1 -1
- package/components/CDSImageButton.jsx +32 -16
- package/components/CDSImageButtonGroup.jsx +3 -2
- package/components/CDSInput.jsx +29 -10
- package/components/CDSLoader.jsx +2 -2
- package/components/CDSTooltip.jsx +62 -90
- 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
|
|
@@ -118,19 +132,19 @@ const animatedStyle = useAnimatedStyle(() => {
|
|
|
118
132
|
return (
|
|
119
133
|
<>
|
|
120
134
|
<Animated.View
|
|
121
|
-
pointerEvents={isVisible ? "auto" : "none"} // Evita clics fantasma
|
|
122
135
|
style={[
|
|
123
136
|
styles.overlay,
|
|
124
|
-
{ backgroundColor: theme.surface.special.overlay },
|
|
137
|
+
{ pointerEvents: isVisible ? "auto" : "none", backgroundColor: theme.surface.special.overlay },
|
|
125
138
|
backdropStyle
|
|
126
139
|
]}
|
|
127
140
|
>
|
|
128
|
-
<Pressable onPress={() => runOnJS(handleClose)()} style={{ flex: 1 }} />
|
|
141
|
+
<Pressable onPress={() => runOnJS(handleClose)(false)} style={{ flex: 1 }} />
|
|
129
142
|
</Animated.View>
|
|
130
143
|
<Animated.View
|
|
131
144
|
style={[
|
|
132
145
|
isMobile ? styles.container.typeBottomSheet : styles.container.typeDrawer,
|
|
133
146
|
animatedStyle,
|
|
147
|
+
theme.shadows.lg,
|
|
134
148
|
{
|
|
135
149
|
backgroundColor: theme.surface.neutral.primary,
|
|
136
150
|
paddingTop: (hasClose ? theme.space['2xl'] : theme.space.xl),
|
|
@@ -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>
|
|
@@ -239,7 +275,6 @@ const styles = StyleSheet.create({
|
|
|
239
275
|
zIndex: 99,
|
|
240
276
|
...Platform.select({
|
|
241
277
|
web: {
|
|
242
|
-
boxShadow: '-10px 0px 15px rgba(0,0,0,0.1)',
|
|
243
278
|
height: 'auto', // En web forzamos que sea auto
|
|
244
279
|
}
|
|
245
280
|
})
|
|
@@ -250,27 +285,29 @@ const styles = StyleSheet.create({
|
|
|
250
285
|
width: '100%',
|
|
251
286
|
},
|
|
252
287
|
scrollArea: {
|
|
288
|
+
|
|
253
289
|
width: '100%',
|
|
254
290
|
},
|
|
255
291
|
scrollContent: {
|
|
256
292
|
gap: 16,
|
|
293
|
+
|
|
257
294
|
},
|
|
258
295
|
fadeGradient: {
|
|
259
296
|
position: 'absolute',
|
|
260
297
|
bottom: 0,
|
|
261
298
|
left: 0,
|
|
262
299
|
right: 0,
|
|
263
|
-
height:
|
|
300
|
+
height: 32,
|
|
264
301
|
},
|
|
265
302
|
actionsContainer: {
|
|
266
303
|
typeBottomSheet: {
|
|
267
304
|
flexDirection: "column",
|
|
268
|
-
width: "
|
|
305
|
+
width: "auto",
|
|
269
306
|
gap: 8,
|
|
270
307
|
},
|
|
271
308
|
typeDrawer: {
|
|
272
309
|
flexDirection: "row-reverse",
|
|
273
|
-
width: "
|
|
310
|
+
width: "auto",
|
|
274
311
|
gap: 8,
|
|
275
312
|
marginTop: 'auto',
|
|
276
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={{
|
|
@@ -4,17 +4,33 @@ import { MaterialIcons } from "@expo/vector-icons";
|
|
|
4
4
|
import { useTheme } from "../context/CDSThemeContext";
|
|
5
5
|
import { CDSTooltip } from "./CDSTooltip"; // Asegúrate de importar tu Tooltip
|
|
6
6
|
|
|
7
|
-
export const CDSImageButton = ({ object, isActive, onPress, hasHelper, helperMessage }) => {
|
|
7
|
+
export const CDSImageButton = ({ object, isActive, onPress, hasHelper, helperMessage, arrowAlignment }) => {
|
|
8
8
|
const { theme } = useTheme();
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
// Estados para controlar el Tooltip interno
|
|
11
11
|
const [tooltipVisible, setTooltipVisible] = useState(false);
|
|
12
12
|
const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
|
|
13
13
|
|
|
14
14
|
const handleHelperPress = (event) => {
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
// Si estamos en Web, usamos getBoundingClientRect para precisión total
|
|
16
|
+
if (event.target && event.target.getBoundingClientRect) {
|
|
17
|
+
const rect = event.target.getBoundingClientRect();
|
|
18
|
+
setTooltipPos({
|
|
19
|
+
x: rect.left,
|
|
20
|
+
y: rect.top,
|
|
21
|
+
width: rect.width,
|
|
22
|
+
height: rect.height
|
|
23
|
+
});
|
|
24
|
+
} else {
|
|
25
|
+
// Fallback para Mobile (Nativo)
|
|
26
|
+
const { pageX, pageY } = event.nativeEvent;
|
|
27
|
+
setTooltipPos({
|
|
28
|
+
x: pageX - 10, // Ajuste manual si no hay medida
|
|
29
|
+
y: pageY,
|
|
30
|
+
width: 20,
|
|
31
|
+
height: 20
|
|
32
|
+
});
|
|
33
|
+
}
|
|
18
34
|
setTooltipVisible(true);
|
|
19
35
|
};
|
|
20
36
|
|
|
@@ -26,8 +42,8 @@ export const CDSImageButton = ({ object, isActive, onPress, hasHelper, helperMes
|
|
|
26
42
|
style={[
|
|
27
43
|
styles.mainContainer,
|
|
28
44
|
{
|
|
29
|
-
backgroundColor: isActive
|
|
30
|
-
? theme.surface.neutral.primaryVariant
|
|
45
|
+
backgroundColor: isActive
|
|
46
|
+
? theme.surface.neutral.primaryVariant
|
|
31
47
|
: theme.surface.neutral.primary,
|
|
32
48
|
gap: theme.space.xs, // Reducimos un poco el gap para el label
|
|
33
49
|
padding: theme.space.sm,
|
|
@@ -35,7 +51,7 @@ export const CDSImageButton = ({ object, isActive, onPress, hasHelper, helperMes
|
|
|
35
51
|
borderWidth: 1,
|
|
36
52
|
borderColor: isActive ? theme.outline.neutral.focus : 'transparent',
|
|
37
53
|
...(!isActive && theme.shadows.lg),
|
|
38
|
-
transform: [{ scale: isActive ? 0.98 : 1 }]
|
|
54
|
+
transform: [{ scale: isActive ? 0.98 : 1 }]
|
|
39
55
|
},
|
|
40
56
|
]}
|
|
41
57
|
>
|
|
@@ -52,7 +68,7 @@ export const CDSImageButton = ({ object, isActive, onPress, hasHelper, helperMes
|
|
|
52
68
|
|
|
53
69
|
{/* Contenedor de Texto + Ícono de ayuda */}
|
|
54
70
|
<View style={styles.labelContainer}>
|
|
55
|
-
<Text
|
|
71
|
+
<Text
|
|
56
72
|
numberOfLines={1}
|
|
57
73
|
style={[
|
|
58
74
|
isActive ? theme.typography.semiBold.sm : theme.typography.regular.sm,
|
|
@@ -63,14 +79,14 @@ export const CDSImageButton = ({ object, isActive, onPress, hasHelper, helperMes
|
|
|
63
79
|
</Text>
|
|
64
80
|
|
|
65
81
|
{hasHelper && (
|
|
66
|
-
<TouchableOpacity
|
|
82
|
+
<TouchableOpacity
|
|
67
83
|
onPress={handleHelperPress}
|
|
68
84
|
hitSlop={{ top: 15, bottom: 15, left: 15, right: 15 }}
|
|
69
85
|
>
|
|
70
|
-
<MaterialIcons
|
|
71
|
-
name="help-outline"
|
|
72
|
-
size={14}
|
|
73
|
-
color={theme.text.neutral.secondary}
|
|
86
|
+
<MaterialIcons
|
|
87
|
+
name="help-outline"
|
|
88
|
+
size={14}
|
|
89
|
+
color={theme.text.neutral.secondary}
|
|
74
90
|
/>
|
|
75
91
|
</TouchableOpacity>
|
|
76
92
|
)}
|
|
@@ -78,12 +94,12 @@ export const CDSImageButton = ({ object, isActive, onPress, hasHelper, helperMes
|
|
|
78
94
|
</TouchableOpacity>
|
|
79
95
|
|
|
80
96
|
{/* Tooltip renderizado fuera del botón para evitar cortes de overflow */}
|
|
81
|
-
<CDSTooltip
|
|
97
|
+
<CDSTooltip
|
|
82
98
|
visible={tooltipVisible}
|
|
83
99
|
message={helperMessage}
|
|
84
100
|
targetPosition={tooltipPos}
|
|
85
101
|
onDismiss={() => setTooltipVisible(false)}
|
|
86
|
-
arrowAlign=
|
|
102
|
+
arrowAlign={arrowAlignment}
|
|
87
103
|
/>
|
|
88
104
|
</>
|
|
89
105
|
);
|
|
@@ -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
|
]}>
|
|
@@ -61,10 +61,11 @@ export const CDSImageButtonGroup = ({ array, onSelect, isCentered }) => {
|
|
|
61
61
|
onPress={() => handlePress(item.id)}
|
|
62
62
|
hasHelper={item.helper ? true : false}
|
|
63
63
|
helperMessage={item.helper}
|
|
64
|
+
arrowAlignment={item.arrowAlignment}
|
|
64
65
|
/>
|
|
65
66
|
))}
|
|
66
67
|
</ScrollView>
|
|
67
|
-
</
|
|
68
|
+
</View>
|
|
68
69
|
);
|
|
69
70
|
}
|
|
70
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.01) }]
|
|
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
|
}
|
|
@@ -1,126 +1,98 @@
|
|
|
1
|
-
import React, { useEffect } from "react";
|
|
2
|
-
import { StyleSheet, Text, View, Modal,
|
|
3
|
-
import Animated, {
|
|
4
|
-
useSharedValue,
|
|
5
|
-
useAnimatedStyle,
|
|
6
|
-
withSpring,
|
|
7
|
-
withTiming,
|
|
8
|
-
} from "react-native-reanimated";
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { StyleSheet, Text, View, Modal, Pressable, Dimensions } from "react-native";
|
|
3
|
+
import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS } from "react-native-reanimated";
|
|
9
4
|
import { useTheme } from "../context/CDSThemeContext";
|
|
10
5
|
|
|
11
|
-
|
|
6
|
+
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|
7
|
+
|
|
8
|
+
export const CDSTooltip = ({ message, visible, onDismiss, targetPosition = { x: 0, y: 0, width: 0, height: 0 }, arrowAlign = 'center' }) => {
|
|
12
9
|
const { theme } = useTheme();
|
|
13
10
|
const opacity = useSharedValue(0);
|
|
14
|
-
const
|
|
11
|
+
const [shouldRender, setShouldRender] = useState(visible);
|
|
15
12
|
|
|
16
13
|
useEffect(() => {
|
|
17
14
|
if (visible) {
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
setShouldRender(true);
|
|
16
|
+
opacity.value = withTiming(1, { duration: 150 });
|
|
20
17
|
} else {
|
|
21
|
-
opacity.value = withTiming(0, { duration:
|
|
22
|
-
|
|
18
|
+
opacity.value = withTiming(0, { duration: 150 }, (finished) => {
|
|
19
|
+
if (finished) runOnJS(setShouldRender)(false);
|
|
20
|
+
});
|
|
23
21
|
}
|
|
24
22
|
}, [visible]);
|
|
25
23
|
|
|
26
|
-
const animatedStyle = useAnimatedStyle(() => ({
|
|
27
|
-
opacity: opacity.value,
|
|
28
|
-
// transform: [{ scale: scale.value }],
|
|
29
|
-
}));
|
|
24
|
+
const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
|
30
25
|
|
|
31
|
-
//
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
case 'left': return { left: 15 };
|
|
35
|
-
case 'right': return { right: 15 };
|
|
36
|
-
default: return { alignSelf: 'center' };
|
|
37
|
-
}
|
|
38
|
-
};
|
|
26
|
+
// --- LÓGICA DE POSICIONAMIENTO ---
|
|
27
|
+
const tooltipWidth = 320; // Ancho fijo para facilitar el cálculo de la flecha
|
|
28
|
+
const paddingScreen = 16;
|
|
39
29
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
const tooltipWidth = 200; // Ancho estimado o fijo
|
|
43
|
-
let leftPosition = targetPosition.x;
|
|
30
|
+
// Centro horizontal del icono/botón
|
|
31
|
+
const targetCenterX = targetPosition.x + (targetPosition.width / 2);
|
|
44
32
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} else {
|
|
50
|
-
leftPosition = targetPosition.x - 25;
|
|
51
|
-
}
|
|
33
|
+
// Calculamos el Left del contenedor
|
|
34
|
+
let leftPos = targetCenterX - (tooltipWidth / 2);
|
|
35
|
+
if (arrowAlign === 'left') leftPos = targetCenterX - 30;
|
|
36
|
+
if (arrowAlign === 'right') leftPos = targetCenterX - tooltipWidth + 30;
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
left: Math.max(10, leftPosition), // Evita que se salga de la pantalla por la izquierda
|
|
56
|
-
width: tooltipWidth,
|
|
57
|
-
};
|
|
58
|
-
};
|
|
38
|
+
// Ajuste para que no choque con los bordes del celular/navegador
|
|
39
|
+
const finalLeft = Math.max(paddingScreen, Math.min(leftPos, SCREEN_WIDTH - tooltipWidth - paddingScreen));
|
|
59
40
|
|
|
60
|
-
|
|
41
|
+
// Calculamos dónde debe ir la flecha respecto al globo
|
|
42
|
+
const arrowLeft = targetCenterX - finalLeft - 8; // 8 es la mitad del ancho de la flecha
|
|
43
|
+
|
|
44
|
+
if (!shouldRender) return null;
|
|
61
45
|
|
|
62
46
|
return (
|
|
63
47
|
<Modal transparent visible={visible} animationType="none" onRequestClose={onDismiss}>
|
|
64
|
-
<
|
|
65
|
-
<View
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
48
|
+
<Pressable style={styles.overlay} onPress={onDismiss}>
|
|
49
|
+
<Animated.View
|
|
50
|
+
style={[
|
|
51
|
+
styles.tooltip,
|
|
52
|
+
animatedStyle,
|
|
53
|
+
theme.shadows.lg,
|
|
54
|
+
{
|
|
55
|
+
top: targetPosition.y + targetPosition.height + 8,
|
|
56
|
+
left: finalLeft,
|
|
57
|
+
width: tooltipWidth,
|
|
58
|
+
backgroundColor: theme.surface.special.tooltip,
|
|
59
|
+
borderRadius: theme.radius.sm,
|
|
60
|
+
padding: theme.space.sm,
|
|
61
|
+
}
|
|
62
|
+
]}
|
|
63
|
+
>
|
|
64
|
+
{/* Flecha dinámica */}
|
|
65
|
+
<View style={[
|
|
66
|
+
styles.arrowUp,
|
|
67
|
+
{ left: arrowLeft, borderBottomColor: theme.surface.special.tooltip }
|
|
68
|
+
]} />
|
|
84
69
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{message}
|
|
94
|
-
</Text>
|
|
95
|
-
</Animated.View>
|
|
96
|
-
</View>
|
|
97
|
-
</TouchableWithoutFeedback>
|
|
70
|
+
<Text style={[
|
|
71
|
+
theme.typography.regular.xs,
|
|
72
|
+
{ color: theme.text.neutral.contrast, textAlign: 'center' }
|
|
73
|
+
]}>
|
|
74
|
+
{message}
|
|
75
|
+
</Text>
|
|
76
|
+
</Animated.View>
|
|
77
|
+
</Pressable>
|
|
98
78
|
</Modal>
|
|
99
79
|
);
|
|
100
80
|
};
|
|
101
81
|
|
|
102
82
|
const styles = StyleSheet.create({
|
|
103
|
-
overlay: {
|
|
104
|
-
tooltip: {
|
|
105
|
-
position: 'absolute',
|
|
106
|
-
zIndex: 10000,
|
|
107
|
-
elevation: 5,
|
|
108
|
-
shadowColor: "#000",
|
|
109
|
-
shadowOffset: { width: 0, height: 2 },
|
|
110
|
-
shadowOpacity: 0.2,
|
|
111
|
-
shadowRadius: 4,
|
|
112
|
-
},
|
|
83
|
+
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'transparent' },
|
|
84
|
+
tooltip: { position: 'absolute', zIndex: 10000 },
|
|
113
85
|
arrowUp: {
|
|
114
86
|
position: 'absolute',
|
|
115
|
-
top: -
|
|
87
|
+
top: -7,
|
|
116
88
|
width: 0,
|
|
117
89
|
height: 0,
|
|
118
90
|
borderLeftWidth: 8,
|
|
119
91
|
borderRightWidth: 8,
|
|
120
92
|
borderBottomWidth: 8,
|
|
121
|
-
borderStyle: 'solid',
|
|
122
|
-
backgroundColor: 'transparent',
|
|
123
93
|
borderLeftColor: 'transparent',
|
|
124
94
|
borderRightColor: 'transparent',
|
|
95
|
+
borderTopColor: 'transparent',
|
|
96
|
+
backgroundColor: 'transparent',
|
|
125
97
|
}
|
|
126
98
|
});
|