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.
@@ -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 triggerOnFinish = useCallback(() => {
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
- const handleClose = useCallback(() => {
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
- contentContainerStyle={[styles.scrollContent, { paddingBottom: theme.space.xl }]} // Tu padding original
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, { padding: theme.space.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
- <LinearGradient
189
- colors={['transparent', theme.surface.neutral.primary]}
190
- style={[styles.fadeGradient, { pointerEvents: 'none' }]}
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, { paddingHorizontal: theme.space.md, paddingVertical: theme.space.lg }]}>
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={() => runOnJS(handleClose)()}
220
+ onPress={() => {
221
+ if (primaryButtonOnPress) primaryButtonOnPress();
222
+ runOnJS(handleClose)(true); // <--- TRUE: Acción ya hecha
223
+ }}
199
224
  />
200
- {secondaryButtonLabel && (<CDSButton
201
- label={secondaryButtonLabel}
202
- type="ghost"
203
- onPress={() => runOnJS(handleClose)()}
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: 24,
300
+ height: 32,
264
301
  },
265
302
  actionsContainer: {
266
303
  typeBottomSheet: {
267
304
  flexDirection: "column",
268
- width: "100%",
305
+ width: "auto",
269
306
  gap: 8,
270
307
  },
271
308
  typeDrawer: {
272
309
  flexDirection: "row-reverse",
273
- width: "100%",
310
+ width: "auto",
274
311
  gap: 8,
275
312
  marginTop: 'auto',
276
313
  },
@@ -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
- <Animated.View style={[
62
- animatedLayout,
33
+ <View style={[
63
34
  {
64
35
  overflow: 'hidden',
65
- alignSelf: (variant === "fill" || isMobile) ? "stretch" : "center",
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
- </Animated.View>
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', backgroundColor: 'red' }}>
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
- // Capturamos la posición global para el tooltip
16
- const { pageX, pageY } = event.nativeEvent;
17
- setTooltipPos({ x: pageX, y: pageY });
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="center"
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
- <Animated.View style={[
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
- </Animated.View>
68
+ </View>
68
69
  );
69
70
  }
70
71
 
@@ -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?.surface?.special?.progress
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.06) }]
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={(txt) => {
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"}
@@ -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
- 1000,
25
- withTiming(0, { duration: 1200 }, (isFinished) => {
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, TouchableWithoutFeedback } from "react-native";
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
- export const CDSTooltip = ({ message, visible, onDismiss, targetPosition = { x: 0, y: 0 }, arrowAlign = 'center' }) => {
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 scale = useSharedValue(1);
11
+ const [shouldRender, setShouldRender] = useState(visible);
15
12
 
16
13
  useEffect(() => {
17
14
  if (visible) {
18
- opacity.value = withTiming(1, { duration: 200 });
19
- // scale.value = withSpring(1, { damping: 12 });
15
+ setShouldRender(true);
16
+ opacity.value = withTiming(1, { duration: 150 });
20
17
  } else {
21
- opacity.value = withTiming(0, { duration: 200 });
22
- // scale.value = withTiming(0.8);
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
- // 1. Alineación de la flecha dentro del globo
32
- const getArrowStyle = () => {
33
- switch (arrowAlign) {
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
- // 2. Alineación del globo respecto al clic (x)
41
- const getTooltipContainerStyle = () => {
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
- if (arrowAlign === 'center') {
46
- leftPosition = targetPosition.x - (tooltipWidth / 2);
47
- } else if (arrowAlign === 'right') {
48
- leftPosition = targetPosition.x - tooltipWidth + 25; // 25 es el margen de la flecha
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
- return {
54
- top: targetPosition.y + 24, // Debajo del clic
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
- if (!visible && opacity.value === 0) return null;
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
- <TouchableWithoutFeedback onPress={onDismiss}>
65
- <View style={styles.overlay}>
66
- <Animated.View
67
- style={[
68
- styles.tooltip,
69
- animatedStyle,
70
- getTooltipContainerStyle(),
71
- {
72
- backgroundColor: theme.surface.special.tooltip,
73
- borderRadius: theme.radius.sm,
74
- padding: theme.space.sm,
75
- }
76
- ]}
77
- >
78
- {/* La Flecha */}
79
- <View style={[
80
- styles.arrowUp,
81
- getArrowStyle(),
82
- { borderBottomColor: theme.surface.special.tooltip }
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
- {/* El Texto con alineación dinámica */}
86
- <Text style={[
87
- theme.typography.regular.xs,
88
- {
89
- color: theme.text.neutral.contrast,
90
- textAlign: arrowAlign // 'left', 'right' o 'center'
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: { flex: 1, backgroundColor: 'transparent' },
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: -8,
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
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cdslibrary",
3
3
  "license": "0BSD",
4
- "version": "1.2.87",
4
+ "version": "1.2.89",
5
5
  "main": "index.js",
6
6
  "author": "Nat Viramontes",
7
7
  "description": "A library of components for the CDS project",