cdslibrary 1.2.78 → 1.2.80
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/CDSAccordion.jsx +1 -1
- package/components/CDSBottomSheet.jsx +74 -84
- package/components/CDSButtonGroup.jsx +2 -0
- package/components/CDSInput.jsx +41 -30
- package/components/CDSLoader.jsx +42 -26
- package/components/CDSNavBar.jsx +66 -41
- package/components/CDSSelect.jsx +8 -11
- package/components/CDSTable.jsx +47 -34
- package/package.json +1 -1
|
@@ -32,19 +32,17 @@ const bottomSheetRender = ({
|
|
|
32
32
|
const { theme } = useTheme();
|
|
33
33
|
const isMobile = theme.isMobile;
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
const startPos = isMobile ? height : width; // Abajo en mobile, Derecha en desktop
|
|
35
|
+
const startPos = isMobile ? height : width;
|
|
37
36
|
const translation = useSharedValue(startPos);
|
|
38
37
|
const opacity = useSharedValue(0);
|
|
39
38
|
|
|
40
|
-
// --- 2. Funciones de Cierre (Wrapper para JS Thread) ---
|
|
41
39
|
const triggerOnFinish = useCallback(() => {
|
|
42
40
|
if (onFinish) onFinish();
|
|
43
41
|
if (primaryButtonOnPress) primaryButtonOnPress();
|
|
44
42
|
}, [onFinish, primaryButtonOnPress]);
|
|
45
43
|
|
|
46
44
|
const handleClose = useCallback(() => {
|
|
47
|
-
"worklet";
|
|
45
|
+
"worklet";
|
|
48
46
|
translation.value = withTiming(startPos, { duration: 300 }, (finished) => {
|
|
49
47
|
if (finished) {
|
|
50
48
|
runOnJS(triggerOnFinish)();
|
|
@@ -53,18 +51,15 @@ const bottomSheetRender = ({
|
|
|
53
51
|
opacity.value = withTiming(0, { duration: 300 });
|
|
54
52
|
}, [startPos, triggerOnFinish]);
|
|
55
53
|
|
|
56
|
-
// --- 3. Lógica del Gesto (Solo Mobile) ---
|
|
57
54
|
const pan = Gesture.Pan()
|
|
58
|
-
.enabled(isMobile)
|
|
55
|
+
.enabled(isMobile)
|
|
59
56
|
.onUpdate((event) => {
|
|
60
|
-
// Solo permitir arrastrar hacia abajo (valores positivos)
|
|
61
57
|
if (event.translationY > 0) {
|
|
62
58
|
translation.value = event.translationY;
|
|
63
59
|
}
|
|
64
60
|
})
|
|
65
61
|
.onEnd((event) => {
|
|
66
|
-
|
|
67
|
-
if (event.translationY > 200 || event.velocityY > 2000) {
|
|
62
|
+
if (event.translationY > 150 || event.velocityY > 2000) {
|
|
68
63
|
handleClose();
|
|
69
64
|
} else {
|
|
70
65
|
translation.value = withTiming(0, {
|
|
@@ -74,10 +69,10 @@ const bottomSheetRender = ({
|
|
|
74
69
|
}
|
|
75
70
|
});
|
|
76
71
|
|
|
77
|
-
|
|
72
|
+
const nativeGesture = Gesture.Native();
|
|
73
|
+
|
|
78
74
|
useEffect(() => {
|
|
79
75
|
if (isVisible) {
|
|
80
|
-
// Usamos una curva Bezier suave (out-expo) que se frena al final
|
|
81
76
|
translation.value = withTiming(0, {
|
|
82
77
|
duration: 300,
|
|
83
78
|
easing: Easing.out(Easing.exp),
|
|
@@ -86,7 +81,6 @@ const bottomSheetRender = ({
|
|
|
86
81
|
}
|
|
87
82
|
}, [isVisible]);
|
|
88
83
|
|
|
89
|
-
// --- 5. Estilos Animados ---
|
|
90
84
|
const animatedStyle = useAnimatedStyle(() => {
|
|
91
85
|
return {
|
|
92
86
|
opacity: opacity.value,
|
|
@@ -99,7 +93,6 @@ const bottomSheetRender = ({
|
|
|
99
93
|
});
|
|
100
94
|
|
|
101
95
|
const backdropStyle = useAnimatedStyle(() => {
|
|
102
|
-
// Interpolamos para que el fondo se aclare mientras arrastras
|
|
103
96
|
const currentY = translation.value;
|
|
104
97
|
const opacityVal = interpolate(currentY, [0, height], [1, 0], Extrapolation.CLAMP);
|
|
105
98
|
return {
|
|
@@ -113,62 +106,60 @@ const bottomSheetRender = ({
|
|
|
113
106
|
style={[
|
|
114
107
|
styles.overlay,
|
|
115
108
|
{ backgroundColor: theme.surface.special.overlay },
|
|
116
|
-
backdropStyle
|
|
109
|
+
backdropStyle
|
|
117
110
|
]}
|
|
118
111
|
>
|
|
119
112
|
<Pressable onPress={() => runOnJS(handleClose)()} style={{ flex: 1 }} />
|
|
120
113
|
</Animated.View>
|
|
121
114
|
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{/* --- NUEVO: La línea visual (Handle) ---
|
|
144
|
-
Se agrega position absolute para NO afectar tus paddings/espacios originales */}
|
|
145
|
-
{isMobile && (
|
|
115
|
+
<Animated.View
|
|
116
|
+
style={[
|
|
117
|
+
isMobile ? styles.container.typeBottomSheet : styles.container.typeDrawer,
|
|
118
|
+
animatedStyle,
|
|
119
|
+
{
|
|
120
|
+
backgroundColor: theme.surface.neutral.primary,
|
|
121
|
+
paddingTop: (hasClose ? theme.space['2xl'] : theme.space.xl),
|
|
122
|
+
gap: theme.space.md, // Mantengo tu gap original
|
|
123
|
+
...(isMobile ? {
|
|
124
|
+
borderTopLeftRadius: theme.radius.lg,
|
|
125
|
+
borderTopRightRadius: theme.radius.lg,
|
|
126
|
+
} : {
|
|
127
|
+
borderBottomLeftRadius: theme.radius.lg,
|
|
128
|
+
borderTopLeftRadius: theme.radius.lg,
|
|
129
|
+
paddingBottom: theme.space.md,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
]}
|
|
133
|
+
>
|
|
134
|
+
{isMobile && (
|
|
135
|
+
<GestureDetector gesture={pan}>
|
|
146
136
|
<View style={styles.handleBarContainer}>
|
|
147
137
|
<View style={[styles.handleBarLine, { backgroundColor: theme.outline.neutral.tertiary }]} />
|
|
148
138
|
</View>
|
|
149
|
-
|
|
139
|
+
</GestureDetector>
|
|
140
|
+
)}
|
|
150
141
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)}
|
|
142
|
+
{hasClose && (
|
|
143
|
+
<MaterialIcons
|
|
144
|
+
name="close"
|
|
145
|
+
size={theme.typography.icon.lg}
|
|
146
|
+
color={theme.text.neutral.primary}
|
|
147
|
+
onPress={() => runOnJS(handleClose)()}
|
|
148
|
+
style={{ position: "absolute", right: theme.space.md, top: theme.space.md, zIndex: 10 }}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
161
151
|
|
|
162
|
-
|
|
152
|
+
{!!title && <Text style={[theme.typography.h3, { marginHorizontal: theme.space.md }]}>{title}</Text>}
|
|
163
153
|
|
|
164
|
-
|
|
154
|
+
<View style={styles.scrollWrapper}>
|
|
155
|
+
<GestureDetector gesture={Gesture.Simultaneous(nativeGesture)}>
|
|
165
156
|
<ScrollView
|
|
166
157
|
ref={ref}
|
|
167
158
|
style={styles.scrollArea}
|
|
168
|
-
contentContainerStyle={[styles.scrollContent, { paddingBottom: theme.space.xl }]}
|
|
159
|
+
contentContainerStyle={[styles.scrollContent, { paddingBottom: theme.space.xl }]} // Tu padding original
|
|
169
160
|
showsVerticalScrollIndicator={true}
|
|
170
161
|
bounces={true}
|
|
171
|
-
|
|
162
|
+
nestedScrollEnabled={true}
|
|
172
163
|
scrollEventThrottle={16}
|
|
173
164
|
>
|
|
174
165
|
{!!description && (
|
|
@@ -178,33 +169,32 @@ const bottomSheetRender = ({
|
|
|
178
169
|
)}
|
|
179
170
|
{customSlot}
|
|
180
171
|
</ScrollView>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
172
|
+
</GestureDetector>
|
|
173
|
+
<LinearGradient
|
|
174
|
+
colors={['transparent', theme.surface.neutral.primary]}
|
|
175
|
+
style={[styles.fadeGradient, { pointerEvents: 'none' }]}
|
|
176
|
+
/>
|
|
177
|
+
</View>
|
|
178
|
+
|
|
179
|
+
{type !== "informative" && (
|
|
180
|
+
<View style={[isMobile ? styles.actionsContainer.typeBottomSheet : styles.actionsContainer.typeDrawer, { paddingHorizontal: theme.space.md, paddingVertical: theme.space.lg }]}>
|
|
181
|
+
<CDSButton
|
|
182
|
+
label={primaryButtonLabel}
|
|
183
|
+
onPress={() => runOnJS(handleClose)()}
|
|
184
184
|
/>
|
|
185
|
+
{secondaryButtonLabel && (<CDSButton
|
|
186
|
+
label={secondaryButtonLabel}
|
|
187
|
+
type="ghost"
|
|
188
|
+
onPress={() => runOnJS(handleClose)()}
|
|
189
|
+
/>)}
|
|
185
190
|
</View>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
<View style={[isMobile ? styles.actionsContainer.typeBottomSheet : styles.actionsContainer.typeDrawer, { paddingHorizontal: theme.space.md, paddingVertical: theme.space.lg }]}>
|
|
189
|
-
<CDSButton
|
|
190
|
-
label={primaryButtonLabel}
|
|
191
|
-
onPress={() => runOnJS(handleClose)()}
|
|
192
|
-
/>
|
|
193
|
-
{secondaryButtonLabel && (<CDSButton
|
|
194
|
-
label={secondaryButtonLabel}
|
|
195
|
-
type="ghost"
|
|
196
|
-
onPress={() => runOnJS(handleClose)()}
|
|
197
|
-
/>)}
|
|
198
|
-
</View>
|
|
199
|
-
)}
|
|
200
|
-
</Animated.View>
|
|
201
|
-
</GestureDetector>
|
|
191
|
+
)}
|
|
192
|
+
</Animated.View>
|
|
202
193
|
</>
|
|
203
194
|
);
|
|
204
195
|
};
|
|
205
196
|
|
|
206
197
|
const styles = StyleSheet.create({
|
|
207
|
-
// --- Estilos originales intactos ---
|
|
208
198
|
overlay: {
|
|
209
199
|
position: "absolute",
|
|
210
200
|
top: 0,
|
|
@@ -215,13 +205,15 @@ const styles = StyleSheet.create({
|
|
|
215
205
|
},
|
|
216
206
|
container: {
|
|
217
207
|
typeBottomSheet: {
|
|
218
|
-
|
|
208
|
+
flexShrink: 1,
|
|
209
|
+
maxHeight: height * 0.83,
|
|
219
210
|
width: "100%",
|
|
220
211
|
position: "absolute",
|
|
221
212
|
bottom: 0,
|
|
222
213
|
left: 0,
|
|
223
214
|
right: 0,
|
|
224
215
|
zIndex: 99,
|
|
216
|
+
...Platform.select({web:{maxHeight: '95%'}})
|
|
225
217
|
},
|
|
226
218
|
typeDrawer: {
|
|
227
219
|
position: "absolute",
|
|
@@ -236,15 +228,11 @@ const styles = StyleSheet.create({
|
|
|
236
228
|
},
|
|
237
229
|
},
|
|
238
230
|
scrollWrapper: {
|
|
239
|
-
|
|
240
|
-
flex: 1,
|
|
241
|
-
position: 'relative',
|
|
231
|
+
flexShrink: 1, // CLAVE: Permite encogerse si el contenido es mucho
|
|
242
232
|
width: '100%',
|
|
243
|
-
paddingBottom: 1
|
|
244
233
|
},
|
|
245
234
|
scrollArea: {
|
|
246
|
-
flex: 1
|
|
247
|
-
width: '100%',
|
|
235
|
+
width: '100%', // Quitamos el flex: 1 de aquí para que sea dinámico
|
|
248
236
|
},
|
|
249
237
|
scrollContent: {
|
|
250
238
|
gap: 16,
|
|
@@ -269,13 +257,15 @@ const styles = StyleSheet.create({
|
|
|
269
257
|
marginTop: 'auto',
|
|
270
258
|
},
|
|
271
259
|
},
|
|
272
|
-
// --- NUEVOS ESTILOS PARA EL HANDLE ---
|
|
273
260
|
handleBarContainer: {
|
|
274
261
|
position: 'absolute',
|
|
275
|
-
top:
|
|
276
|
-
|
|
262
|
+
top: 0,
|
|
263
|
+
left: 0,
|
|
264
|
+
right: 0,
|
|
265
|
+
height: 30,
|
|
277
266
|
alignItems: 'center',
|
|
278
|
-
|
|
267
|
+
justifyContent: 'center',
|
|
268
|
+
zIndex: 20,
|
|
279
269
|
},
|
|
280
270
|
handleBarLine: {
|
|
281
271
|
width: 40,
|
|
@@ -45,11 +45,13 @@ export const CDSButtonGroup = ({ options, onSelect, label = 'Label', selected =
|
|
|
45
45
|
const styles = StyleSheet.create({
|
|
46
46
|
container: {
|
|
47
47
|
width: '100%',
|
|
48
|
+
|
|
48
49
|
},
|
|
49
50
|
|
|
50
51
|
actionsContainer: {
|
|
51
52
|
flexDirection: 'row',
|
|
52
53
|
alignItems: 'center',
|
|
53
54
|
justifyContent: 'center',
|
|
55
|
+
width: '100%'
|
|
54
56
|
},
|
|
55
57
|
});
|
package/components/CDSInput.jsx
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef } from "react";
|
|
2
2
|
import { View, Text, TextInput, StyleSheet, Platform } from "react-native";
|
|
3
|
-
import Animated, {
|
|
4
|
-
useAnimatedStyle,
|
|
5
|
-
useSharedValue,
|
|
6
|
-
withSequence,
|
|
7
|
-
withTiming,
|
|
8
|
-
interpolateColor
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withSequence,
|
|
7
|
+
withTiming,
|
|
8
|
+
interpolateColor
|
|
9
9
|
} from "react-native-reanimated";
|
|
10
10
|
import { useTheme } from "../context/CDSThemeContext";
|
|
11
11
|
|
|
12
|
-
export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeText, animationTrigger }) => {
|
|
12
|
+
export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeText, animationTrigger, returnKeyType, onFocus }) => {
|
|
13
13
|
const { theme } = useTheme();
|
|
14
14
|
const [isFocused, setIsFocused] = useState(false);
|
|
15
|
-
|
|
16
|
-
// Valor animado para el "flash" (0 a 1)
|
|
15
|
+
|
|
17
16
|
const flashValue = useSharedValue(0);
|
|
18
|
-
// Ref para detectar si el cambio vino del teclado o del código
|
|
19
17
|
const isInternalChange = useRef(false);
|
|
20
18
|
|
|
21
19
|
useEffect(() => {
|
|
@@ -29,8 +27,8 @@ export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeTe
|
|
|
29
27
|
}, [value, animationTrigger]);
|
|
30
28
|
|
|
31
29
|
const handleTextChange = (inputText) => {
|
|
32
|
-
isInternalChange.current = true;
|
|
33
|
-
|
|
30
|
+
isInternalChange.current = true;
|
|
31
|
+
|
|
34
32
|
if (keyboard === "numeric" || keyboard === "decimal-pad") {
|
|
35
33
|
let cleaned = inputText.replace(',', '.').replace(/[^0-9.]/g, "");
|
|
36
34
|
const parts = cleaned.split('.');
|
|
@@ -41,49 +39,54 @@ export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeTe
|
|
|
41
39
|
}
|
|
42
40
|
};
|
|
43
41
|
|
|
44
|
-
// Estilo animado para el borde y el fondo
|
|
45
42
|
const animatedBoxStyle = useAnimatedStyle(() => {
|
|
46
43
|
const borderColor = interpolateColor(
|
|
47
44
|
flashValue.value,
|
|
48
45
|
[0, 1],
|
|
49
46
|
[
|
|
50
47
|
isFocused ? theme.outline.neutral.focus : value ? theme.outline.neutral.tertiaryVariant : theme.outline.neutral.primary,
|
|
51
|
-
theme.outline.neutral.focus
|
|
48
|
+
theme.outline.neutral.focus
|
|
52
49
|
]
|
|
53
50
|
);
|
|
54
51
|
|
|
55
52
|
return {
|
|
56
53
|
borderColor: borderColor,
|
|
57
|
-
|
|
58
|
-
|
|
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
57
|
};
|
|
60
58
|
});
|
|
61
|
-
|
|
59
|
+
|
|
62
60
|
return (
|
|
63
|
-
<View style={
|
|
61
|
+
<View style={styles.container}>
|
|
64
62
|
{label && (
|
|
65
|
-
<Text style={[
|
|
63
|
+
<Text style={[
|
|
64
|
+
theme.typography.label,
|
|
65
|
+
{ color: theme.text.neutral.primary, marginBottom: theme.space.xs }
|
|
66
|
+
]}>
|
|
66
67
|
{label}
|
|
67
68
|
</Text>
|
|
68
69
|
)}
|
|
69
70
|
|
|
70
71
|
<Animated.View style={[
|
|
71
|
-
styles.wrapper,
|
|
72
|
+
styles.wrapper,
|
|
72
73
|
animatedBoxStyle,
|
|
73
74
|
{
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
backgroundColor: type === 'readOnly' ? theme.surface.neutral.primaryVariant : theme.surface.neutral.primary,
|
|
76
|
+
borderRadius: theme.radius.sm,
|
|
76
77
|
}
|
|
77
78
|
]}>
|
|
78
79
|
<TextInput
|
|
79
80
|
style={[
|
|
80
81
|
styles.textBox,
|
|
81
82
|
theme.typography.inputText.value,
|
|
82
|
-
{
|
|
83
|
+
{
|
|
83
84
|
paddingHorizontal: theme.space.sm,
|
|
84
|
-
color:
|
|
85
|
-
//
|
|
86
|
-
|
|
85
|
+
color: type === 'readOnly' ? theme.text.neutral.secondary : theme.text.neutral.primary,
|
|
86
|
+
// Fix para web:
|
|
87
|
+
...Platform.select({
|
|
88
|
+
web: { outlineStyle: 'none' }
|
|
89
|
+
})
|
|
87
90
|
},
|
|
88
91
|
]}
|
|
89
92
|
placeholder={placeholder}
|
|
@@ -93,9 +96,11 @@ export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeTe
|
|
|
93
96
|
value={value}
|
|
94
97
|
editable={type !== 'readOnly'}
|
|
95
98
|
secureTextEntry={type === "password"}
|
|
96
|
-
onFocus={() => setIsFocused(true)}
|
|
99
|
+
onFocus={() => { setIsFocused(true); onFocus }}
|
|
97
100
|
onBlur={() => setIsFocused(false)}
|
|
98
101
|
underlineColorAndroid="transparent"
|
|
102
|
+
returnKeyType={returnKeyType}
|
|
103
|
+
dataSet={{ lpignore: "true" }}
|
|
99
104
|
/>
|
|
100
105
|
</Animated.View>
|
|
101
106
|
</View>
|
|
@@ -103,14 +108,20 @@ export const CDSInput = ({ label, type, keyboard, placeholder, value, onChangeTe
|
|
|
103
108
|
};
|
|
104
109
|
|
|
105
110
|
const styles = StyleSheet.create({
|
|
106
|
-
container: {
|
|
111
|
+
container: {
|
|
112
|
+
width: "100%",
|
|
113
|
+
alignSelf: 'stretch',
|
|
114
|
+
},
|
|
107
115
|
wrapper: {
|
|
116
|
+
width: '100%',
|
|
108
117
|
height: 48,
|
|
118
|
+
flexDirection: 'row', // Asegura que el TextInput interno tenga un eje claro
|
|
119
|
+
alignItems: 'center',
|
|
109
120
|
borderWidth: 1,
|
|
110
|
-
justifyContent: 'center',
|
|
111
121
|
},
|
|
112
122
|
textBox: {
|
|
113
|
-
flex: 1,
|
|
123
|
+
flex: 1, // Esto es lo más importante: ocupa todo el espacio sobrante
|
|
114
124
|
height: '100%',
|
|
125
|
+
width: '100%',
|
|
115
126
|
},
|
|
116
127
|
});
|
package/components/CDSLoader.jsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React, { useEffect, useState } from "react";
|
|
2
|
-
import { View, StyleSheet, Modal, Text } from "react-native";
|
|
2
|
+
import { View, StyleSheet, Modal, Text, Platform } from "react-native";
|
|
3
3
|
import LottieView from "lottie-react-native";
|
|
4
4
|
import Animated, {
|
|
5
5
|
useSharedValue,
|
|
6
6
|
useAnimatedStyle,
|
|
7
7
|
withTiming,
|
|
8
|
-
withDelay,
|
|
8
|
+
withDelay,
|
|
9
9
|
runOnJS
|
|
10
10
|
} from "react-native-reanimated";
|
|
11
11
|
import { useTheme } from "../context/CDSThemeContext";
|
|
@@ -18,11 +18,8 @@ export const CDSLoader = ({ visible = false, message }) => {
|
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
if (visible) {
|
|
20
20
|
setShouldRender(true);
|
|
21
|
-
// Entrada rápida para que no se sienta lag al empezar
|
|
22
21
|
opacity.value = withTiming(1, { duration: 300 });
|
|
23
22
|
} else {
|
|
24
|
-
// 1. Espera 800ms antes de hacer nada (Sostenido)
|
|
25
|
-
// 2. Desvanece lentamente en 1200ms
|
|
26
23
|
opacity.value = withDelay(
|
|
27
24
|
1000,
|
|
28
25
|
withTiming(0, { duration: 1200 }, (isFinished) => {
|
|
@@ -38,26 +35,38 @@ export const CDSLoader = ({ visible = false, message }) => {
|
|
|
38
35
|
opacity: opacity.value,
|
|
39
36
|
}));
|
|
40
37
|
|
|
41
|
-
if (!shouldRender
|
|
38
|
+
if (!shouldRender) return null;
|
|
39
|
+
|
|
42
40
|
return (
|
|
43
|
-
<Modal transparent visible={
|
|
44
|
-
<Animated.View style={[
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
>
|
|
41
|
+
<Modal transparent visible={true} animationType="none">
|
|
42
|
+
<Animated.View style={[
|
|
43
|
+
styles.overlay,
|
|
44
|
+
animatedStyle,
|
|
45
|
+
{ backgroundColor: theme.surface.special.overlay }
|
|
46
|
+
]}>
|
|
47
|
+
<View style={styles.loaderContainer}>
|
|
51
48
|
<LottieView
|
|
52
49
|
autoPlay
|
|
53
50
|
loop
|
|
54
51
|
source={require("../assets/animations/animationLoaderWhite.json")}
|
|
52
|
+
// CLAVE: Definimos un tamaño que funcione en ambos,
|
|
53
|
+
// pero limitado para que en Web no explote
|
|
54
|
+
style={styles.lottie}
|
|
55
55
|
resizeMode="contain"
|
|
56
56
|
/>
|
|
57
|
-
{message &&
|
|
58
|
-
style={[
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
{message && (
|
|
58
|
+
<Text style={[
|
|
59
|
+
theme.typography.semiBold.md,
|
|
60
|
+
{
|
|
61
|
+
textAlign: 'center',
|
|
62
|
+
color: theme.text.special.loader,
|
|
63
|
+
marginTop: 10,
|
|
64
|
+
paddingHorizontal: 20
|
|
65
|
+
}
|
|
66
|
+
]}>
|
|
67
|
+
{message}
|
|
68
|
+
</Text>
|
|
69
|
+
)}
|
|
61
70
|
</View>
|
|
62
71
|
</Animated.View>
|
|
63
72
|
</Modal>
|
|
@@ -66,19 +75,26 @@ export const CDSLoader = ({ visible = false, message }) => {
|
|
|
66
75
|
|
|
67
76
|
const styles = StyleSheet.create({
|
|
68
77
|
overlay: {
|
|
69
|
-
|
|
70
|
-
width: '100%',
|
|
78
|
+
flex: 1,
|
|
71
79
|
justifyContent: "center",
|
|
72
80
|
alignItems: "center",
|
|
73
|
-
zIndex: 1000,
|
|
74
81
|
},
|
|
75
82
|
loaderContainer: {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
minHeight: 180,
|
|
80
|
-
maxWidth: 400,
|
|
83
|
+
// Evita que el contenedor se estire en pantallas grandes (Web)
|
|
84
|
+
width: '100%',
|
|
85
|
+
maxWidth: 300,
|
|
81
86
|
justifyContent: "center",
|
|
82
87
|
alignItems: "center",
|
|
83
88
|
},
|
|
89
|
+
lottie: {
|
|
90
|
+
// Tamaño controlado para móvil y Web
|
|
91
|
+
width: 150,
|
|
92
|
+
height: 150,
|
|
93
|
+
// En Web a veces necesita una alineación extra
|
|
94
|
+
...Platform.select({
|
|
95
|
+
web: {
|
|
96
|
+
alignSelf: 'center',
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
},
|
|
84
100
|
});
|
package/components/CDSNavBar.jsx
CHANGED
|
@@ -1,56 +1,76 @@
|
|
|
1
|
-
|
|
2
|
-
import { View, Text, StyleSheet, Image, TouchableOpacity } from "react-native";
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { View, Text, StyleSheet, Image, TouchableOpacity } from "react-native";
|
|
5
2
|
import { MaterialIcons } from "@expo/vector-icons";
|
|
6
3
|
import { useTheme } from "../context/CDSThemeContext";
|
|
7
4
|
import { useNavigation } from "@react-navigation/native";
|
|
8
5
|
|
|
9
6
|
export const CDSNavBar = ({
|
|
10
|
-
|
|
11
7
|
type,
|
|
12
8
|
isExpanded,
|
|
13
9
|
hasClose,
|
|
14
10
|
title,
|
|
15
|
-
hasBack,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const isMobile = theme.isMobile
|
|
19
|
-
|
|
11
|
+
hasBack,
|
|
12
|
+
}) => {
|
|
13
|
+
const { theme, isDarkMode } = useTheme();
|
|
20
14
|
const navigation = useNavigation();
|
|
21
15
|
|
|
22
16
|
const handleBackPress = () => {
|
|
23
|
-
if (hasBack)
|
|
24
|
-
navigation.goBack();
|
|
25
|
-
}
|
|
17
|
+
if (hasBack) navigation.goBack();
|
|
26
18
|
};
|
|
27
19
|
|
|
28
20
|
const handleClosePress = () => {
|
|
29
|
-
if (hasClose)
|
|
30
|
-
navigation.goBack();
|
|
31
|
-
}
|
|
21
|
+
if (hasClose) navigation.goBack();
|
|
32
22
|
};
|
|
33
23
|
|
|
34
24
|
return (
|
|
35
|
-
<View style={[styles.container, {
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
<View style={[styles.container, {
|
|
26
|
+
backgroundColor: theme.surface.neutral.primary,
|
|
27
|
+
borderBottomColor: theme.outline.neutral.primary // Usando tu variable de tema
|
|
28
|
+
}]}>
|
|
29
|
+
|
|
30
|
+
{/* Contenedor del título o logo (Capa absoluta para centrado perfecto) */}
|
|
31
|
+
<View style={styles.centerContainer}>
|
|
32
|
+
{type === 'title' ? (
|
|
33
|
+
<Text
|
|
34
|
+
numberOfLines={1}
|
|
35
|
+
style={[styles.title, theme.typography.semiBold.lg, { color: theme.text.neutral.primary }]}
|
|
36
|
+
>
|
|
37
|
+
{title || 'title'}
|
|
38
|
+
</Text>
|
|
39
|
+
) : (
|
|
40
|
+
<Image
|
|
41
|
+
source={
|
|
42
|
+
isDarkMode
|
|
43
|
+
? require("../assets/images/logoMonteTW.png")
|
|
44
|
+
: require("../assets/images/logoMonte.png")
|
|
45
|
+
}
|
|
46
|
+
style={styles.logo}
|
|
47
|
+
resizeMode="contain"
|
|
48
|
+
/>
|
|
49
|
+
)}
|
|
50
|
+
</View>
|
|
51
|
+
|
|
52
|
+
{/* Icono Izquierdo */}
|
|
53
|
+
<TouchableOpacity onPress={handleBackPress} disabled={!hasBack}>
|
|
54
|
+
<MaterialIcons
|
|
55
|
+
name="arrow-back-ios"
|
|
56
|
+
size={theme.typography.icon.md}
|
|
57
|
+
color={theme.text.neutral.primary}
|
|
58
|
+
style={{ opacity: hasBack ? 1 : 0 }}
|
|
59
|
+
/>
|
|
38
60
|
</TouchableOpacity>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<TouchableOpacity onPress={handleClosePress}>
|
|
49
|
-
<MaterialIcons name="close" size={theme.typography.icon.md} color={theme.text.neutral.primary} style={{ cursor: hasClose ? 'pointer' : 'default', opacity: hasClose ? 1 : 0 }} />
|
|
61
|
+
|
|
62
|
+
{/* Icono Derecho */}
|
|
63
|
+
<TouchableOpacity onPress={handleClosePress} disabled={!hasClose}>
|
|
64
|
+
<MaterialIcons
|
|
65
|
+
name="close"
|
|
66
|
+
size={theme.typography.icon.md}
|
|
67
|
+
color={theme.text.neutral.primary}
|
|
68
|
+
style={{ opacity: hasClose ? 1 : 0 }}
|
|
69
|
+
/>
|
|
50
70
|
</TouchableOpacity>
|
|
51
71
|
</View>
|
|
52
|
-
)
|
|
53
|
-
}
|
|
72
|
+
);
|
|
73
|
+
};
|
|
54
74
|
|
|
55
75
|
const styles = StyleSheet.create({
|
|
56
76
|
container: {
|
|
@@ -61,18 +81,23 @@ const styles = StyleSheet.create({
|
|
|
61
81
|
justifyContent: 'space-between',
|
|
62
82
|
paddingHorizontal: 16,
|
|
63
83
|
borderBottomWidth: 1,
|
|
64
|
-
borderBottomColor: '#E0E0E0',
|
|
65
84
|
},
|
|
66
|
-
|
|
85
|
+
centerContainer: {
|
|
86
|
+
// Esta capa flota detrás de los botones y ocupa todo el ancho
|
|
87
|
+
position: 'absolute',
|
|
88
|
+
left: 0,
|
|
89
|
+
right: 0,
|
|
90
|
+
top: 0,
|
|
91
|
+
bottom: 0,
|
|
92
|
+
justifyContent: 'center',
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
paddingHorizontal: 48, // Evita que el texto choque con los iconos
|
|
95
|
+
},
|
|
67
96
|
title: {
|
|
68
|
-
width: '100%',
|
|
69
97
|
textAlign: 'center',
|
|
70
98
|
},
|
|
71
|
-
|
|
72
99
|
logo: {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
resizeMode: 'contain',
|
|
100
|
+
height: '60%',
|
|
101
|
+
width: 150, // Ajusta según tu logo
|
|
76
102
|
},
|
|
77
|
-
|
|
78
|
-
});
|
|
103
|
+
});
|
package/components/CDSSelect.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
|
-
View, Text, TouchableOpacity, StyleSheet,
|
|
3
|
+
View, Text, TouchableOpacity, StyleSheet, ScrollView,
|
|
4
4
|
Animated, Pressable, Platform
|
|
5
5
|
} from "react-native";
|
|
6
6
|
import { MaterialIcons } from "@expo/vector-icons";
|
|
@@ -81,7 +81,6 @@ export const CDSSelect = ({ label, options = [], onSelect, selectedValue = null
|
|
|
81
81
|
if (onSelect) onSelect(item.value);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
// Estructura idéntica a CardFeedback para mantener consistencia
|
|
85
84
|
const animatedLayout = {
|
|
86
85
|
maxHeight: animatedValue.interpolate({
|
|
87
86
|
inputRange: [0, 1],
|
|
@@ -125,7 +124,7 @@ export const CDSSelect = ({ label, options = [], onSelect, selectedValue = null
|
|
|
125
124
|
<Animated.View
|
|
126
125
|
style={[
|
|
127
126
|
styles.dropdownInline,
|
|
128
|
-
animatedLayout,
|
|
127
|
+
animatedLayout,
|
|
129
128
|
{
|
|
130
129
|
backgroundColor: theme.surface.neutral.primary,
|
|
131
130
|
borderRadius: theme.radius.sm,
|
|
@@ -134,19 +133,17 @@ export const CDSSelect = ({ label, options = [], onSelect, selectedValue = null
|
|
|
134
133
|
}
|
|
135
134
|
]}
|
|
136
135
|
>
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
keyExtractor={(item) => item.value.toString()}
|
|
140
|
-
renderItem={({ item }) => (
|
|
136
|
+
<ScrollView bounces={false} nestedScrollEnabled={true}>
|
|
137
|
+
{options.map((item, index) => (
|
|
141
138
|
<SelectOption
|
|
139
|
+
key={item.value?.toString() || index.toString()}
|
|
142
140
|
item={item}
|
|
143
141
|
isSelected={selectedItem?.value === item.value}
|
|
144
142
|
onSelect={handleSelect}
|
|
145
143
|
theme={theme}
|
|
146
144
|
/>
|
|
147
|
-
)}
|
|
148
|
-
|
|
149
|
-
/>
|
|
145
|
+
))}
|
|
146
|
+
</ScrollView>
|
|
150
147
|
</Animated.View>
|
|
151
148
|
)}
|
|
152
149
|
</View>
|
|
@@ -155,7 +152,7 @@ export const CDSSelect = ({ label, options = [], onSelect, selectedValue = null
|
|
|
155
152
|
|
|
156
153
|
const styles = StyleSheet.create({
|
|
157
154
|
mainContainer: { width: '100%' },
|
|
158
|
-
inputContainer: { height: 48, borderWidth: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
155
|
+
inputContainer: { width: '100%', height: 48, borderWidth: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
159
156
|
dropdownInline: {
|
|
160
157
|
borderWidth: 1,
|
|
161
158
|
},
|
package/components/CDSTable.jsx
CHANGED
|
@@ -1,41 +1,51 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, ScrollView, useWindowDimensions } from 'react-native';
|
|
2
|
+
import { View, Text, StyleSheet, ScrollView, useWindowDimensions, Platform } from 'react-native';
|
|
3
3
|
import { useTheme } from '../context/CDSThemeContext';
|
|
4
4
|
|
|
5
5
|
export const CDSTable = ({ columns, data, breakpoint = 500 }) => {
|
|
6
6
|
const { theme } = useTheme();
|
|
7
7
|
const { width: screenWidth } = useWindowDimensions();
|
|
8
8
|
|
|
9
|
-
// Aquí definimos que "Mobile" es cualquier pantalla menor al breakpoint
|
|
10
9
|
const isSmallScreen = screenWidth < breakpoint;
|
|
11
10
|
|
|
12
11
|
const TableContent = (
|
|
13
12
|
<View style={[
|
|
14
13
|
styles.tableContainer,
|
|
15
|
-
{
|
|
16
|
-
borderColor: theme.outline.neutral.primary,
|
|
14
|
+
{
|
|
15
|
+
borderColor: theme.outline.neutral.primary,
|
|
17
16
|
borderRadius: theme.radius.md,
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
// Asegura el 100% en desktop y auto-ajuste en móvil
|
|
18
|
+
width: isSmallScreen ? 'auto' : '100%',
|
|
19
|
+
minWidth: '100%',
|
|
21
20
|
}
|
|
22
21
|
]}>
|
|
23
22
|
{/* Encabezado */}
|
|
24
23
|
<View style={[
|
|
25
|
-
styles.row,
|
|
26
|
-
styles.header,
|
|
27
|
-
{
|
|
24
|
+
styles.row,
|
|
25
|
+
styles.header,
|
|
26
|
+
{
|
|
27
|
+
backgroundColor: theme.surface.neutral.primaryVariant,
|
|
28
|
+
borderBottomColor: theme.outline.neutral.primary
|
|
29
|
+
}
|
|
28
30
|
]}>
|
|
29
31
|
{columns.map((col, index) => (
|
|
30
|
-
<View
|
|
32
|
+
<View
|
|
31
33
|
key={`header-${index}`}
|
|
32
34
|
style={[
|
|
33
|
-
styles.cell,
|
|
35
|
+
styles.cell,
|
|
34
36
|
{ padding: theme.space.sm },
|
|
35
|
-
col.width ? { width: col.width } : { flex: 1 }
|
|
37
|
+
col.width ? { width: col.width } : { flex: 1 },
|
|
38
|
+
// Alinea el contenedor según la propiedad align de la columna
|
|
39
|
+
col.align === 'right' ? { alignItems: 'flex-end' } : { alignItems: 'flex-start' }
|
|
36
40
|
]}
|
|
37
41
|
>
|
|
38
|
-
<Text style={[
|
|
42
|
+
<Text style={[
|
|
43
|
+
theme.typography.semiBold.md,
|
|
44
|
+
{
|
|
45
|
+
color: theme.text.neutral.primary,
|
|
46
|
+
textAlign: col.align || 'left'
|
|
47
|
+
}
|
|
48
|
+
]}>
|
|
39
49
|
{col.title}
|
|
40
50
|
</Text>
|
|
41
51
|
</View>
|
|
@@ -53,15 +63,25 @@ export const CDSTable = ({ columns, data, breakpoint = 500 }) => {
|
|
|
53
63
|
]}
|
|
54
64
|
>
|
|
55
65
|
{columns.map((col, colIndex) => (
|
|
56
|
-
<View
|
|
66
|
+
<View
|
|
57
67
|
key={`cell-${rowIndex}-${colIndex}`}
|
|
58
68
|
style={[
|
|
59
|
-
styles.cell,
|
|
69
|
+
styles.cell,
|
|
60
70
|
{ padding: theme.space.sm },
|
|
61
|
-
col.width ? { width: col.width } : { flex: 1 }
|
|
71
|
+
col.width ? { width: col.width } : { flex: 1 },
|
|
72
|
+
// Alinea el contenedor para móvil (Expo Go)
|
|
73
|
+
col.align === 'right' ? { alignItems: 'flex-end' } : { alignItems: 'flex-start' }
|
|
62
74
|
]}
|
|
63
75
|
>
|
|
64
|
-
<Text style={[
|
|
76
|
+
<Text style={[
|
|
77
|
+
theme.typography.regular.md,
|
|
78
|
+
{
|
|
79
|
+
color: theme.text.neutral.secondary,
|
|
80
|
+
textAlign: col.align || 'left',
|
|
81
|
+
// Tabular-nums asegura que los números tengan el mismo ancho (perfecto para precios)
|
|
82
|
+
fontVariant: col.align === 'right' ? ['tabular-nums'] : [],
|
|
83
|
+
}
|
|
84
|
+
]}>
|
|
65
85
|
{item[col.key]}
|
|
66
86
|
</Text>
|
|
67
87
|
</View>
|
|
@@ -73,11 +93,10 @@ export const CDSTable = ({ columns, data, breakpoint = 500 }) => {
|
|
|
73
93
|
|
|
74
94
|
if (isSmallScreen) {
|
|
75
95
|
return (
|
|
76
|
-
<View style={styles.scrollWrapper}>
|
|
77
|
-
<ScrollView
|
|
78
|
-
horizontal
|
|
79
|
-
showsHorizontalScrollIndicator={
|
|
80
|
-
// Importante: No ponemos alignItems: 'flex-start' aquí para que no muerda el final
|
|
96
|
+
<View style={styles.scrollWrapper}>
|
|
97
|
+
<ScrollView
|
|
98
|
+
horizontal
|
|
99
|
+
showsHorizontalScrollIndicator={false}
|
|
81
100
|
contentContainerStyle={styles.scrollContent}
|
|
82
101
|
>
|
|
83
102
|
{TableContent}
|
|
@@ -98,9 +117,7 @@ const styles = StyleSheet.create({
|
|
|
98
117
|
width: '100%',
|
|
99
118
|
},
|
|
100
119
|
scrollContent: {
|
|
101
|
-
|
|
102
|
-
minWidth: '100%',
|
|
103
|
-
paddingRight: 20, // Espacio extra al final del scroll para que la última columna no pegue
|
|
120
|
+
flexGrow: 1,
|
|
104
121
|
},
|
|
105
122
|
fullWidthContainer: {
|
|
106
123
|
width: '100%',
|
|
@@ -111,19 +128,15 @@ const styles = StyleSheet.create({
|
|
|
111
128
|
},
|
|
112
129
|
row: {
|
|
113
130
|
flexDirection: 'row',
|
|
131
|
+
width: '100%',
|
|
132
|
+
// Importante para que el fondo del header no se corte en scroll horizontal
|
|
133
|
+
minWidth: '100%',
|
|
114
134
|
},
|
|
115
135
|
header: {
|
|
116
136
|
borderBottomWidth: 1,
|
|
117
137
|
},
|
|
118
138
|
cell: {
|
|
119
139
|
justifyContent: 'center',
|
|
120
|
-
//
|
|
121
|
-
minWidth: 100,
|
|
122
|
-
},
|
|
123
|
-
headerText: {
|
|
124
|
-
textAlign: 'center',
|
|
125
|
-
},
|
|
126
|
-
cellText: {
|
|
127
|
-
textAlign: 'center',
|
|
140
|
+
minWidth: 100, // Un poco más de aire para evitar saltos de línea agresivos
|
|
128
141
|
},
|
|
129
142
|
});
|