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,9 +1,19 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
FlatList,
|
|
5
|
+
RefreshControl,
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type FlatListProps,
|
|
9
|
+
type StyleProp,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
} from 'react-native';
|
|
3
12
|
import { createStyles } from '../theme/createStyles';
|
|
4
13
|
import { useColors, useLabels } from '../theme/UIProvider';
|
|
5
14
|
import { Input } from '../atoms/Input';
|
|
6
15
|
import { EmptyState } from '../atoms/EmptyState';
|
|
16
|
+
import { Skeleton } from '../atoms/Skeleton';
|
|
7
17
|
import { Spinner } from '../atoms/Spinner';
|
|
8
18
|
import { IconButton } from '../atoms/IconButton';
|
|
9
19
|
import type { IconName } from '../icons/types';
|
|
@@ -21,31 +31,50 @@ export interface SearchListProps<T> {
|
|
|
21
31
|
extraActions?: React.ReactNode;
|
|
22
32
|
|
|
23
33
|
data: T[];
|
|
24
|
-
keyExtractor: (item: T) => string;
|
|
34
|
+
keyExtractor: (item: T, index: number) => string;
|
|
25
35
|
renderItem: FlatListProps<T>['renderItem'];
|
|
26
36
|
|
|
37
|
+
/** Carga inicial: mientras sea true y no haya datos, se muestran placeholders. */
|
|
27
38
|
isLoading?: boolean;
|
|
28
|
-
/** 'replace' cambia toda la lista por un spinner; 'overlay' lo muestra encima manteniendo el layout. */
|
|
39
|
+
/** 'replace' cambia toda la lista por un spinner/skeleton; 'overlay' lo muestra encima manteniendo el layout. */
|
|
29
40
|
loadingMode?: 'replace' | 'overlay';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
onLoadMore?: () => void;
|
|
41
|
+
/** Recarga con datos ya en pantalla (pull-to-refresh). @default false */
|
|
42
|
+
isRefreshing?: boolean;
|
|
33
43
|
isLoadingMore?: boolean;
|
|
44
|
+
/** Controlado por el consumidor: mientras es false, se muestra un spinner de pantalla completa. @default true */
|
|
45
|
+
isReady?: boolean;
|
|
46
|
+
loadingText?: string;
|
|
34
47
|
loadingMoreText?: string;
|
|
35
48
|
hasMore?: boolean;
|
|
36
49
|
noMoreText?: string;
|
|
37
50
|
|
|
51
|
+
/** Filas fantasma de la carga inicial. @default 6 */
|
|
52
|
+
skeletonCount?: number;
|
|
53
|
+
/** Placeholder propio, para que calce con la forma real del ítem. */
|
|
54
|
+
renderSkeletonItem?: () => React.ReactElement;
|
|
55
|
+
|
|
56
|
+
onLoadMore?: () => void;
|
|
57
|
+
onEndReached?: () => void;
|
|
58
|
+
onEndReachedThreshold?: number;
|
|
59
|
+
onRefresh?: () => void;
|
|
60
|
+
|
|
38
61
|
emptyTitle?: string;
|
|
39
62
|
emptyDescription?: string;
|
|
40
63
|
emptyIconName?: IconName;
|
|
41
64
|
|
|
65
|
+
ListHeaderComponent?: FlatListProps<T>['ListHeaderComponent'];
|
|
66
|
+
ListFooterComponent?: FlatListProps<T>['ListFooterComponent'];
|
|
67
|
+
|
|
42
68
|
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
69
|
+
containerStyle?: ViewStyle;
|
|
70
|
+
|
|
71
|
+
/** Componente de lista a usar (FlashList, etc). @default FlatList */
|
|
43
72
|
ListComponent?: React.ComponentType<any>;
|
|
44
73
|
listProps?: Record<string, unknown>;
|
|
45
74
|
}
|
|
46
75
|
|
|
47
76
|
const useStyles = createStyles((theme) => ({
|
|
48
|
-
container: { flex: 1 },
|
|
77
|
+
container: { flex: 1, backgroundColor: theme.colors.background },
|
|
49
78
|
searchRow: { flexDirection: 'row', alignItems: 'center', marginBottom: theme.tokens.spacing.lg - 1, gap: theme.tokens.spacing.sm + 2 },
|
|
50
79
|
searchInput: { flex: 1, marginBottom: 0 },
|
|
51
80
|
scanButton: { backgroundColor: theme.colors.primary, width: 50, height: 50, borderRadius: theme.tokens.radius.lg, ...theme.tokens.shadow.sm },
|
|
@@ -64,6 +93,13 @@ const useStyles = createStyles((theme) => ({
|
|
|
64
93
|
},
|
|
65
94
|
footer: { paddingVertical: theme.tokens.spacing.xl, alignItems: 'center', gap: theme.tokens.spacing.md },
|
|
66
95
|
footerText: { color: theme.colors.textLight, fontSize: theme.tokens.fontSize.md },
|
|
96
|
+
skeletonRow: {
|
|
97
|
+
flexDirection: 'row',
|
|
98
|
+
alignItems: 'center',
|
|
99
|
+
gap: theme.tokens.spacing.md,
|
|
100
|
+
padding: theme.tokens.spacing.md,
|
|
101
|
+
},
|
|
102
|
+
skeletonText: { flex: 1, gap: theme.tokens.spacing.sm },
|
|
67
103
|
}));
|
|
68
104
|
|
|
69
105
|
/**
|
|
@@ -86,16 +122,26 @@ export function SearchList<T>({
|
|
|
86
122
|
renderItem,
|
|
87
123
|
isLoading = false,
|
|
88
124
|
loadingMode = 'replace',
|
|
89
|
-
|
|
90
|
-
onLoadMore,
|
|
125
|
+
isRefreshing = false,
|
|
91
126
|
isLoadingMore = false,
|
|
127
|
+
isReady = true,
|
|
128
|
+
loadingText,
|
|
92
129
|
loadingMoreText,
|
|
93
130
|
hasMore = false,
|
|
94
131
|
noMoreText,
|
|
132
|
+
skeletonCount = 6,
|
|
133
|
+
renderSkeletonItem,
|
|
134
|
+
onLoadMore,
|
|
135
|
+
onEndReached,
|
|
136
|
+
onEndReachedThreshold = 0.3,
|
|
137
|
+
onRefresh,
|
|
95
138
|
emptyTitle,
|
|
96
139
|
emptyDescription,
|
|
97
140
|
emptyIconName,
|
|
141
|
+
ListHeaderComponent,
|
|
142
|
+
ListFooterComponent,
|
|
98
143
|
contentContainerStyle,
|
|
144
|
+
containerStyle,
|
|
99
145
|
ListComponent = FlatList,
|
|
100
146
|
listProps,
|
|
101
147
|
}: SearchListProps<T>) {
|
|
@@ -106,30 +152,55 @@ export function SearchList<T>({
|
|
|
106
152
|
const resolvedLoadingText = loadingText ?? labels.loading;
|
|
107
153
|
const resolvedLoadingMoreText = loadingMoreText ?? labels.loadingMore;
|
|
108
154
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<View style={styles.footer}>
|
|
120
|
-
<Text style={styles.footerText}>{noMoreText}</Text>
|
|
121
|
-
</View>
|
|
155
|
+
const handleEndReached = onLoadMore ?? onEndReached;
|
|
156
|
+
|
|
157
|
+
// El footer propio y el loader de paginación conviven: antes el primero
|
|
158
|
+
// tapaba al segundo y la lista dejaba de avisar que estaba trayendo más.
|
|
159
|
+
const renderFooter = useCallback(() => {
|
|
160
|
+
const custom =
|
|
161
|
+
typeof ListFooterComponent === 'function' ? (
|
|
162
|
+
<ListFooterComponent />
|
|
163
|
+
) : (
|
|
164
|
+
(ListFooterComponent as React.ReactElement | undefined) ?? null
|
|
122
165
|
);
|
|
123
|
-
}
|
|
124
|
-
return null;
|
|
125
|
-
};
|
|
126
166
|
|
|
127
|
-
|
|
167
|
+
return (
|
|
168
|
+
<>
|
|
169
|
+
{custom}
|
|
170
|
+
{isLoadingMore && (
|
|
171
|
+
<View style={styles.footer}>
|
|
172
|
+
<Spinner text={resolvedLoadingMoreText} fullscreen={false} />
|
|
173
|
+
</View>
|
|
174
|
+
)}
|
|
175
|
+
{noMoreText && !hasMore && data.length > 0 && (
|
|
176
|
+
<View style={styles.footer}>
|
|
177
|
+
<Text style={styles.footerText}>{noMoreText}</Text>
|
|
178
|
+
</View>
|
|
179
|
+
)}
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
}, [ListFooterComponent, isLoadingMore, resolvedLoadingMoreText, noMoreText, hasMore, data.length, styles.footer, styles.footerText]);
|
|
183
|
+
|
|
184
|
+
// Vacío sólo cuando ya se sabe que no hay nada: durante la carga inicial se
|
|
185
|
+
// muestran placeholders, no "sin resultados".
|
|
186
|
+
const renderEmpty = useCallback(() => {
|
|
187
|
+
if (isLoading || isRefreshing) return null;
|
|
188
|
+
return <EmptyState title={emptyTitle} description={emptyDescription} iconName={emptyIconName} style={{ marginTop: 50 }} />;
|
|
189
|
+
}, [isLoading, isRefreshing, emptyTitle, emptyDescription, emptyIconName]);
|
|
190
|
+
|
|
191
|
+
if (!isReady) {
|
|
192
|
+
return (
|
|
193
|
+
<View style={[styles.container, containerStyle]}>
|
|
194
|
+
<Spinner text={resolvedLoadingText} />
|
|
195
|
+
</View>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const isInitialLoading = isLoading && loadingMode === 'replace' && data.length === 0;
|
|
128
200
|
const showOverlayLoading = isLoading && loadingMode === 'overlay' && data.length === 0;
|
|
129
|
-
const showEmpty = !isLoading && data.length === 0;
|
|
130
201
|
|
|
131
202
|
return (
|
|
132
|
-
<View style={styles.container}>
|
|
203
|
+
<View style={[styles.container, containerStyle]}>
|
|
133
204
|
{header}
|
|
134
205
|
|
|
135
206
|
<View style={styles.searchRow}>
|
|
@@ -163,20 +234,44 @@ export function SearchList<T>({
|
|
|
163
234
|
</View>
|
|
164
235
|
)}
|
|
165
236
|
|
|
166
|
-
{
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
237
|
+
{isInitialLoading ? (
|
|
238
|
+
<View testID="list-skeleton" style={[styles.listContent, contentContainerStyle]}>
|
|
239
|
+
{Array.from({ length: skeletonCount }, (_, i) => (
|
|
240
|
+
<React.Fragment key={i}>
|
|
241
|
+
{renderSkeletonItem?.() ?? (
|
|
242
|
+
<View style={styles.skeletonRow}>
|
|
243
|
+
<Skeleton variant="circle" width={40} />
|
|
244
|
+
<View style={styles.skeletonText}>
|
|
245
|
+
<Skeleton width="60%" height={14} />
|
|
246
|
+
<Skeleton width="35%" height={12} />
|
|
247
|
+
</View>
|
|
248
|
+
</View>
|
|
249
|
+
)}
|
|
250
|
+
</React.Fragment>
|
|
251
|
+
))}
|
|
252
|
+
</View>
|
|
170
253
|
) : (
|
|
171
254
|
<ListComponent
|
|
172
255
|
data={data}
|
|
173
256
|
keyExtractor={keyExtractor}
|
|
174
257
|
keyboardDismissMode="on-drag"
|
|
175
|
-
contentContainerStyle={
|
|
176
|
-
onEndReached={
|
|
177
|
-
onEndReachedThreshold={
|
|
258
|
+
contentContainerStyle={[styles.listContent, contentContainerStyle]}
|
|
259
|
+
onEndReached={handleEndReached}
|
|
260
|
+
onEndReachedThreshold={onEndReachedThreshold}
|
|
261
|
+
ListHeaderComponent={ListHeaderComponent}
|
|
178
262
|
ListFooterComponent={renderFooter}
|
|
263
|
+
ListEmptyComponent={renderEmpty}
|
|
179
264
|
renderItem={renderItem}
|
|
265
|
+
refreshControl={
|
|
266
|
+
onRefresh ? (
|
|
267
|
+
<RefreshControl
|
|
268
|
+
refreshing={isRefreshing}
|
|
269
|
+
onRefresh={onRefresh}
|
|
270
|
+
colors={[colors.primary]}
|
|
271
|
+
tintColor={colors.primary}
|
|
272
|
+
/>
|
|
273
|
+
) : undefined
|
|
274
|
+
}
|
|
180
275
|
{...listProps}
|
|
181
276
|
/>
|
|
182
277
|
)}
|
|
@@ -184,3 +279,4 @@ export function SearchList<T>({
|
|
|
184
279
|
</View>
|
|
185
280
|
);
|
|
186
281
|
}
|
|
282
|
+
|
package/src/organisms/Select.tsx
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
Keyboard,
|
|
5
|
+
Modal,
|
|
6
|
+
Platform,
|
|
7
|
+
Pressable,
|
|
8
|
+
ScrollView,
|
|
9
|
+
Text,
|
|
10
|
+
TextInput,
|
|
11
|
+
View,
|
|
12
|
+
useWindowDimensions,
|
|
13
|
+
type StyleProp,
|
|
14
|
+
type ViewStyle,
|
|
15
|
+
} from 'react-native';
|
|
3
16
|
import { createStyles } from '../theme/createStyles';
|
|
4
17
|
import { useAllowFontScaling, useColors, useLabels } from '../theme/UIProvider';
|
|
5
18
|
import { Icon } from '../icons/Icon';
|
|
19
|
+
import type { IconName } from '../icons/types';
|
|
6
20
|
|
|
7
21
|
export interface SelectOption<T> {
|
|
8
22
|
label: string;
|
|
9
23
|
value: T;
|
|
24
|
+
/** Glifo del set de la librería, a la izquierda del label. */
|
|
25
|
+
iconName?: IconName;
|
|
26
|
+
/** Emoji a la izquierda del label. Gana sobre `iconName`. */
|
|
27
|
+
emoji?: string;
|
|
28
|
+
disabled?: boolean;
|
|
10
29
|
}
|
|
11
30
|
|
|
12
31
|
export interface SelectProps<T> {
|
|
@@ -18,12 +37,24 @@ export interface SelectProps<T> {
|
|
|
18
37
|
placeholder?: string;
|
|
19
38
|
error?: string;
|
|
20
39
|
isLoading?: boolean;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
/** Agrega un buscador dentro del desplegable. @default false */
|
|
42
|
+
searchable?: boolean;
|
|
43
|
+
searchPlaceholder?: string;
|
|
44
|
+
/** Alto máximo del desplegable, si el espacio disponible lo permite. @default 280 */
|
|
45
|
+
maxHeight?: number;
|
|
46
|
+
onOpenChange?: (open: boolean) => void;
|
|
21
47
|
containerStyle?: StyleProp<ViewStyle>;
|
|
48
|
+
testID?: string;
|
|
22
49
|
}
|
|
23
50
|
|
|
51
|
+
/** Aire entre el campo y el desplegable. */
|
|
52
|
+
const GAP = 8;
|
|
53
|
+
/** Por debajo de esto el desplegable no muestra ni dos filas. */
|
|
54
|
+
const MIN_HEIGHT = 120;
|
|
55
|
+
|
|
24
56
|
const useStyles = createStyles((theme) => ({
|
|
25
57
|
container: { gap: theme.tokens.spacing.sm },
|
|
26
|
-
containerOpen: { zIndex: 20, elevation: 20 },
|
|
27
58
|
label: { fontWeight: theme.tokens.fontWeight.bold, fontSize: theme.tokens.fontSize.md, color: theme.colors.text },
|
|
28
59
|
required: { color: theme.colors.errorText },
|
|
29
60
|
field: {
|
|
@@ -37,23 +68,29 @@ const useStyles = createStyles((theme) => ({
|
|
|
37
68
|
minHeight: 50,
|
|
38
69
|
paddingHorizontal: theme.tokens.spacing.lg,
|
|
39
70
|
},
|
|
40
|
-
|
|
71
|
+
fieldOpen: { borderColor: theme.colors.primary },
|
|
72
|
+
fieldDisabled: { opacity: 0.5 },
|
|
73
|
+
fieldValue: { flexDirection: 'row', alignItems: 'center', gap: theme.tokens.spacing.sm, flexShrink: 1 },
|
|
74
|
+
fieldText: { fontSize: theme.tokens.fontSize.lg, color: theme.colors.text, flexShrink: 1 },
|
|
41
75
|
fieldPlaceholder: { color: theme.colors.textLight },
|
|
42
|
-
|
|
76
|
+
backdrop: { flex: 1 },
|
|
77
|
+
sheet: {
|
|
43
78
|
position: 'absolute',
|
|
44
|
-
top: '100%',
|
|
45
|
-
left: 0,
|
|
46
|
-
right: 0,
|
|
47
|
-
marginTop: theme.tokens.spacing.sm,
|
|
48
79
|
borderWidth: 2,
|
|
49
80
|
borderColor: theme.colors.border,
|
|
50
81
|
borderRadius: theme.tokens.radius.lg,
|
|
51
82
|
backgroundColor: theme.colors.surface,
|
|
52
83
|
overflow: 'hidden',
|
|
53
|
-
zIndex: 20,
|
|
54
|
-
elevation: 20,
|
|
55
84
|
...theme.tokens.shadow.md,
|
|
56
85
|
},
|
|
86
|
+
search: {
|
|
87
|
+
fontSize: theme.tokens.fontSize.md,
|
|
88
|
+
color: theme.colors.text,
|
|
89
|
+
paddingHorizontal: theme.tokens.spacing.lg,
|
|
90
|
+
paddingVertical: theme.tokens.spacing.md,
|
|
91
|
+
borderBottomWidth: 1,
|
|
92
|
+
borderBottomColor: theme.colors.border,
|
|
93
|
+
},
|
|
57
94
|
optionRow: {
|
|
58
95
|
flexDirection: 'row',
|
|
59
96
|
alignItems: 'center',
|
|
@@ -65,16 +102,33 @@ const useStyles = createStyles((theme) => ({
|
|
|
65
102
|
},
|
|
66
103
|
optionRowLast: { borderBottomWidth: 0 },
|
|
67
104
|
optionRowSelected: { backgroundColor: theme.colors.primarySoft },
|
|
105
|
+
optionRowDisabled: { opacity: 0.4 },
|
|
106
|
+
optionLabel: { flexDirection: 'row', alignItems: 'center', gap: theme.tokens.spacing.sm, flexShrink: 1 },
|
|
68
107
|
optionText: { fontSize: theme.tokens.fontSize.lg - 1, color: theme.colors.text },
|
|
69
108
|
optionTextSelected: { fontWeight: theme.tokens.fontWeight.semibold, color: theme.colors.primaryText },
|
|
109
|
+
emoji: { fontSize: theme.tokens.fontSize.lg },
|
|
110
|
+
empty: {
|
|
111
|
+
padding: theme.tokens.spacing.lg,
|
|
112
|
+
textAlign: 'center',
|
|
113
|
+
color: theme.colors.textLight,
|
|
114
|
+
fontSize: theme.tokens.fontSize.md,
|
|
115
|
+
},
|
|
70
116
|
errorText: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.errorText },
|
|
71
117
|
}));
|
|
72
118
|
|
|
119
|
+
interface Anchor {
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
width: number;
|
|
123
|
+
height: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
73
126
|
/**
|
|
74
127
|
* Select
|
|
75
|
-
* Dropdown
|
|
76
|
-
*
|
|
77
|
-
*
|
|
128
|
+
* Dropdown de una sola opción. La lista se mide contra la ventana y se dibuja
|
|
129
|
+
* dentro de un Modal, no en el árbol del campo: así ningún `overflow: hidden`
|
|
130
|
+
* la recorta, no depende del zIndex (poco confiable en Android) y el fondo del
|
|
131
|
+
* Modal resuelve cerrar tocando afuera y con el botón atrás.
|
|
78
132
|
*/
|
|
79
133
|
export function Select<T extends string | number>({
|
|
80
134
|
label,
|
|
@@ -85,17 +139,93 @@ export function Select<T extends string | number>({
|
|
|
85
139
|
placeholder,
|
|
86
140
|
error,
|
|
87
141
|
isLoading = false,
|
|
142
|
+
disabled = false,
|
|
143
|
+
searchable = false,
|
|
144
|
+
searchPlaceholder,
|
|
145
|
+
maxHeight = 280,
|
|
146
|
+
onOpenChange,
|
|
88
147
|
containerStyle,
|
|
148
|
+
testID,
|
|
89
149
|
}: SelectProps<T>) {
|
|
90
150
|
const colors = useColors();
|
|
91
151
|
const styles = useStyles();
|
|
92
152
|
const labels = useLabels();
|
|
93
153
|
const allowFontScaling = useAllowFontScaling();
|
|
154
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
155
|
+
|
|
156
|
+
const fieldRef = useRef<View>(null);
|
|
94
157
|
const [open, setOpen] = useState(false);
|
|
158
|
+
const [anchor, setAnchor] = useState<Anchor | null>(null);
|
|
159
|
+
const [query, setQuery] = useState('');
|
|
160
|
+
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
161
|
+
const isDisabled = disabled || isLoading;
|
|
95
162
|
const selectedOption = options.find((o) => o.value === selectedValue);
|
|
96
163
|
|
|
164
|
+
const close = useCallback(() => {
|
|
165
|
+
setOpen(false);
|
|
166
|
+
setAnchor(null);
|
|
167
|
+
setQuery('');
|
|
168
|
+
onOpenChange?.(false);
|
|
169
|
+
}, [onOpenChange]);
|
|
170
|
+
|
|
171
|
+
// Abrir no espera a la medición: si `measureInWindow` no responde, la lista
|
|
172
|
+
// igual aparece (centrada) en vez de que el campo deje de funcionar.
|
|
173
|
+
const openList = () => {
|
|
174
|
+
fieldRef.current?.measureInWindow((x, y, width, height) => setAnchor({ x, y, width, height }));
|
|
175
|
+
setOpen(true);
|
|
176
|
+
onOpenChange?.(true);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// El buscador abre el teclado encima del desplegable: sin descontarlo, la
|
|
180
|
+
// lista se dibuja donde ya no se ve.
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!open) return;
|
|
183
|
+
setKeyboardHeight(Keyboard.metrics()?.height ?? 0);
|
|
184
|
+
const [show, hide] =
|
|
185
|
+
Platform.OS === 'ios' ? ['keyboardWillShow', 'keyboardWillHide'] : ['keyboardDidShow', 'keyboardDidHide'];
|
|
186
|
+
const subs = [
|
|
187
|
+
Keyboard.addListener(show as 'keyboardDidShow', (e) => setKeyboardHeight(e.endCoordinates.height)),
|
|
188
|
+
Keyboard.addListener(hide as 'keyboardDidHide', () => setKeyboardHeight(0)),
|
|
189
|
+
];
|
|
190
|
+
return () => subs.forEach((sub) => sub.remove());
|
|
191
|
+
}, [open]);
|
|
192
|
+
|
|
193
|
+
const visibleOptions = useMemo(() => {
|
|
194
|
+
if (!searchable || query.trim() === '') return options;
|
|
195
|
+
const needle = query.trim().toLowerCase();
|
|
196
|
+
return options.filter((option) => option.label.toLowerCase().includes(needle));
|
|
197
|
+
}, [options, query, searchable]);
|
|
198
|
+
|
|
199
|
+
// Se abre hacia donde haya más aire: contra el borde inferior de la pantalla,
|
|
200
|
+
// hacia arriba es la única forma de que entre.
|
|
201
|
+
const placement = useMemo((): ViewStyle => {
|
|
202
|
+
if (!anchor) return { left: GAP * 2, right: GAP * 2, top: windowHeight * 0.25, maxHeight };
|
|
203
|
+
const usableHeight = windowHeight - keyboardHeight;
|
|
204
|
+
const below = usableHeight - (anchor.y + anchor.height) - GAP * 2;
|
|
205
|
+
const above = anchor.y - GAP * 2;
|
|
206
|
+
const downwards = below >= Math.min(maxHeight, above);
|
|
207
|
+
return {
|
|
208
|
+
left: anchor.x,
|
|
209
|
+
width: anchor.width,
|
|
210
|
+
maxHeight: Math.max(Math.min(maxHeight, downwards ? below : above), MIN_HEIGHT),
|
|
211
|
+
...(downwards ? { top: anchor.y + anchor.height + GAP } : { bottom: windowHeight - anchor.y + GAP }),
|
|
212
|
+
};
|
|
213
|
+
}, [anchor, keyboardHeight, maxHeight, windowHeight]);
|
|
214
|
+
|
|
215
|
+
const renderLeading = (option: SelectOption<T> | undefined, color: string) => {
|
|
216
|
+
if (!option) return null;
|
|
217
|
+
if (option.emoji) {
|
|
218
|
+
return (
|
|
219
|
+
<Text allowFontScaling={allowFontScaling} style={styles.emoji}>
|
|
220
|
+
{option.emoji}
|
|
221
|
+
</Text>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return option.iconName ? <Icon name={option.iconName} size={20} color={color} /> : null;
|
|
225
|
+
};
|
|
226
|
+
|
|
97
227
|
return (
|
|
98
|
-
<View style={[styles.container,
|
|
228
|
+
<View style={[styles.container, containerStyle]} testID={testID}>
|
|
99
229
|
{label && (
|
|
100
230
|
<Text allowFontScaling={allowFontScaling} style={styles.label}>
|
|
101
231
|
{label}
|
|
@@ -104,16 +234,25 @@ export function Select<T extends string | number>({
|
|
|
104
234
|
)}
|
|
105
235
|
|
|
106
236
|
<Pressable
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
237
|
+
ref={fieldRef}
|
|
238
|
+
style={[styles.field, open && styles.fieldOpen, isDisabled && styles.fieldDisabled]}
|
|
239
|
+
onPress={openList}
|
|
240
|
+
disabled={isDisabled}
|
|
241
|
+
accessibilityRole="combobox"
|
|
111
242
|
accessibilityLabel={label ?? placeholder ?? labels.selectPlaceholder}
|
|
112
|
-
|
|
243
|
+
accessibilityValue={{ text: selectedOption?.label ?? '' }}
|
|
244
|
+
accessibilityState={{ expanded: open, disabled: isDisabled }}
|
|
113
245
|
>
|
|
114
|
-
<
|
|
115
|
-
{selectedOption
|
|
116
|
-
|
|
246
|
+
<View style={styles.fieldValue}>
|
|
247
|
+
{renderLeading(selectedOption, colors.textLight)}
|
|
248
|
+
<Text
|
|
249
|
+
allowFontScaling={allowFontScaling}
|
|
250
|
+
numberOfLines={1}
|
|
251
|
+
style={[styles.fieldText, !selectedOption && styles.fieldPlaceholder]}
|
|
252
|
+
>
|
|
253
|
+
{selectedOption?.label || placeholder || labels.selectPlaceholder}
|
|
254
|
+
</Text>
|
|
255
|
+
</View>
|
|
117
256
|
{isLoading ? (
|
|
118
257
|
<ActivityIndicator size="small" color={colors.primary} />
|
|
119
258
|
) : (
|
|
@@ -121,29 +260,68 @@ export function Select<T extends string | number>({
|
|
|
121
260
|
)}
|
|
122
261
|
</Pressable>
|
|
123
262
|
|
|
124
|
-
{open &&
|
|
125
|
-
<
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
263
|
+
{open && (
|
|
264
|
+
<Modal visible transparent animationType="fade" statusBarTranslucent onRequestClose={close}>
|
|
265
|
+
<Pressable style={styles.backdrop} onPress={close} testID="select-backdrop" />
|
|
266
|
+
|
|
267
|
+
<View style={[styles.sheet, placement]} accessibilityRole="radiogroup" testID="select-list">
|
|
268
|
+
{searchable && (
|
|
269
|
+
<TextInput
|
|
270
|
+
allowFontScaling={allowFontScaling}
|
|
271
|
+
style={styles.search}
|
|
272
|
+
value={query}
|
|
273
|
+
onChangeText={setQuery}
|
|
274
|
+
placeholder={searchPlaceholder ?? labels.search}
|
|
275
|
+
placeholderTextColor={colors.textLight}
|
|
276
|
+
autoFocus
|
|
277
|
+
accessibilityLabel={searchPlaceholder ?? labels.search}
|
|
278
|
+
/>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
<ScrollView keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false} bounces={false}>
|
|
282
|
+
{visibleOptions.map((option, index) => {
|
|
283
|
+
const isSelected = option.value === selectedValue;
|
|
284
|
+
return (
|
|
285
|
+
<Pressable
|
|
286
|
+
key={String(option.value)}
|
|
287
|
+
style={[
|
|
288
|
+
styles.optionRow,
|
|
289
|
+
index === visibleOptions.length - 1 && styles.optionRowLast,
|
|
290
|
+
isSelected && styles.optionRowSelected,
|
|
291
|
+
option.disabled && styles.optionRowDisabled,
|
|
292
|
+
]}
|
|
293
|
+
onPress={() => {
|
|
294
|
+
onSelect(option.value);
|
|
295
|
+
close();
|
|
296
|
+
}}
|
|
297
|
+
disabled={option.disabled}
|
|
298
|
+
accessibilityRole="radio"
|
|
299
|
+
accessibilityLabel={option.label}
|
|
300
|
+
accessibilityState={{ selected: isSelected, checked: isSelected, disabled: !!option.disabled }}
|
|
301
|
+
>
|
|
302
|
+
<View style={styles.optionLabel}>
|
|
303
|
+
{renderLeading(option, isSelected ? colors.primary : colors.textLight)}
|
|
304
|
+
<Text
|
|
305
|
+
allowFontScaling={allowFontScaling}
|
|
306
|
+
numberOfLines={1}
|
|
307
|
+
style={[styles.optionText, isSelected && styles.optionTextSelected]}
|
|
308
|
+
>
|
|
309
|
+
{option.label}
|
|
310
|
+
</Text>
|
|
311
|
+
</View>
|
|
312
|
+
{isSelected && <Icon name="check" size={18} color={colors.primary} />}
|
|
313
|
+
</Pressable>
|
|
314
|
+
);
|
|
315
|
+
})}
|
|
316
|
+
|
|
317
|
+
{visibleOptions.length === 0 && (
|
|
318
|
+
<Text allowFontScaling={allowFontScaling} style={styles.empty}>
|
|
319
|
+
{labels.noResultsTitle}
|
|
141
320
|
</Text>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
</View>
|
|
321
|
+
)}
|
|
322
|
+
</ScrollView>
|
|
323
|
+
</View>
|
|
324
|
+
</Modal>
|
|
147
325
|
)}
|
|
148
326
|
|
|
149
327
|
{!!error && (
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { memo, useMemo } from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { useThemeMode, type ThemeMode } from '../theme/UIProvider';
|
|
4
|
+
import { SegmentedControl } from '../molecules/SegmentedControl';
|
|
5
|
+
|
|
6
|
+
export interface ThemeToggleProps {
|
|
7
|
+
style?: StyleProp<ViewStyle>;
|
|
8
|
+
labels?: Partial<Record<ThemeMode, string>>;
|
|
9
|
+
testID?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MODES: ThemeMode[] = ['system', 'light', 'dark'];
|
|
13
|
+
const DEFAULT_LABELS: Record<ThemeMode, string> = { system: 'Auto', light: 'Light', dark: 'Dark' };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ThemeToggle
|
|
17
|
+
* Conecta un SegmentedControl con UIProvider.setMode. Sin persistencia — el
|
|
18
|
+
* consumidor la resuelve vía onModeChange en <UIProvider>.
|
|
19
|
+
*/
|
|
20
|
+
export const ThemeToggle = memo(({ style, labels: labelOverrides, testID }: ThemeToggleProps) => {
|
|
21
|
+
const { mode, setMode } = useThemeMode();
|
|
22
|
+
|
|
23
|
+
const options = useMemo(() => {
|
|
24
|
+
const labels = { ...DEFAULT_LABELS, ...labelOverrides };
|
|
25
|
+
return MODES.map((value) => ({ value, label: labels[value] }));
|
|
26
|
+
}, [labelOverrides]);
|
|
27
|
+
|
|
28
|
+
return <SegmentedControl options={options} value={mode} onChange={setMode} style={style} testID={testID} />;
|
|
29
|
+
});
|
|
30
|
+
ThemeToggle.displayName = 'ThemeToggle';
|
package/src/organisms/Toast.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { memo
|
|
2
|
-
import {
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import { Pressable, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import { createStyles } from '../theme/createStyles';
|
|
4
4
|
import { useAllowFontScaling, useColors } from '../theme/UIProvider';
|
|
5
|
+
import { Transition } from '../atoms/Transition';
|
|
5
6
|
import { Icon } from '../icons/Icon';
|
|
6
7
|
import type { IconName } from '../icons/types';
|
|
7
8
|
|
|
@@ -120,24 +121,10 @@ Toast.displayName = 'Toast';
|
|
|
120
121
|
|
|
121
122
|
/** Envuelve un Toast con su animación de entrada/salida. Uso interno del provider. */
|
|
122
123
|
export const AnimatedToast = memo(
|
|
123
|
-
({ children, onHeight }: { children: React.ReactNode; onHeight?: (h: number) => void }) =>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}, [progress]);
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<Animated.View
|
|
132
|
-
onLayout={(e) => onHeight?.(e.nativeEvent.layout.height)}
|
|
133
|
-
style={{
|
|
134
|
-
opacity: progress,
|
|
135
|
-
transform: [{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [-16, 0] }) }],
|
|
136
|
-
}}
|
|
137
|
-
>
|
|
138
|
-
{children}
|
|
139
|
-
</Animated.View>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
124
|
+
({ children, onHeight }: { children: React.ReactNode; onHeight?: (h: number) => void }) => (
|
|
125
|
+
<Transition preset={['fade', 'slide-down']} distance={16} spring>
|
|
126
|
+
<View onLayout={(e) => onHeight?.(e.nativeEvent.layout.height)}>{children}</View>
|
|
127
|
+
</Transition>
|
|
128
|
+
)
|
|
142
129
|
);
|
|
143
130
|
AnimatedToast.displayName = 'AnimatedToast';
|