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
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
|
|
1
|
+
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { Alert, type AlertButton, type AlertType } from './Alert';
|
|
3
|
+
import { createImperativeBridge } from './imperativeBridge';
|
|
3
4
|
|
|
4
5
|
export interface AlertOptions {
|
|
5
6
|
title: string;
|
|
@@ -23,6 +24,25 @@ interface AlertContextValue {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const AlertContext = createContext<AlertContextValue | undefined>(undefined);
|
|
27
|
+
const bridge = createImperativeBridge<AlertContextValue>('AlertProvider');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Abre un alert desde fuera del árbol de React —un servicio, un interceptor—
|
|
31
|
+
* sin pasar por el hook. Sin <AlertProvider> montado resuelve de inmediato, así
|
|
32
|
+
* que un `await` nunca queda colgado.
|
|
33
|
+
*/
|
|
34
|
+
export function showAlert(options: AlertOptions): Promise<void> {
|
|
35
|
+
return bridge.get()?.alert(options) ?? Promise.resolve();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Igual que showAlert, pero resuelve true/false. Sin provider, false. */
|
|
39
|
+
export function showConfirm(options: ConfirmOptions): Promise<boolean> {
|
|
40
|
+
return bridge.get()?.confirm(options) ?? Promise.resolve(false);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function hideAlert(): void {
|
|
44
|
+
bridge.get()?.hide();
|
|
45
|
+
}
|
|
26
46
|
|
|
27
47
|
interface InternalState {
|
|
28
48
|
visible: boolean;
|
|
@@ -108,6 +128,11 @@ export function AlertProvider({ children }: { children: React.ReactNode }) {
|
|
|
108
128
|
|
|
109
129
|
const value = useMemo<AlertContextValue>(() => ({ alert: alertFn, confirm: confirmFn, hide }), [alertFn, confirmFn, hide]);
|
|
110
130
|
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
bridge.set(value);
|
|
133
|
+
return () => bridge.set(null);
|
|
134
|
+
}, [value]);
|
|
135
|
+
|
|
111
136
|
return (
|
|
112
137
|
<AlertContext.Provider value={value}>
|
|
113
138
|
{children}
|
|
@@ -1,16 +1,52 @@
|
|
|
1
1
|
import React, { memo, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Dimensions,
|
|
5
|
+
Keyboard,
|
|
6
|
+
KeyboardAvoidingView,
|
|
7
|
+
Modal,
|
|
8
|
+
PanResponder,
|
|
9
|
+
Platform,
|
|
10
|
+
Pressable,
|
|
11
|
+
ScrollView,
|
|
12
|
+
View,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from 'react-native';
|
|
3
15
|
import { createStyles } from '../theme/createStyles';
|
|
4
|
-
import { useInsets } from '../theme/UIProvider';
|
|
16
|
+
import { useColors, useInsets } from '../theme/UIProvider';
|
|
17
|
+
import { Title } from '../atoms/Text';
|
|
18
|
+
import { IconButton } from '../atoms/IconButton';
|
|
19
|
+
import { Transition } from '../atoms/Transition';
|
|
5
20
|
|
|
6
21
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
7
22
|
const DISMISS_THRESHOLD = 100;
|
|
8
23
|
|
|
24
|
+
export type FooterPlacement = 'fixed' | 'scroll' | 'hide-with-keyboard';
|
|
25
|
+
|
|
9
26
|
export interface BottomSheetProps {
|
|
10
27
|
visible: boolean;
|
|
11
28
|
onClose: () => void;
|
|
12
29
|
children: React.ReactNode;
|
|
30
|
+
/** Con título aparece el header, con su botón de cerrar. */
|
|
31
|
+
title?: string;
|
|
32
|
+
/** Acciones de la hoja. Dónde vive lo decide `footerPlacement`. */
|
|
33
|
+
footer?: React.ReactNode;
|
|
34
|
+
/**
|
|
35
|
+
* 'fixed' lo ancla al pie de la hoja; 'scroll' lo deja al final del
|
|
36
|
+
* contenido; 'hide-with-keyboard' lo esconde mientras se escribe, que en un
|
|
37
|
+
* formulario largo le devuelve esa franja al campo enfocado. @default 'fixed'
|
|
38
|
+
*/
|
|
39
|
+
footerPlacement?: FooterPlacement;
|
|
40
|
+
/** El contenido scrollea cuando no entra. @default true */
|
|
41
|
+
scrollable?: boolean;
|
|
42
|
+
/** Arrastrar la hoja hacia abajo la cierra. @default true */
|
|
43
|
+
draggable?: boolean;
|
|
44
|
+
/** Fracción de la pantalla (0-1) o píxeles. @default 0.9 */
|
|
45
|
+
maxHeight?: number;
|
|
13
46
|
closeAccessibilityLabel?: string;
|
|
47
|
+
containerStyle?: ViewStyle;
|
|
48
|
+
contentStyle?: ViewStyle;
|
|
49
|
+
testID?: string;
|
|
14
50
|
}
|
|
15
51
|
|
|
16
52
|
const useStyles = createStyles((theme) => ({
|
|
@@ -20,76 +56,192 @@ const useStyles = createStyles((theme) => ({
|
|
|
20
56
|
backgroundColor: theme.colors.surface,
|
|
21
57
|
borderTopLeftRadius: theme.tokens.radius.xxl,
|
|
22
58
|
borderTopRightRadius: theme.tokens.radius.xxl,
|
|
23
|
-
paddingHorizontal: theme.tokens.spacing.xl,
|
|
24
59
|
...theme.tokens.shadow.lg,
|
|
25
60
|
},
|
|
26
61
|
handleArea: { paddingVertical: theme.tokens.spacing.md, alignItems: 'center' },
|
|
27
62
|
handle: { width: 40, height: 5, borderRadius: 3, backgroundColor: theme.colors.border },
|
|
63
|
+
header: {
|
|
64
|
+
flexDirection: 'row',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
gap: theme.tokens.spacing.md,
|
|
67
|
+
paddingHorizontal: theme.tokens.spacing.xl,
|
|
68
|
+
paddingBottom: theme.tokens.spacing.md,
|
|
69
|
+
},
|
|
70
|
+
headerSpaced: { paddingTop: theme.tokens.spacing.xl },
|
|
71
|
+
title: { flex: 1, fontSize: theme.tokens.fontSize.xl, fontWeight: theme.tokens.fontWeight.bold },
|
|
72
|
+
content: { paddingHorizontal: theme.tokens.spacing.xl },
|
|
73
|
+
scroll: { flexShrink: 1 },
|
|
74
|
+
footer: {
|
|
75
|
+
paddingHorizontal: theme.tokens.spacing.xl,
|
|
76
|
+
paddingTop: theme.tokens.spacing.md,
|
|
77
|
+
borderTopWidth: 1,
|
|
78
|
+
borderTopColor: theme.colors.border,
|
|
79
|
+
},
|
|
80
|
+
footerInline: { paddingTop: theme.tokens.spacing.xl },
|
|
28
81
|
}));
|
|
29
82
|
|
|
30
83
|
const absoluteFill: ViewStyle = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 };
|
|
31
84
|
|
|
32
85
|
/**
|
|
33
86
|
* BottomSheet
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
87
|
+
* Hoja que entra desde abajo, con arrastre para cerrar (PanResponder, no un
|
|
88
|
+
* gesture-handler externo: la librería es cero dependencias). Header, footer y
|
|
89
|
+
* scroll son opcionales, así que cubre tanto la hoja desnuda como el formulario
|
|
90
|
+
* completo.
|
|
37
91
|
*/
|
|
38
|
-
export const BottomSheet = memo(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
export const BottomSheet = memo(
|
|
93
|
+
({
|
|
94
|
+
visible,
|
|
95
|
+
onClose,
|
|
96
|
+
children,
|
|
97
|
+
title,
|
|
98
|
+
footer,
|
|
99
|
+
footerPlacement = 'fixed',
|
|
100
|
+
scrollable = true,
|
|
101
|
+
draggable = true,
|
|
102
|
+
maxHeight = 0.9,
|
|
103
|
+
closeAccessibilityLabel = 'Close',
|
|
104
|
+
containerStyle,
|
|
105
|
+
contentStyle,
|
|
106
|
+
testID,
|
|
107
|
+
}: BottomSheetProps) => {
|
|
108
|
+
const styles = useStyles();
|
|
109
|
+
const colors = useColors();
|
|
110
|
+
const insets = useInsets();
|
|
111
|
+
const [mounted, setMounted] = useState(visible);
|
|
112
|
+
const [keyboardOpen, setKeyboardOpen] = useState(false);
|
|
113
|
+
const progress = useRef(new Animated.Value(0)).current;
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (visible) {
|
|
117
|
+
setMounted(true);
|
|
118
|
+
Animated.timing(progress, { toValue: 1, duration: 250, useNativeDriver: true }).start();
|
|
119
|
+
} else {
|
|
120
|
+
Animated.timing(progress, { toValue: 0, duration: 200, useNativeDriver: true }).start(({ finished }) => {
|
|
121
|
+
if (finished) setMounted(false);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}, [visible, progress]);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (footerPlacement !== 'hide-with-keyboard') return;
|
|
128
|
+
const [show, hide] =
|
|
129
|
+
Platform.OS === 'ios' ? ['keyboardWillShow', 'keyboardWillHide'] : ['keyboardDidShow', 'keyboardDidHide'];
|
|
130
|
+
const subs = [
|
|
131
|
+
Keyboard.addListener(show as 'keyboardDidShow', () => setKeyboardOpen(true)),
|
|
132
|
+
Keyboard.addListener(hide as 'keyboardDidHide', () => setKeyboardOpen(false)),
|
|
133
|
+
];
|
|
134
|
+
return () => subs.forEach((sub) => sub.remove());
|
|
135
|
+
}, [footerPlacement]);
|
|
136
|
+
|
|
137
|
+
const panResponder = useRef(
|
|
138
|
+
PanResponder.create({
|
|
139
|
+
onMoveShouldSetPanResponder: (_, gesture) => gesture.dy > 5 && Math.abs(gesture.dy) > Math.abs(gesture.dx),
|
|
140
|
+
onPanResponderMove: (_, gesture) => {
|
|
141
|
+
if (gesture.dy > 0) progress.setValue(1 - Math.min(gesture.dy, SCREEN_HEIGHT) / SCREEN_HEIGHT);
|
|
142
|
+
},
|
|
143
|
+
onPanResponderRelease: (_, gesture) => {
|
|
144
|
+
if (gesture.dy > DISMISS_THRESHOLD) {
|
|
145
|
+
onClose();
|
|
146
|
+
} else {
|
|
147
|
+
Animated.timing(progress, { toValue: 1, duration: 150, useNativeDriver: true }).start();
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
).current;
|
|
152
|
+
|
|
153
|
+
if (!mounted) return null;
|
|
154
|
+
|
|
155
|
+
// Porcentaje y no píxeles: se mide contra el contenedor, que con el teclado
|
|
156
|
+
// abierto es más bajo, así que la hoja entra siempre entera.
|
|
157
|
+
const resolvedMaxHeight: number | `${number}%` = maxHeight <= 1 ? `${maxHeight * 100}%` : maxHeight;
|
|
158
|
+
const hasHeader = !!title;
|
|
159
|
+
|
|
160
|
+
const scrolledFooter = !!footer && footerPlacement === 'scroll' && (
|
|
161
|
+
<View style={styles.footerInline}>{footer}</View>
|
|
162
|
+
);
|
|
163
|
+
const pinnedFooter =
|
|
164
|
+
!!footer && footerPlacement !== 'scroll' && !(footerPlacement === 'hide-with-keyboard' && keyboardOpen) ? (
|
|
165
|
+
<View style={styles.footer}>{footer}</View>
|
|
166
|
+
) : null;
|
|
167
|
+
|
|
168
|
+
const body = scrollable ? (
|
|
169
|
+
<ScrollView
|
|
170
|
+
style={styles.scroll}
|
|
171
|
+
contentContainerStyle={[styles.content, contentStyle]}
|
|
172
|
+
keyboardShouldPersistTaps="handled"
|
|
173
|
+
showsVerticalScrollIndicator={false}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
{scrolledFooter}
|
|
177
|
+
</ScrollView>
|
|
178
|
+
) : (
|
|
179
|
+
<View style={[styles.content, contentStyle]}>
|
|
180
|
+
{children}
|
|
181
|
+
{scrolledFooter}
|
|
91
182
|
</View>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<Modal visible transparent animationType="none" statusBarTranslucent onRequestClose={onClose}>
|
|
187
|
+
{/* La hoja está anclada abajo, así que el teclado la tapa entera: hay
|
|
188
|
+
que empujarla, no ajustar insets adentro del scroll. Una sola
|
|
189
|
+
compensación, la del contenedor. */}
|
|
190
|
+
<KeyboardAvoidingView
|
|
191
|
+
style={styles.container}
|
|
192
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
193
|
+
>
|
|
194
|
+
<Transition value={progress} preset="fade" style={[absoluteFill, styles.backdrop]}>
|
|
195
|
+
{/* Con header, el botón de cerrar ya expone la acción: repetir el
|
|
196
|
+
label acá deja dos elementos con el mismo nombre. */}
|
|
197
|
+
<Pressable
|
|
198
|
+
style={absoluteFill}
|
|
199
|
+
onPress={onClose}
|
|
200
|
+
testID="bottom-sheet-backdrop"
|
|
201
|
+
accessibilityRole={hasHeader ? undefined : 'button'}
|
|
202
|
+
accessibilityLabel={hasHeader ? undefined : closeAccessibilityLabel}
|
|
203
|
+
/>
|
|
204
|
+
</Transition>
|
|
205
|
+
|
|
206
|
+
<Transition
|
|
207
|
+
value={progress}
|
|
208
|
+
preset="slide-up"
|
|
209
|
+
distance={SCREEN_HEIGHT}
|
|
210
|
+
testID={testID}
|
|
211
|
+
style={[
|
|
212
|
+
styles.sheet,
|
|
213
|
+
{ maxHeight: resolvedMaxHeight, paddingBottom: Math.max(insets.bottom, 20) },
|
|
214
|
+
containerStyle,
|
|
215
|
+
]}
|
|
216
|
+
>
|
|
217
|
+
{draggable && (
|
|
218
|
+
<View style={styles.handleArea} testID="bottom-sheet-handle" {...panResponder.panHandlers}>
|
|
219
|
+
<View style={styles.handle} />
|
|
220
|
+
</View>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
{hasHeader && (
|
|
224
|
+
<View style={[styles.header, !draggable && styles.headerSpaced]}>
|
|
225
|
+
<Title style={styles.title} numberOfLines={1}>
|
|
226
|
+
{title}
|
|
227
|
+
</Title>
|
|
228
|
+
<IconButton
|
|
229
|
+
iconName="close"
|
|
230
|
+
size={24}
|
|
231
|
+
color={colors.text}
|
|
232
|
+
onPress={onClose}
|
|
233
|
+
accessibilityLabel={closeAccessibilityLabel}
|
|
234
|
+
/>
|
|
235
|
+
</View>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{body}
|
|
239
|
+
|
|
240
|
+
{pinnedFooter}
|
|
241
|
+
</Transition>
|
|
242
|
+
</KeyboardAvoidingView>
|
|
243
|
+
</Modal>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
);
|
|
95
247
|
BottomSheet.displayName = 'BottomSheet';
|
package/src/organisms/List.tsx
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
3
|
-
ActivityIndicator,
|
|
4
|
-
FlatList,
|
|
5
|
-
Platform,
|
|
6
|
-
RefreshControl,
|
|
7
|
-
Text,
|
|
8
|
-
View,
|
|
9
|
-
type FlatListProps,
|
|
10
|
-
type ViewStyle,
|
|
11
|
-
} from 'react-native';
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { FlatList, RefreshControl, View, type FlatListProps, type ViewStyle } from 'react-native';
|
|
12
3
|
import { createStyles } from '../theme/createStyles';
|
|
13
4
|
import { useColors, useLabels } from '../theme/UIProvider';
|
|
14
5
|
import { EmptyState } from '../atoms/EmptyState';
|
|
6
|
+
import { Skeleton } from '../atoms/Skeleton';
|
|
15
7
|
import { Spinner } from '../atoms/Spinner';
|
|
16
8
|
import type { IconName } from '../icons/types';
|
|
17
9
|
|
|
18
10
|
export interface ListProps<T> {
|
|
19
11
|
data: T[];
|
|
20
|
-
keyExtractor: (item: T) => string;
|
|
12
|
+
keyExtractor: (item: T, index: number) => string;
|
|
21
13
|
renderItem: FlatListProps<T>['renderItem'];
|
|
22
14
|
|
|
15
|
+
/** Carga inicial: mientras sea true y no haya datos, se muestran placeholders. */
|
|
23
16
|
isLoading: boolean;
|
|
17
|
+
/** Recarga con datos ya en pantalla (pull-to-refresh). @default false */
|
|
18
|
+
isRefreshing?: boolean;
|
|
24
19
|
isLoadingMore?: boolean;
|
|
25
20
|
/** Controlado por el consumidor: mientras es false, se muestra un spinner de pantalla completa. @default true */
|
|
26
21
|
isReady?: boolean;
|
|
27
22
|
loadingText?: string;
|
|
28
23
|
loadingMoreText?: string;
|
|
29
24
|
|
|
25
|
+
/** Filas fantasma de la carga inicial. @default 6 */
|
|
26
|
+
skeletonCount?: number;
|
|
27
|
+
/** Placeholder propio, para que calce con la forma real del ítem. */
|
|
28
|
+
renderSkeletonItem?: () => React.ReactElement;
|
|
29
|
+
|
|
30
30
|
onEndReached?: () => void;
|
|
31
31
|
onEndReachedThreshold?: number;
|
|
32
32
|
onRefresh?: () => void;
|
|
@@ -48,20 +48,15 @@ export interface ListProps<T> {
|
|
|
48
48
|
|
|
49
49
|
const useStyles = createStyles((theme) => ({
|
|
50
50
|
container: { flex: 1, backgroundColor: theme.colors.background },
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
right: 0,
|
|
56
|
-
bottom: 0,
|
|
57
|
-
backgroundColor: theme.colors.scrim,
|
|
58
|
-
zIndex: 10,
|
|
59
|
-
justifyContent: 'center',
|
|
51
|
+
listContent: { padding: theme.tokens.spacing.md },
|
|
52
|
+
footerLoader: { paddingVertical: theme.tokens.spacing.xl, alignItems: 'center' },
|
|
53
|
+
skeletonRow: {
|
|
54
|
+
flexDirection: 'row',
|
|
60
55
|
alignItems: 'center',
|
|
56
|
+
gap: theme.tokens.spacing.md,
|
|
57
|
+
padding: theme.tokens.spacing.md,
|
|
61
58
|
},
|
|
62
|
-
|
|
63
|
-
listContent: { padding: theme.tokens.spacing.md, paddingBottom: Platform.OS === 'android' ? 100 : 80 },
|
|
64
|
-
footerLoader: { paddingVertical: theme.tokens.spacing.xl, alignItems: 'center' },
|
|
59
|
+
skeletonText: { flex: 1, gap: theme.tokens.spacing.sm },
|
|
65
60
|
}));
|
|
66
61
|
|
|
67
62
|
/**
|
|
@@ -75,10 +70,13 @@ export function List<T>({
|
|
|
75
70
|
keyExtractor,
|
|
76
71
|
renderItem,
|
|
77
72
|
isLoading,
|
|
73
|
+
isRefreshing = false,
|
|
78
74
|
isLoadingMore = false,
|
|
79
75
|
isReady = true,
|
|
80
76
|
loadingText,
|
|
81
77
|
loadingMoreText,
|
|
78
|
+
skeletonCount = 6,
|
|
79
|
+
renderSkeletonItem,
|
|
82
80
|
onEndReached,
|
|
83
81
|
onEndReachedThreshold = 0.3,
|
|
84
82
|
onRefresh,
|
|
@@ -98,22 +96,36 @@ export function List<T>({
|
|
|
98
96
|
const resolvedLoadingText = loadingText ?? labels.loading;
|
|
99
97
|
const resolvedLoadingMoreText = loadingMoreText ?? labels.loadingMore;
|
|
100
98
|
|
|
99
|
+
const isInitialLoading = isLoading && data.length === 0;
|
|
100
|
+
|
|
101
|
+
// El footer propio y el loader de paginación conviven: antes el primero
|
|
102
|
+
// tapaba al segundo y la lista dejaba de avisar que estaba trayendo más.
|
|
101
103
|
const renderFooter = useCallback(() => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const custom =
|
|
105
|
+
typeof ListFooterComponent === 'function' ? (
|
|
106
|
+
<ListFooterComponent />
|
|
107
|
+
) : (
|
|
108
|
+
(ListFooterComponent as React.ReactElement | undefined) ?? null
|
|
109
|
+
);
|
|
110
|
+
|
|
106
111
|
return (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
<>
|
|
113
|
+
{custom}
|
|
114
|
+
{isLoadingMore && (
|
|
115
|
+
<View style={styles.footerLoader}>
|
|
116
|
+
<Spinner text={resolvedLoadingMoreText} fullscreen={false} />
|
|
117
|
+
</View>
|
|
118
|
+
)}
|
|
119
|
+
</>
|
|
110
120
|
);
|
|
111
121
|
}, [isLoadingMore, resolvedLoadingMoreText, ListFooterComponent, styles.footerLoader]);
|
|
112
122
|
|
|
123
|
+
// Vacío sólo cuando ya se sabe que no hay nada: durante la carga inicial se
|
|
124
|
+
// muestran placeholders, no "sin resultados".
|
|
113
125
|
const renderEmpty = useCallback(() => {
|
|
114
|
-
if (isLoading) return null;
|
|
126
|
+
if (isLoading || isRefreshing) return null;
|
|
115
127
|
return <EmptyState title={emptyTitle} description={emptyDescription} iconName={emptyIconName} style={{ marginTop: 50 }} />;
|
|
116
|
-
}, [isLoading, emptyTitle, emptyDescription, emptyIconName]);
|
|
128
|
+
}, [isLoading, isRefreshing, emptyTitle, emptyDescription, emptyIconName]);
|
|
117
129
|
|
|
118
130
|
if (!isReady) {
|
|
119
131
|
return (
|
|
@@ -123,15 +135,30 @@ export function List<T>({
|
|
|
123
135
|
);
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
<View style={styles.
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
if (isInitialLoading) {
|
|
139
|
+
return (
|
|
140
|
+
<View style={[styles.container, containerStyle]} testID="list-skeleton">
|
|
141
|
+
<View style={[styles.listContent, contentContainerStyle]}>
|
|
142
|
+
{Array.from({ length: skeletonCount }, (_, i) => (
|
|
143
|
+
<React.Fragment key={i}>
|
|
144
|
+
{renderSkeletonItem?.() ?? (
|
|
145
|
+
<View style={styles.skeletonRow}>
|
|
146
|
+
<Skeleton variant="circle" width={40} />
|
|
147
|
+
<View style={styles.skeletonText}>
|
|
148
|
+
<Skeleton width="60%" height={14} />
|
|
149
|
+
<Skeleton width="35%" height={12} />
|
|
150
|
+
</View>
|
|
151
|
+
</View>
|
|
152
|
+
)}
|
|
153
|
+
</React.Fragment>
|
|
154
|
+
))}
|
|
132
155
|
</View>
|
|
133
|
-
|
|
156
|
+
</View>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
134
159
|
|
|
160
|
+
return (
|
|
161
|
+
<View style={[styles.container, containerStyle]}>
|
|
135
162
|
<ListComponent
|
|
136
163
|
data={data}
|
|
137
164
|
keyExtractor={keyExtractor}
|
|
@@ -144,7 +171,12 @@ export function List<T>({
|
|
|
144
171
|
ListEmptyComponent={renderEmpty}
|
|
145
172
|
refreshControl={
|
|
146
173
|
onRefresh ? (
|
|
147
|
-
<RefreshControl
|
|
174
|
+
<RefreshControl
|
|
175
|
+
refreshing={isRefreshing}
|
|
176
|
+
onRefresh={onRefresh}
|
|
177
|
+
colors={[colors.primary]}
|
|
178
|
+
tintColor={colors.primary}
|
|
179
|
+
/>
|
|
148
180
|
) : undefined
|
|
149
181
|
}
|
|
150
182
|
{...listProps}
|