cdslibrary 1.2.85 → 1.2.87

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.
@@ -2,62 +2,105 @@ import React, { useState } from "react";
2
2
  import { Text, StyleSheet, TouchableOpacity, View, Image } from "react-native";
3
3
  import { MaterialIcons } from "@expo/vector-icons";
4
4
  import { useTheme } from "../context/CDSThemeContext";
5
+ import { CDSTooltip } from "./CDSTooltip"; // Asegúrate de importar tu Tooltip
5
6
 
6
-
7
-
8
- export const CDSImageButton = ({ object, isActive, onPress }) => { // 👈 Añadimos isActive
7
+ export const CDSImageButton = ({ object, isActive, onPress, hasHelper, helperMessage }) => {
9
8
  const { theme } = useTheme();
9
+
10
+ // Estados para controlar el Tooltip interno
11
+ const [tooltipVisible, setTooltipVisible] = useState(false);
12
+ const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
10
13
 
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 });
18
+ setTooltipVisible(true);
19
+ };
11
20
 
12
21
  return (
13
- <TouchableOpacity
14
- onPress={onPress}
15
- style={[
16
- styles.mainContainer,
17
- {
18
- backgroundColor: isActive
19
- ? theme.surface.neutral.primaryVariant // Color cuando está presionado
20
- : theme.surface.neutral.primary,
21
- gap: theme.space.sm,
22
- padding: theme.space.sm,
23
- // borderColor: isActive
24
- // ? theme.outline.neutral.primary // Borde de color de marca si está activo
25
- // : theme.outline.neutral.primary,
26
- borderRadius: theme.radius.md,
27
- // Si está activo, quitamos la sombra o usamos una más pequeña (md)
28
- ...(!isActive && theme.shadows.lg),
29
- // Si quieres que se vea "hundido", podrías bajar la escala
30
- transform: [{ scale: isActive ? 0.99 : 1 }]
31
- },
32
- ]}
33
- >
34
- <Image
35
- source={object.image}
36
- resizeMode="cover"
37
- style={{
38
- flexGrow: 1,
39
- width: "100%",
40
- height: 80,
41
- borderRadius: theme.radius.sm,
42
- }}
43
- />
44
- <Text style={isActive ? theme.typography.semiBold.sm : theme.typography.regular.sm }>
45
- {object.label}
46
- </Text>
47
- </TouchableOpacity>
48
- );
49
- };
22
+ <>
23
+ <TouchableOpacity
24
+ onPress={onPress}
25
+ activeOpacity={0.7}
26
+ style={[
27
+ styles.mainContainer,
28
+ {
29
+ backgroundColor: isActive
30
+ ? theme.surface.neutral.primaryVariant
31
+ : theme.surface.neutral.primary,
32
+ gap: theme.space.xs, // Reducimos un poco el gap para el label
33
+ padding: theme.space.sm,
34
+ borderRadius: theme.radius.md,
35
+ borderWidth: 1,
36
+ borderColor: isActive ? theme.outline.neutral.focus : 'transparent',
37
+ ...(!isActive && theme.shadows.lg),
38
+ transform: [{ scale: isActive ? 0.98 : 1 }]
39
+ },
40
+ ]}
41
+ >
42
+ <Image
43
+ source={object.image}
44
+ resizeMode="cover"
45
+ style={{
46
+ width: "100%",
47
+ height: 80,
48
+ borderRadius: theme.radius.sm,
49
+ marginBottom: 4
50
+ }}
51
+ />
50
52
 
53
+ {/* Contenedor de Texto + Ícono de ayuda */}
54
+ <View style={styles.labelContainer}>
55
+ <Text
56
+ numberOfLines={1}
57
+ style={[
58
+ isActive ? theme.typography.semiBold.sm : theme.typography.regular.sm,
59
+ { color: theme.text.neutral.primary }
60
+ ]}
61
+ >
62
+ {object.label}
63
+ </Text>
51
64
 
65
+ {hasHelper && (
66
+ <TouchableOpacity
67
+ onPress={handleHelperPress}
68
+ hitSlop={{ top: 15, bottom: 15, left: 15, right: 15 }}
69
+ >
70
+ <MaterialIcons
71
+ name="help-outline"
72
+ size={14}
73
+ color={theme.text.neutral.secondary}
74
+ />
75
+ </TouchableOpacity>
76
+ )}
77
+ </View>
78
+ </TouchableOpacity>
52
79
 
53
- const styles = StyleSheet.create ({
80
+ {/* Tooltip renderizado fuera del botón para evitar cortes de overflow */}
81
+ <CDSTooltip
82
+ visible={tooltipVisible}
83
+ message={helperMessage}
84
+ targetPosition={tooltipPos}
85
+ onDismiss={() => setTooltipVisible(false)}
86
+ arrowAlign="center"
87
+ />
88
+ </>
89
+ );
90
+ };
54
91
 
55
- mainContainer:{
92
+ const styles = StyleSheet.create({
93
+ mainContainer: {
56
94
  alignItems: 'center',
57
95
  flexGrow: 1,
58
96
  maxWidth: 160,
59
97
  minWidth: 96,
60
- height: 'auto'
61
- }
62
-
63
- })
98
+ },
99
+ labelContainer: {
100
+ flexDirection: 'row',
101
+ alignItems: 'center',
102
+ justifyContent: 'center',
103
+ gap: 4, // Espacio entre el texto y el ícono
104
+ width: '100%',
105
+ }
106
+ });
@@ -59,6 +59,8 @@ export const CDSImageButtonGroup = ({ array, onSelect, isCentered }) => {
59
59
  object={item}
60
60
  isActive={selectedId === item.id}
61
61
  onPress={() => handlePress(item.id)}
62
+ hasHelper={item.helper ? true : false}
63
+ helperMessage={item.helper}
62
64
  />
63
65
  ))}
64
66
  </ScrollView>
@@ -5,66 +5,84 @@ import Animated, {
5
5
  useSharedValue,
6
6
  withSequence,
7
7
  withTiming,
8
+ withDelay,
8
9
  interpolateColor
9
10
  } from "react-native-reanimated";
10
11
  import { useTheme } from "../context/CDSThemeContext";
11
12
 
13
+ const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
14
+
12
15
  export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeText, animationTrigger, returnKeyType, onFocus }) => {
13
16
  const { theme } = useTheme();
14
17
  const [isFocused, setIsFocused] = useState(false);
15
18
 
16
19
  const flashValue = useSharedValue(0);
20
+ const textHighlight = useSharedValue(0);
17
21
  const isInternalChange = useRef(false);
18
22
 
23
+ const isReadOnly = type === 'readOnly';
24
+
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
31
+
19
32
  useEffect(() => {
20
- if (!isInternalChange.current) {
21
- flashValue.value = withSequence(
22
- withTiming(1, { duration: 200 }),
33
+ // ELIMINAMOS la restricción de !isReadOnly para que se anime siempre
34
+ if (!isInternalChange.current && value) {
35
+ flashValue.value = withDelay(400, withSequence(
36
+ withTiming(1, { duration: 400 }),
37
+ withTiming(0, { duration: 600 })
38
+ ));
39
+
40
+ textHighlight.value = withSequence(
41
+ withTiming(1, { duration: 400 }),
23
42
  withTiming(0, { duration: 600 })
24
43
  );
25
44
  }
26
45
  isInternalChange.current = false;
27
- }, [value, animationTrigger]);
28
-
29
- const handleTextChange = (inputText) => {
30
- isInternalChange.current = true;
31
-
32
- if (keyboard === "decimal-pad") {
33
- let cleaned = inputText.replace(',', '.').replace(/[^0-9.]/g, "");
34
- const parts = cleaned.split('.');
35
- if (parts.length > 2) cleaned = parts[0] + '.' + parts.slice(1).join('');
36
- onChangeText && onChangeText(cleaned);
37
- } else {
38
- onChangeText && onChangeText(inputText);
39
- }
40
- };
46
+ }, [value, animationTrigger]); // Quitamos isReadOnly de las dependencias para evitar disparos extra
41
47
 
42
48
  const animatedBoxStyle = useAnimatedStyle(() => {
43
49
  const borderColor = interpolateColor(
44
50
  flashValue.value,
45
51
  [0, 1],
46
52
  [
47
- isFocused ? theme.outline.neutral.focus : value ? theme.outline.neutral.tertiaryVariant : theme.outline.neutral.primary,
48
- theme.outline.neutral.focus
53
+ isFocused ? colorFocus : value ? colorTertiary : colorPrimaryOutline,
54
+ colorFocus
49
55
  ]
50
56
  );
51
57
 
52
58
  return {
53
- borderColor: borderColor,
54
- // Quitamos el scale momentáneamente para asegurar que el layout no brinque
55
- // transform: [{ scale: 1 + flashValue.value * 0.01 }],
56
- borderWidth: 1 + (flashValue.value * 1),
59
+ borderColor,
60
+ borderWidth: 1 + (flashValue.value * 2),
61
+ transform: [{ scale: 1 + (flashValue.value * 0.06) }]
57
62
  };
58
63
  });
59
64
 
65
+ const animatedTextStyle = useAnimatedStyle(() => {
66
+ // Definimos el color inicial según si es readOnly o no
67
+ const startColor = isReadOnly ? colorReadOnlyText : colorBase;
68
+
69
+ return {
70
+ color: interpolateColor(
71
+ textHighlight.value,
72
+ [0, 1],
73
+ [startColor, colorFocus] // El color de "brillo" sigue siendo el de foco
74
+ ),
75
+ letterSpacing: textHighlight.value * 1,
76
+ opacity: isReadOnly ? 0.85 + (textHighlight.value * 0.3) : 0.8 + (textHighlight.value * 0.2),
77
+ };
78
+ });
79
+
80
+ if (!theme) return null;
60
81
 
61
82
  return (
62
83
  <View style={styles.container}>
63
84
  {label && (
64
- <Text style={[
65
- theme.typography.label,
66
- { color: theme.text.neutral.primary, marginBottom: theme.space.xs }
67
- ]}>
85
+ <Text style={[theme.typography.label, { color: colorBase, marginBottom: theme.space?.xs || 4 }]}>
68
86
  {label}
69
87
  </Text>
70
88
  )}
@@ -73,35 +91,34 @@ export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeTe
73
91
  styles.wrapper,
74
92
  animatedBoxStyle,
75
93
  {
76
- backgroundColor: type === 'readOnly' ? theme.surface.neutral.primaryVariant : theme.surface.neutral.primary,
77
- borderRadius: theme.radius.sm,
94
+ backgroundColor: isReadOnly ? theme.surface.neutral.primaryVariant : theme.surface.neutral.primary,
95
+ borderRadius: theme.radius?.sm || 8,
78
96
  }
79
97
  ]}>
80
- <TextInput
98
+ <AnimatedTextInput
81
99
  style={[
82
100
  styles.textBox,
83
- theme.typography.inputText.value,
101
+ theme.typography.inputText?.value,
102
+ animatedTextStyle,
84
103
  {
85
- paddingHorizontal: theme.space.sm,
86
- color: type === 'readOnly' ? theme.text.neutral.secondary : theme.text.neutral.primary,
87
- // Fix para web:
88
- ...Platform.select({
89
- web: { outlineStyle: 'none' }
90
- })
104
+ paddingHorizontal: theme.space?.sm || 12,
105
+ fontWeight: '600',
106
+ ...Platform.select({ web: { outlineStyle: 'none' } })
91
107
  },
92
108
  ]}
93
109
  placeholder={placeholder}
94
110
  placeholderTextColor={theme.text.neutral.placeholder}
95
111
  keyboardType={keyboard}
96
- onChangeText={handleTextChange}
112
+ onChangeText={(txt) => {
113
+ isInternalChange.current = true;
114
+ onChangeText && onChangeText(txt);
115
+ }}
97
116
  value={value}
98
- editable={type !== 'readOnly'}
117
+ editable={!isReadOnly}
99
118
  secureTextEntry={type === "password"}
100
- onFocus={() => { setIsFocused(true); onFocus }}
119
+ onFocus={() => { setIsFocused(true); onFocus && onFocus(); }}
101
120
  onBlur={() => setIsFocused(false)}
102
121
  underlineColorAndroid="transparent"
103
- returnKeyType={returnKeyType}
104
- dataSet={{ lpignore: "true" }}
105
122
  />
106
123
  </Animated.View>
107
124
  </View>
@@ -109,20 +126,14 @@ export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeTe
109
126
  };
110
127
 
111
128
  const styles = StyleSheet.create({
112
- container: {
113
- width: "100%",
114
- alignSelf: 'stretch',
115
- },
129
+ container: { width: "100%", alignSelf: 'stretch' },
116
130
  wrapper: {
117
131
  width: '100%',
118
132
  height: 48,
119
- flexDirection: 'row', // Asegura que el TextInput interno tenga un eje claro
133
+ flexDirection: 'row',
120
134
  alignItems: 'center',
121
135
  borderWidth: 1,
136
+ overflow: 'hidden'
122
137
  },
123
- textBox: {
124
- flex: 1, // Esto es lo más importante: ocupa todo el espacio sobrante
125
- height: '100%',
126
- width: '100%',
127
- },
138
+ textBox: { flex: 1, height: '100%', width: '100%' },
128
139
  });
@@ -0,0 +1,126 @@
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";
9
+ import { useTheme } from "../context/CDSThemeContext";
10
+
11
+ export const CDSTooltip = ({ message, visible, onDismiss, targetPosition = { x: 0, y: 0 }, arrowAlign = 'center' }) => {
12
+ const { theme } = useTheme();
13
+ const opacity = useSharedValue(0);
14
+ const scale = useSharedValue(1);
15
+
16
+ useEffect(() => {
17
+ if (visible) {
18
+ opacity.value = withTiming(1, { duration: 200 });
19
+ // scale.value = withSpring(1, { damping: 12 });
20
+ } else {
21
+ opacity.value = withTiming(0, { duration: 200 });
22
+ // scale.value = withTiming(0.8);
23
+ }
24
+ }, [visible]);
25
+
26
+ const animatedStyle = useAnimatedStyle(() => ({
27
+ opacity: opacity.value,
28
+ // transform: [{ scale: scale.value }],
29
+ }));
30
+
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
+ };
39
+
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;
44
+
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
+ }
52
+
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
+ };
59
+
60
+ if (!visible && opacity.value === 0) return null;
61
+
62
+ return (
63
+ <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
+ ]} />
84
+
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>
98
+ </Modal>
99
+ );
100
+ };
101
+
102
+ 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
+ },
113
+ arrowUp: {
114
+ position: 'absolute',
115
+ top: -8,
116
+ width: 0,
117
+ height: 0,
118
+ borderLeftWidth: 8,
119
+ borderRightWidth: 8,
120
+ borderBottomWidth: 8,
121
+ borderStyle: 'solid',
122
+ backgroundColor: 'transparent',
123
+ borderLeftColor: 'transparent',
124
+ borderRightColor: 'transparent',
125
+ }
126
+ });
package/index.js CHANGED
@@ -25,6 +25,7 @@ export {CDSSnackBar} from './components/CDSSnackBar';
25
25
  export {CDSImageButton} from './components/CDSImageButton'
26
26
  export {CDSImageButtonGroup} from './components/CDSImageButtonGroup'
27
27
  export {CDSCheckbox} from './components/CDSCheckbox';
28
+ export {CDSTooltip} from './components/CDSTooltip';
28
29
 
29
30
 
30
31
  export {CDSThemeProvider, useTheme} from './context/CDSThemeContext';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cdslibrary",
3
3
  "license": "0BSD",
4
- "version": "1.2.85",
4
+ "version": "1.2.87",
5
5
  "main": "index.js",
6
6
  "author": "Nat Viramontes",
7
7
  "description": "A library of components for the CDS project",
@@ -54,7 +54,7 @@ export const CDSsemanticColors = {
54
54
  overlay: CDSprimitiveColors.overlay.light,
55
55
  scroll: CDSprimitiveColors.neutral[400],
56
56
  snackbar: CDSprimitiveColors.neutral[700],
57
- tooltip: CDSprimitiveColors.neutral[500],
57
+ tooltip: CDSprimitiveColors.neutral[600],
58
58
  progress: CDSprimitiveColors.brand[600],
59
59
  progressbarBg: CDSprimitiveColors.neutral[400],
60
60
  display: CDSprimitiveColors.neutral[800],