cdslibrary 1.2.88 → 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
@@ -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
- 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>
@@ -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: 24,
300
+ height: 32,
263
301
  },
264
302
  actionsContainer: {
265
303
  typeBottomSheet: {
266
304
  flexDirection: "column",
267
- width: "100%",
305
+ width: "auto",
268
306
  gap: 8,
269
307
  },
270
308
  typeDrawer: {
271
309
  flexDirection: "row-reverse",
272
- width: "100%",
310
+ width: "auto",
273
311
  gap: 8,
274
312
  marginTop: 'auto',
275
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={{
@@ -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
  ]}>
@@ -65,7 +65,7 @@ export const CDSImageButtonGroup = ({ array, onSelect, isCentered }) => {
65
65
  />
66
66
  ))}
67
67
  </ScrollView>
68
- </Animated.View>
68
+ </View>
69
69
  );
70
70
  }
71
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
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cdslibrary",
3
3
  "license": "0BSD",
4
- "version": "1.2.88",
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",