orn-ui 0.1.1 → 0.2.0
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/README.md +9 -2
- package/lib/commonjs/atoms/PressableScale.js +9 -10
- package/lib/commonjs/atoms/PressableScale.js.map +1 -1
- package/lib/commonjs/atoms/Skeleton.js +96 -0
- package/lib/commonjs/atoms/Skeleton.js.map +1 -0
- package/lib/commonjs/atoms/Transition.js +171 -0
- package/lib/commonjs/atoms/Transition.js.map +1 -0
- package/lib/commonjs/atoms/index.js +20 -0
- package/lib/commonjs/atoms/index.js.map +1 -1
- package/lib/commonjs/molecules/OptionCard.js +55 -22
- package/lib/commonjs/molecules/OptionCard.js.map +1 -1
- package/lib/commonjs/molecules/{ThemeToggle.js → SegmentedControl.js} +36 -36
- package/lib/commonjs/molecules/SegmentedControl.js.map +1 -0
- package/lib/commonjs/molecules/Stepper.js +124 -47
- package/lib/commonjs/molecules/Stepper.js.map +1 -1
- package/lib/commonjs/molecules/index.js +9 -3
- package/lib/commonjs/molecules/index.js.map +1 -1
- package/lib/commonjs/organisms/AlertProvider.js +26 -0
- package/lib/commonjs/organisms/AlertProvider.js.map +1 -1
- package/lib/commonjs/organisms/BottomSheet.js +118 -39
- package/lib/commonjs/organisms/BottomSheet.js.map +1 -1
- package/lib/commonjs/organisms/List.js +66 -44
- package/lib/commonjs/organisms/List.js.map +1 -1
- package/lib/commonjs/organisms/Modal.js +42 -87
- package/lib/commonjs/organisms/Modal.js.map +1 -1
- package/lib/commonjs/organisms/NavigationBar.js +205 -0
- package/lib/commonjs/organisms/NavigationBar.js.map +1 -0
- package/lib/commonjs/organisms/Screen.js +9 -2
- package/lib/commonjs/organisms/Screen.js.map +1 -1
- package/lib/commonjs/organisms/SearchList.js +97 -28
- package/lib/commonjs/organisms/SearchList.js.map +1 -1
- package/lib/commonjs/organisms/Select.js +222 -50
- package/lib/commonjs/organisms/Select.js.map +1 -1
- package/lib/commonjs/organisms/ThemeToggle.js +52 -0
- package/lib/commonjs/organisms/ThemeToggle.js.map +1 -0
- package/lib/commonjs/organisms/Toast.js +8 -22
- package/lib/commonjs/organisms/Toast.js.map +1 -1
- package/lib/commonjs/organisms/ToastProvider.js +24 -0
- package/lib/commonjs/organisms/ToastProvider.js.map +1 -1
- package/lib/commonjs/organisms/Wizard.js +22 -2
- package/lib/commonjs/organisms/Wizard.js.map +1 -1
- package/lib/commonjs/organisms/imperativeBridge.js +28 -0
- package/lib/commonjs/organisms/imperativeBridge.js.map +1 -0
- package/lib/commonjs/organisms/index.js +50 -0
- package/lib/commonjs/organisms/index.js.map +1 -1
- package/lib/commonjs/theme/colors.js +207 -0
- package/lib/commonjs/theme/colors.js.map +1 -0
- package/lib/commonjs/theme/createTheme.js +25 -2
- package/lib/commonjs/theme/createTheme.js.map +1 -1
- package/lib/commonjs/theme/index.js +25 -0
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/commonjs/theme/palettes.js +5 -2
- package/lib/commonjs/theme/palettes.js.map +1 -1
- package/lib/module/atoms/PressableScale.js +9 -10
- package/lib/module/atoms/PressableScale.js.map +1 -1
- package/lib/module/atoms/Skeleton.js +91 -0
- package/lib/module/atoms/Skeleton.js.map +1 -0
- package/lib/module/atoms/Transition.js +165 -0
- package/lib/module/atoms/Transition.js.map +1 -0
- package/lib/module/atoms/index.js +2 -0
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/molecules/OptionCard.js +56 -23
- package/lib/module/molecules/OptionCard.js.map +1 -1
- package/lib/module/molecules/{ThemeToggle.js → SegmentedControl.js} +36 -36
- package/lib/module/molecules/SegmentedControl.js.map +1 -0
- package/lib/module/molecules/Stepper.js +125 -49
- package/lib/module/molecules/Stepper.js.map +1 -1
- package/lib/module/molecules/index.js +2 -2
- package/lib/module/molecules/index.js.map +1 -1
- package/lib/module/organisms/AlertProvider.js +24 -1
- package/lib/module/organisms/AlertProvider.js.map +1 -1
- package/lib/module/organisms/BottomSheet.js +120 -41
- package/lib/module/organisms/BottomSheet.js.map +1 -1
- package/lib/module/organisms/List.js +68 -46
- package/lib/module/organisms/List.js.map +1 -1
- package/lib/module/organisms/Modal.js +44 -89
- package/lib/module/organisms/Modal.js.map +1 -1
- package/lib/module/organisms/NavigationBar.js +200 -0
- package/lib/module/organisms/NavigationBar.js.map +1 -0
- package/lib/module/organisms/Screen.js +9 -2
- package/lib/module/organisms/Screen.js.map +1 -1
- package/lib/module/organisms/SearchList.js +98 -29
- package/lib/module/organisms/SearchList.js.map +1 -1
- package/lib/module/organisms/Select.js +224 -52
- package/lib/module/organisms/Select.js.map +1 -1
- package/lib/module/organisms/ThemeToggle.js +47 -0
- package/lib/module/organisms/ThemeToggle.js.map +1 -0
- package/lib/module/organisms/Toast.js +10 -24
- package/lib/module/organisms/Toast.js.map +1 -1
- package/lib/module/organisms/ToastProvider.js +21 -0
- package/lib/module/organisms/ToastProvider.js.map +1 -1
- package/lib/module/organisms/Wizard.js +23 -3
- package/lib/module/organisms/Wizard.js.map +1 -1
- package/lib/module/organisms/imperativeBridge.js +24 -0
- package/lib/module/organisms/imperativeBridge.js.map +1 -0
- package/lib/module/organisms/index.js +4 -2
- package/lib/module/organisms/index.js.map +1 -1
- package/lib/module/theme/colors.js +194 -0
- package/lib/module/theme/colors.js.map +1 -0
- package/lib/module/theme/createTheme.js +25 -2
- package/lib/module/theme/createTheme.js.map +1 -1
- package/lib/module/theme/index.js +1 -0
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/palettes.js +5 -2
- package/lib/module/theme/palettes.js.map +1 -1
- package/lib/typescript/atoms/PressableScale.d.ts +3 -2
- package/lib/typescript/atoms/PressableScale.d.ts.map +1 -1
- package/lib/typescript/atoms/Skeleton.d.ts +29 -0
- package/lib/typescript/atoms/Skeleton.d.ts.map +1 -0
- package/lib/typescript/atoms/Transition.d.ts +51 -0
- package/lib/typescript/atoms/Transition.d.ts.map +1 -0
- package/lib/typescript/atoms/index.d.ts +4 -0
- package/lib/typescript/atoms/index.d.ts.map +1 -1
- package/lib/typescript/molecules/OptionCard.d.ts +11 -6
- package/lib/typescript/molecules/OptionCard.d.ts.map +1 -1
- package/lib/typescript/molecules/SegmentedControl.d.ts +24 -0
- package/lib/typescript/molecules/SegmentedControl.d.ts.map +1 -0
- package/lib/typescript/molecules/Stepper.d.ts +35 -3
- package/lib/typescript/molecules/Stepper.d.ts.map +1 -1
- package/lib/typescript/molecules/index.d.ts +4 -4
- package/lib/typescript/molecules/index.d.ts.map +1 -1
- package/lib/typescript/organisms/AlertProvider.d.ts +9 -0
- package/lib/typescript/organisms/AlertProvider.d.ts.map +1 -1
- package/lib/typescript/organisms/BottomSheet.d.ts +26 -4
- package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/organisms/List.d.ts +9 -2
- package/lib/typescript/organisms/List.d.ts.map +1 -1
- package/lib/typescript/organisms/Modal.d.ts +3 -5
- package/lib/typescript/organisms/Modal.d.ts.map +1 -1
- package/lib/typescript/organisms/NavigationBar.d.ts +32 -0
- package/lib/typescript/organisms/NavigationBar.d.ts.map +1 -0
- package/lib/typescript/organisms/Screen.d.ts +2 -1
- package/lib/typescript/organisms/Screen.d.ts.map +1 -1
- package/lib/typescript/organisms/SearchList.d.ts +21 -5
- package/lib/typescript/organisms/SearchList.d.ts.map +1 -1
- package/lib/typescript/organisms/Select.d.ts +19 -4
- package/lib/typescript/organisms/Select.d.ts.map +1 -1
- package/lib/typescript/{molecules → organisms}/ThemeToggle.d.ts +4 -4
- package/lib/typescript/organisms/ThemeToggle.d.ts.map +1 -0
- package/lib/typescript/organisms/Toast.d.ts.map +1 -1
- package/lib/typescript/organisms/ToastProvider.d.ts +8 -0
- package/lib/typescript/organisms/ToastProvider.d.ts.map +1 -1
- package/lib/typescript/organisms/Wizard.d.ts +7 -1
- package/lib/typescript/organisms/Wizard.d.ts.map +1 -1
- package/lib/typescript/organisms/imperativeBridge.d.ts +12 -0
- package/lib/typescript/organisms/imperativeBridge.d.ts.map +1 -0
- package/lib/typescript/organisms/index.d.ts +7 -3
- package/lib/typescript/organisms/index.d.ts.map +1 -1
- package/lib/typescript/theme/colors.d.ts +44 -0
- package/lib/typescript/theme/colors.d.ts.map +1 -0
- package/lib/typescript/theme/createTheme.d.ts +3 -0
- package/lib/typescript/theme/createTheme.d.ts.map +1 -1
- package/lib/typescript/theme/index.d.ts +3 -1
- package/lib/typescript/theme/index.d.ts.map +1 -1
- package/lib/typescript/theme/palettes.d.ts.map +1 -1
- package/lib/typescript/theme/types.d.ts +9 -0
- package/lib/typescript/theme/types.d.ts.map +1 -1
- package/package.json +67 -6
- package/registry/alert-provider.json +9 -2
- package/registry/bottom-sheet.json +6 -2
- package/registry/core.json +8 -4
- package/registry/date-field.json +1 -0
- package/registry/index.json +96 -27
- package/registry/list.json +2 -1
- package/registry/manifest.json +44 -6
- package/registry/modal.json +3 -2
- package/registry/navigation-bar.json +18 -0
- package/registry/option-card.json +3 -2
- package/registry/pressable-scale.json +1 -1
- package/registry/screen.json +1 -1
- package/registry/search-list.json +2 -1
- package/registry/segmented-control.json +18 -0
- package/registry/select.json +1 -1
- package/registry/skeleton.json +18 -0
- package/registry/stepper.json +3 -2
- package/registry/theme-toggle.json +5 -4
- package/registry/toast-provider.json +11 -3
- package/registry/toast.json +3 -2
- package/registry/transition.json +19 -0
- package/registry/wizard.json +3 -2
- package/src/atoms/PressableScale.tsx +16 -6
- package/src/atoms/Skeleton.tsx +114 -0
- package/src/atoms/Transition.tsx +189 -0
- package/src/atoms/index.ts +4 -0
- package/src/molecules/OptionCard.tsx +62 -25
- package/src/molecules/SegmentedControl.tsx +84 -0
- package/src/molecules/Stepper.tsx +147 -32
- package/src/molecules/index.ts +4 -4
- package/src/organisms/AlertProvider.tsx +26 -1
- package/src/organisms/BottomSheet.tsx +214 -62
- package/src/organisms/List.tsx +73 -41
- package/src/organisms/Modal.tsx +49 -61
- package/src/organisms/NavigationBar.tsx +223 -0
- package/src/organisms/Screen.tsx +11 -3
- package/src/organisms/SearchList.tsx +132 -36
- package/src/organisms/Select.tsx +223 -45
- package/src/organisms/ThemeToggle.tsx +30 -0
- package/src/organisms/Toast.tsx +8 -21
- package/src/organisms/ToastProvider.tsx +24 -0
- package/src/organisms/Wizard.tsx +36 -3
- package/src/organisms/imperativeBridge.ts +28 -0
- package/src/organisms/index.ts +7 -3
- package/src/theme/colors.ts +175 -0
- package/src/theme/createTheme.ts +38 -4
- package/src/theme/index.ts +3 -1
- package/src/theme/palettes.ts +5 -2
- package/src/theme/types.ts +10 -0
- package/lib/commonjs/molecules/ThemeToggle.js.map +0 -1
- package/lib/module/molecules/ThemeToggle.js.map +0 -1
- package/lib/typescript/molecules/ThemeToggle.d.ts.map +0 -1
- package/src/molecules/ThemeToggle.tsx +0 -67
package/src/organisms/Modal.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import React, { memo, useEffect,
|
|
1
|
+
import React, { memo, useEffect, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
Animated,
|
|
4
3
|
Dimensions,
|
|
5
4
|
KeyboardAvoidingView,
|
|
6
5
|
Modal as RNModal,
|
|
@@ -15,10 +14,11 @@ import { createStyles } from '../theme/createStyles';
|
|
|
15
14
|
import { useColors, useInsets } from '../theme/UIProvider';
|
|
16
15
|
import { Title } from '../atoms/Text';
|
|
17
16
|
import { IconButton } from '../atoms/IconButton';
|
|
17
|
+
import { Transition, type TransitionPreset } from '../atoms/Transition';
|
|
18
18
|
|
|
19
19
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
20
20
|
|
|
21
|
-
export type ModalVariant = 'full' | 'overlay' | 'fullScreen'
|
|
21
|
+
export type ModalVariant = 'full' | 'overlay' | 'fullScreen';
|
|
22
22
|
|
|
23
23
|
export interface ModalProps
|
|
24
24
|
extends Pick<RNModalProps, 'statusBarTranslucent' | 'onRequestClose' | 'hardwareAccelerated'> {
|
|
@@ -38,7 +38,6 @@ const useStyles = createStyles((theme) => ({
|
|
|
38
38
|
flex1: { flex: 1 },
|
|
39
39
|
fullWrapper: { flex: 1, backgroundColor: theme.colors.background },
|
|
40
40
|
overlayContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: theme.tokens.spacing.xxl - 4 },
|
|
41
|
-
bottomSheetContainer: { justifyContent: 'flex-end', padding: 0 },
|
|
42
41
|
backdrop: { backgroundColor: theme.colors.overlay },
|
|
43
42
|
overlayCard: {
|
|
44
43
|
width: '100%',
|
|
@@ -48,15 +47,6 @@ const useStyles = createStyles((theme) => ({
|
|
|
48
47
|
overflow: 'hidden',
|
|
49
48
|
...theme.tokens.shadow.lg,
|
|
50
49
|
},
|
|
51
|
-
bottomSheetCard: {
|
|
52
|
-
width: '100%',
|
|
53
|
-
maxHeight: '90%',
|
|
54
|
-
backgroundColor: theme.colors.surface,
|
|
55
|
-
borderTopLeftRadius: theme.tokens.radius.xxl,
|
|
56
|
-
borderTopRightRadius: theme.tokens.radius.xxl,
|
|
57
|
-
overflow: 'hidden',
|
|
58
|
-
...theme.tokens.shadow.lg,
|
|
59
|
-
},
|
|
60
50
|
header: {
|
|
61
51
|
flexDirection: 'row',
|
|
62
52
|
justifyContent: 'space-between',
|
|
@@ -83,10 +73,8 @@ const useStyles = createStyles((theme) => ({
|
|
|
83
73
|
/**
|
|
84
74
|
* Modal
|
|
85
75
|
* Un solo componente para todos los modales de la app: maneja scroll,
|
|
86
|
-
* teclado, insets y layout según `variant`.
|
|
87
|
-
*
|
|
88
|
-
* tarjeta: animar opacity ahí dispara un bug de Android donde los hijos con
|
|
89
|
-
* elevation parpadean/redibujan). El fondo semitransparente sí anima opacity.
|
|
76
|
+
* teclado, insets y layout según `variant`. Para una hoja desde abajo está
|
|
77
|
+
* BottomSheet, que además trae arrastre para cerrar.
|
|
90
78
|
*/
|
|
91
79
|
export const Modal = memo(
|
|
92
80
|
({
|
|
@@ -107,43 +95,22 @@ export const Modal = memo(
|
|
|
107
95
|
const isFull = variant === 'full';
|
|
108
96
|
const isOverlay = variant === 'overlay';
|
|
109
97
|
const isFullScreen = variant === 'fullScreen';
|
|
110
|
-
const
|
|
111
|
-
const isAnimatedVariant = isOverlay || isBottomSheet || isFull;
|
|
98
|
+
const isAnimatedVariant = isOverlay || isFull;
|
|
112
99
|
const insets = useInsets();
|
|
113
100
|
const colors = useColors();
|
|
114
101
|
const styles = useStyles();
|
|
115
102
|
|
|
116
103
|
const [mounted, setMounted] = useState(!isAnimatedVariant && visible);
|
|
117
|
-
const backdropOpacity = useRef(new Animated.Value(0)).current;
|
|
118
|
-
const cardProgress = useRef(new Animated.Value(0)).current;
|
|
119
104
|
|
|
120
105
|
useEffect(() => {
|
|
121
|
-
if (!isAnimatedVariant)
|
|
122
|
-
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (visible) {
|
|
126
|
-
setMounted(true);
|
|
127
|
-
Animated.timing(backdropOpacity, { toValue: 1, duration: 200, useNativeDriver: true }).start();
|
|
128
|
-
Animated.timing(cardProgress, { toValue: 1, duration: 250, useNativeDriver: true }).start();
|
|
129
|
-
} else {
|
|
130
|
-
Animated.timing(backdropOpacity, { toValue: 0, duration: 180, useNativeDriver: true }).start();
|
|
131
|
-
Animated.timing(cardProgress, { toValue: 0, duration: 200, useNativeDriver: true }).start(({ finished }) => {
|
|
132
|
-
if (finished) setMounted(false);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
106
|
+
if (!isAnimatedVariant) setMounted(visible);
|
|
107
|
+
else if (visible) setMounted(true);
|
|
136
108
|
}, [visible, isAnimatedVariant]);
|
|
137
109
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
transform: [
|
|
143
|
-
{ translateY: cardProgress.interpolate({ inputRange: [0, 1], outputRange: [-40, 0] }) },
|
|
144
|
-
{ scale: cardProgress.interpolate({ inputRange: [0, 1], outputRange: [0.9, 1] }) },
|
|
145
|
-
],
|
|
146
|
-
};
|
|
110
|
+
// La tarjeta nunca anima opacity: hacerlo dispara un bug de Android donde
|
|
111
|
+
// los hijos con elevation parpadean. El fondo semitransparente sí.
|
|
112
|
+
const cardPreset: TransitionPreset[] = isFull ? ['slide-up'] : ['slide-down', 'scale'];
|
|
113
|
+
const cardDistance = isFull ? SCREEN_HEIGHT : 40;
|
|
147
114
|
|
|
148
115
|
const behavior = Platform.OS === 'ios' ? (isFull ? undefined : 'padding') : 'height';
|
|
149
116
|
const keyboardOffset = Platform.OS === 'ios' ? 0 : 20;
|
|
@@ -204,42 +171,63 @@ export const Modal = memo(
|
|
|
204
171
|
presentationStyle={
|
|
205
172
|
Platform.OS === 'ios' ? (isFullScreen ? 'fullScreen' : 'overFullScreen') : undefined
|
|
206
173
|
}
|
|
207
|
-
transparent={isOverlay ||
|
|
174
|
+
transparent={isOverlay || isFull}
|
|
208
175
|
statusBarTranslucent={statusBarTranslucent}
|
|
209
176
|
hardwareAccelerated={hardwareAccelerated}
|
|
210
177
|
>
|
|
211
178
|
<KeyboardAvoidingView behavior={behavior} keyboardVerticalOffset={keyboardOffset} style={styles.flex1}>
|
|
212
|
-
{isOverlay
|
|
213
|
-
<View style={
|
|
179
|
+
{isOverlay ? (
|
|
180
|
+
<View style={styles.overlayContainer}>
|
|
214
181
|
{/* Sin accessibilityLabel propio: el header ya expone un botón "Close"
|
|
215
182
|
accesible; duplicar el label aquí generaría dos elementos con el
|
|
216
183
|
mismo nombre para lectores de pantalla. Tocar afuera es un atajo
|
|
217
184
|
puramente táctil. */}
|
|
218
|
-
<
|
|
185
|
+
<Transition
|
|
186
|
+
visible={visible}
|
|
187
|
+
preset="fade"
|
|
188
|
+
duration={200}
|
|
189
|
+
keepMounted
|
|
190
|
+
style={[{ ...StyleSheetAbsoluteFill }, styles.backdrop]}
|
|
191
|
+
>
|
|
219
192
|
<Pressable style={StyleSheetAbsoluteFill} onPress={onClose} testID="modal-backdrop" />
|
|
220
|
-
</
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
]}
|
|
193
|
+
</Transition>
|
|
194
|
+
|
|
195
|
+
<Transition
|
|
196
|
+
visible={visible}
|
|
197
|
+
preset={cardPreset}
|
|
198
|
+
distance={cardDistance}
|
|
199
|
+
duration={250}
|
|
200
|
+
onExited={() => setMounted(false)}
|
|
201
|
+
style={[styles.overlayCard, containerStyle]}
|
|
229
202
|
>
|
|
230
203
|
{renderHeader()}
|
|
231
204
|
{renderMainContent()}
|
|
232
205
|
{renderFooter()}
|
|
233
|
-
</
|
|
206
|
+
</Transition>
|
|
234
207
|
</View>
|
|
208
|
+
) : isFull ? (
|
|
209
|
+
<Transition
|
|
210
|
+
visible={visible}
|
|
211
|
+
preset={cardPreset}
|
|
212
|
+
distance={cardDistance}
|
|
213
|
+
duration={250}
|
|
214
|
+
onExited={() => setMounted(false)}
|
|
215
|
+
style={[styles.fullWrapper, containerStyle]}
|
|
216
|
+
>
|
|
217
|
+
<View style={[styles.flex1, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
|
|
218
|
+
{renderHeader()}
|
|
219
|
+
{renderMainContent()}
|
|
220
|
+
{renderFooter()}
|
|
221
|
+
</View>
|
|
222
|
+
</Transition>
|
|
235
223
|
) : (
|
|
236
|
-
<
|
|
224
|
+
<View style={[styles.fullWrapper, containerStyle]}>
|
|
237
225
|
<View style={[styles.flex1, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
|
|
238
226
|
{renderHeader()}
|
|
239
227
|
{renderMainContent()}
|
|
240
228
|
{renderFooter()}
|
|
241
229
|
</View>
|
|
242
|
-
</
|
|
230
|
+
</View>
|
|
243
231
|
)}
|
|
244
232
|
</KeyboardAvoidingView>
|
|
245
233
|
</RNModal>
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import React, { memo, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
View,
|
|
8
|
+
type LayoutChangeEvent,
|
|
9
|
+
type StyleProp,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import { createStyles } from '../theme/createStyles';
|
|
13
|
+
import { useAllowFontScaling, useColors, useInsets, useTheme } from '../theme/UIProvider';
|
|
14
|
+
import { Icon } from '../icons/Icon';
|
|
15
|
+
import type { IconName } from '../icons/types';
|
|
16
|
+
|
|
17
|
+
export interface NavigationBarItem {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
iconName?: IconName;
|
|
21
|
+
/** Contador sobre el ícono. `true` pinta sólo el punto. */
|
|
22
|
+
badge?: number | string | true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface NavigationBarProps {
|
|
26
|
+
items: NavigationBarItem[];
|
|
27
|
+
activeKey: string;
|
|
28
|
+
onChange: (key: string) => void;
|
|
29
|
+
/** Borde de la pantalla donde vive la barra. @default 'bottom' */
|
|
30
|
+
position?: 'bottom' | 'top';
|
|
31
|
+
/** @default true */
|
|
32
|
+
showLabels?: boolean;
|
|
33
|
+
/** Suma el inset de safe area del borde donde vive la barra. @default true */
|
|
34
|
+
safeArea?: boolean;
|
|
35
|
+
style?: StyleProp<ViewStyle>;
|
|
36
|
+
testID?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const useStyles = createStyles((theme) => ({
|
|
40
|
+
bar: {
|
|
41
|
+
flexDirection: 'row',
|
|
42
|
+
backgroundColor: theme.colors.surface,
|
|
43
|
+
borderColor: theme.colors.border,
|
|
44
|
+
paddingHorizontal: theme.tokens.spacing.xs,
|
|
45
|
+
paddingVertical: theme.tokens.spacing.sm,
|
|
46
|
+
},
|
|
47
|
+
barBottom: { borderTopWidth: StyleSheet.hairlineWidth },
|
|
48
|
+
barTop: { borderBottomWidth: StyleSheet.hairlineWidth },
|
|
49
|
+
indicator: {
|
|
50
|
+
position: 'absolute',
|
|
51
|
+
top: theme.tokens.spacing.xs,
|
|
52
|
+
bottom: theme.tokens.spacing.xs,
|
|
53
|
+
left: theme.tokens.spacing.sm,
|
|
54
|
+
borderRadius: theme.tokens.radius.lg,
|
|
55
|
+
backgroundColor: theme.colors.primarySoft,
|
|
56
|
+
},
|
|
57
|
+
item: { flex: 1, paddingVertical: theme.tokens.spacing.xs },
|
|
58
|
+
itemContent: { alignItems: 'center', justifyContent: 'center', gap: 2 },
|
|
59
|
+
label: { fontSize: theme.tokens.fontSize.xs, fontWeight: theme.tokens.fontWeight.medium },
|
|
60
|
+
badge: {
|
|
61
|
+
position: 'absolute',
|
|
62
|
+
top: -4,
|
|
63
|
+
left: '50%',
|
|
64
|
+
marginLeft: 4,
|
|
65
|
+
minWidth: 16,
|
|
66
|
+
height: 16,
|
|
67
|
+
paddingHorizontal: 4,
|
|
68
|
+
borderRadius: theme.tokens.radius.full,
|
|
69
|
+
backgroundColor: theme.colors.error,
|
|
70
|
+
alignItems: 'center',
|
|
71
|
+
justifyContent: 'center',
|
|
72
|
+
},
|
|
73
|
+
badgeDot: { minWidth: 10, height: 10, top: -2, paddingHorizontal: 0 },
|
|
74
|
+
badgeText: {
|
|
75
|
+
fontSize: 10,
|
|
76
|
+
fontWeight: theme.tokens.fontWeight.bold,
|
|
77
|
+
color: theme.colors.onError,
|
|
78
|
+
},
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
const ICON_SIZE = 22;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* NavigationBar
|
|
85
|
+
* Barra de navegación por pestañas con la misma apariencia y las mismas
|
|
86
|
+
* transiciones en iOS y Android: el indicador se desliza hasta la pestaña
|
|
87
|
+
* activa y su contenido crece y toma el color de acento con un cross-fade.
|
|
88
|
+
* No navega por su cuenta — reporta la pestaña tocada y el consumidor decide.
|
|
89
|
+
*/
|
|
90
|
+
export const NavigationBar = memo(
|
|
91
|
+
({
|
|
92
|
+
items,
|
|
93
|
+
activeKey,
|
|
94
|
+
onChange,
|
|
95
|
+
position = 'bottom',
|
|
96
|
+
showLabels = true,
|
|
97
|
+
safeArea = true,
|
|
98
|
+
style,
|
|
99
|
+
testID,
|
|
100
|
+
}: NavigationBarProps) => {
|
|
101
|
+
const theme = useTheme();
|
|
102
|
+
const colors = useColors();
|
|
103
|
+
const styles = useStyles();
|
|
104
|
+
const insets = useInsets();
|
|
105
|
+
const allowFontScaling = useAllowFontScaling();
|
|
106
|
+
|
|
107
|
+
const [barWidth, setBarWidth] = useState(0);
|
|
108
|
+
const slide = useRef(new Animated.Value(0)).current;
|
|
109
|
+
const placed = useRef(false);
|
|
110
|
+
|
|
111
|
+
const activeIndex = Math.max(
|
|
112
|
+
0,
|
|
113
|
+
items.findIndex((item) => item.key === activeKey)
|
|
114
|
+
);
|
|
115
|
+
const itemWidth = items.length > 0 ? barWidth / items.length : 0;
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (itemWidth <= 0) return;
|
|
119
|
+
const to = activeIndex * itemWidth;
|
|
120
|
+
// En el primer layout el indicador aparece en su lugar; animarlo desde 0
|
|
121
|
+
// sería un deslizamiento que el usuario no pidió.
|
|
122
|
+
if (!placed.current) {
|
|
123
|
+
placed.current = true;
|
|
124
|
+
slide.setValue(to);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
Animated.spring(slide, { toValue: to, useNativeDriver: true, friction: 9, tension: 90 }).start();
|
|
128
|
+
}, [activeIndex, itemWidth, slide]);
|
|
129
|
+
|
|
130
|
+
const onLayout = (e: LayoutChangeEvent) => {
|
|
131
|
+
const next = e.nativeEvent.layout.width - theme.tokens.spacing.xs * 2;
|
|
132
|
+
if (next !== barWidth) setBarWidth(next);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const renderContent = (item: NavigationBarItem, color: string) => (
|
|
136
|
+
<>
|
|
137
|
+
{!!item.iconName && <Icon name={item.iconName} size={ICON_SIZE} color={color} />}
|
|
138
|
+
{showLabels && (
|
|
139
|
+
<Text allowFontScaling={allowFontScaling} numberOfLines={1} style={[styles.label, { color }]}>
|
|
140
|
+
{item.label}
|
|
141
|
+
</Text>
|
|
142
|
+
)}
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<View
|
|
148
|
+
testID={testID}
|
|
149
|
+
onLayout={onLayout}
|
|
150
|
+
accessibilityRole="tablist"
|
|
151
|
+
style={[
|
|
152
|
+
styles.bar,
|
|
153
|
+
position === 'bottom' ? styles.barBottom : styles.barTop,
|
|
154
|
+
safeArea && position === 'bottom' && { paddingBottom: theme.tokens.spacing.sm + insets.bottom },
|
|
155
|
+
safeArea && position === 'top' && { paddingTop: theme.tokens.spacing.sm + insets.top },
|
|
156
|
+
style,
|
|
157
|
+
]}
|
|
158
|
+
>
|
|
159
|
+
{itemWidth > 0 && (
|
|
160
|
+
<Animated.View
|
|
161
|
+
pointerEvents="none"
|
|
162
|
+
style={[
|
|
163
|
+
styles.indicator,
|
|
164
|
+
{
|
|
165
|
+
width: itemWidth - theme.tokens.spacing.sm * 2,
|
|
166
|
+
transform: [{ translateX: slide }],
|
|
167
|
+
},
|
|
168
|
+
]}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{items.map((item, index) => {
|
|
173
|
+
const active = index === activeIndex;
|
|
174
|
+
const range = [(index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth];
|
|
175
|
+
const scale =
|
|
176
|
+
itemWidth > 0
|
|
177
|
+
? slide.interpolate({ inputRange: range, outputRange: [1, 1.08, 1], extrapolate: 'clamp' })
|
|
178
|
+
: 1;
|
|
179
|
+
const activeOpacity =
|
|
180
|
+
itemWidth > 0
|
|
181
|
+
? slide.interpolate({ inputRange: range, outputRange: [0, 1, 0], extrapolate: 'clamp' })
|
|
182
|
+
: 0;
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<TouchableOpacity
|
|
186
|
+
key={item.key}
|
|
187
|
+
style={styles.item}
|
|
188
|
+
activeOpacity={0.7}
|
|
189
|
+
onPress={() => onChange(item.key)}
|
|
190
|
+
accessibilityRole="tab"
|
|
191
|
+
accessibilityLabel={item.label}
|
|
192
|
+
accessibilityState={{ selected: active }}
|
|
193
|
+
>
|
|
194
|
+
<Animated.View style={[styles.itemContent, { transform: [{ scale }] }]}>
|
|
195
|
+
{renderContent(item, colors.textLight)}
|
|
196
|
+
{/* Copia decorativa: sin ocultarla, el lector lee el label dos veces. */}
|
|
197
|
+
<Animated.View
|
|
198
|
+
pointerEvents="none"
|
|
199
|
+
accessibilityElementsHidden
|
|
200
|
+
importantForAccessibility="no-hide-descendants"
|
|
201
|
+
style={[StyleSheet.absoluteFill, styles.itemContent, { opacity: activeOpacity }]}
|
|
202
|
+
>
|
|
203
|
+
{renderContent(item, colors.primaryText)}
|
|
204
|
+
</Animated.View>
|
|
205
|
+
|
|
206
|
+
{item.badge !== undefined && (
|
|
207
|
+
<View style={[styles.badge, item.badge === true && styles.badgeDot]}>
|
|
208
|
+
{item.badge !== true && (
|
|
209
|
+
<Text allowFontScaling={false} style={styles.badgeText}>
|
|
210
|
+
{item.badge}
|
|
211
|
+
</Text>
|
|
212
|
+
)}
|
|
213
|
+
</View>
|
|
214
|
+
)}
|
|
215
|
+
</Animated.View>
|
|
216
|
+
</TouchableOpacity>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
219
|
+
</View>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
NavigationBar.displayName = 'NavigationBar';
|
package/src/organisms/Screen.tsx
CHANGED
|
@@ -13,7 +13,8 @@ export interface ScreenProps {
|
|
|
13
13
|
/** Qué insets de safe area aplicar. @default ['top', 'bottom'] */
|
|
14
14
|
edges?: ScreenEdge[];
|
|
15
15
|
/**
|
|
16
|
-
* Compensa el teclado con
|
|
16
|
+
* Compensa el teclado: en iOS con scroll lo hace el propio ScrollView, en el
|
|
17
|
+
* resto de los casos con un KeyboardAvoidingView. Ponelo en false cuando
|
|
17
18
|
* el contenido ya trae su propio scroller que maneja el teclado (Wizard,
|
|
18
19
|
* List, SearchList): las dos compensaciones se suman y el campo enfocado
|
|
19
20
|
* termina empujado fuera de la pantalla.
|
|
@@ -46,8 +47,15 @@ export const Screen = memo(({
|
|
|
46
47
|
const applyTop = edges.includes('top');
|
|
47
48
|
const applyBottom = edges.includes('bottom');
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// En iOS el ScrollView de abajo ya sube el campo enfocado con
|
|
51
|
+
// automaticallyAdjustKeyboardInsets; envolverlo además en un
|
|
52
|
+
// KeyboardAvoidingView compensa el teclado dos veces y el input termina
|
|
53
|
+
// empujado fuera del borde superior. Android no tiene ese prop, así que ahí
|
|
54
|
+
// el KeyboardAvoidingView sigue siendo el único mecanismo.
|
|
55
|
+
const avoidWithView = keyboardAvoiding && !(scrollable && Platform.OS === 'ios');
|
|
56
|
+
|
|
57
|
+
const KeyboardWrapper: React.ComponentType<any> = avoidWithView ? KeyboardAvoidingView : View;
|
|
58
|
+
const keyboardProps = avoidWithView
|
|
51
59
|
? {
|
|
52
60
|
behavior: Platform.OS === 'ios' ? ('padding' as const) : ('height' as const),
|
|
53
61
|
keyboardVerticalOffset: Platform.OS === 'ios' ? 0 : 20,
|