orn-ui 0.2.6 → 0.2.8
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/AGENTS.md +372 -0
- package/README.md +46 -15
- package/lib/commonjs/atoms/Gradient.js +111 -0
- package/lib/commonjs/atoms/Gradient.js.map +1 -0
- package/lib/commonjs/atoms/index.js +7 -0
- package/lib/commonjs/atoms/index.js.map +1 -1
- package/lib/commonjs/organisms/Slides.js +406 -0
- package/lib/commonjs/organisms/Slides.js.map +1 -0
- package/lib/commonjs/organisms/index.js +7 -0
- package/lib/commonjs/organisms/index.js.map +1 -1
- package/lib/commonjs/organisms/useSlides.js +186 -0
- package/lib/commonjs/organisms/useSlides.js.map +1 -0
- package/lib/module/atoms/Gradient.js +106 -0
- package/lib/module/atoms/Gradient.js.map +1 -0
- package/lib/module/atoms/index.js +1 -0
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/organisms/Slides.js +401 -0
- package/lib/module/organisms/Slides.js.map +1 -0
- package/lib/module/organisms/index.js +1 -0
- package/lib/module/organisms/index.js.map +1 -1
- package/lib/module/organisms/useSlides.js +182 -0
- package/lib/module/organisms/useSlides.js.map +1 -0
- package/lib/typescript/atoms/Badge.d.ts +1 -1
- package/lib/typescript/atoms/Badge.d.ts.map +1 -1
- package/lib/typescript/atoms/Gradient.d.ts +37 -0
- package/lib/typescript/atoms/Gradient.d.ts.map +1 -0
- package/lib/typescript/atoms/Image.d.ts +6 -6
- package/lib/typescript/atoms/Image.d.ts.map +1 -1
- package/lib/typescript/atoms/Input.d.ts +1 -1
- package/lib/typescript/atoms/Input.d.ts.map +1 -1
- package/lib/typescript/atoms/PressableScale.d.ts +1 -1
- package/lib/typescript/atoms/PressableScale.d.ts.map +1 -1
- package/lib/typescript/atoms/Skeleton.d.ts +5 -5
- package/lib/typescript/atoms/Skeleton.d.ts.map +1 -1
- package/lib/typescript/atoms/Spinner.d.ts +4 -4
- package/lib/typescript/atoms/Spinner.d.ts.map +1 -1
- package/lib/typescript/atoms/Transition.d.ts +10 -10
- package/lib/typescript/atoms/Transition.d.ts.map +1 -1
- package/lib/typescript/atoms/index.d.ts +2 -0
- package/lib/typescript/atoms/index.d.ts.map +1 -1
- package/lib/typescript/molecules/AvatarHeader.d.ts +2 -2
- package/lib/typescript/molecules/AvatarHeader.d.ts.map +1 -1
- package/lib/typescript/molecules/FormActions.d.ts +1 -1
- package/lib/typescript/molecules/FormActions.d.ts.map +1 -1
- package/lib/typescript/molecules/OptionCard.d.ts +2 -2
- package/lib/typescript/molecules/OptionCard.d.ts.map +1 -1
- package/lib/typescript/molecules/Stepper.d.ts +8 -8
- package/lib/typescript/molecules/Stepper.d.ts.map +1 -1
- package/lib/typescript/molecules/Steps.d.ts +3 -3
- package/lib/typescript/molecules/Steps.d.ts.map +1 -1
- package/lib/typescript/molecules/SymmetricGrid.d.ts +4 -4
- package/lib/typescript/molecules/SymmetricGrid.d.ts.map +1 -1
- package/lib/typescript/organisms/BottomSheet.d.ts +8 -8
- package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/organisms/DateField.d.ts +10 -10
- package/lib/typescript/organisms/DateField.d.ts.map +1 -1
- package/lib/typescript/organisms/DatePicker.d.ts +10 -10
- package/lib/typescript/organisms/DatePicker.d.ts.map +1 -1
- package/lib/typescript/organisms/List.d.ts +6 -6
- package/lib/typescript/organisms/List.d.ts.map +1 -1
- package/lib/typescript/organisms/NavigationBar.d.ts +2 -2
- package/lib/typescript/organisms/NavigationBar.d.ts.map +1 -1
- package/lib/typescript/organisms/ReorderableList.d.ts +7 -7
- package/lib/typescript/organisms/ReorderableList.d.ts.map +1 -1
- package/lib/typescript/organisms/Screen.d.ts +6 -6
- package/lib/typescript/organisms/SearchList.d.ts +8 -8
- package/lib/typescript/organisms/SearchList.d.ts.map +1 -1
- package/lib/typescript/organisms/Select.d.ts +2 -2
- package/lib/typescript/organisms/Select.d.ts.map +1 -1
- package/lib/typescript/organisms/Slides.d.ts +69 -0
- package/lib/typescript/organisms/Slides.d.ts.map +1 -0
- package/lib/typescript/organisms/Toast.d.ts +2 -2
- package/lib/typescript/organisms/Toast.d.ts.map +1 -1
- package/lib/typescript/organisms/ToastProvider.d.ts +2 -2
- package/lib/typescript/organisms/ToastProvider.d.ts.map +1 -1
- package/lib/typescript/organisms/Wizard.d.ts +7 -7
- package/lib/typescript/organisms/Wizard.d.ts.map +1 -1
- package/lib/typescript/organisms/index.d.ts +2 -0
- package/lib/typescript/organisms/index.d.ts.map +1 -1
- package/lib/typescript/organisms/useSlides.d.ts +52 -0
- package/lib/typescript/organisms/useSlides.d.ts.map +1 -0
- package/lib/typescript/theme/UIProvider.d.ts +9 -9
- package/lib/typescript/theme/UIProvider.d.ts.map +1 -1
- package/llms.txt +34 -0
- package/package.json +34 -6
- package/registry/avatar-header.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/bottom-sheet.json +1 -1
- package/registry/core.json +1 -1
- package/registry/date-field.json +1 -1
- package/registry/date-picker.json +1 -1
- package/registry/form-actions.json +1 -1
- package/registry/gradient.json +18 -0
- package/registry/image.json +1 -1
- package/registry/index.json +52 -26
- package/registry/input.json +1 -1
- package/registry/list.json +1 -1
- package/registry/manifest.json +14 -0
- package/registry/navigation-bar.json +1 -1
- package/registry/option-card.json +1 -1
- package/registry/pressable-scale.json +1 -1
- package/registry/reorderable-list.json +1 -1
- package/registry/screen.json +1 -1
- package/registry/search-list.json +1 -1
- package/registry/select.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/slides.json +24 -0
- package/registry/spinner.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/steps.json +1 -1
- package/registry/symmetric-grid.json +1 -1
- package/registry/toast-provider.json +1 -1
- package/registry/toast.json +1 -1
- package/registry/transition.json +1 -1
- package/registry/wizard.json +1 -1
- package/src/atoms/Badge.tsx +1 -1
- package/src/atoms/Gradient.tsx +99 -0
- package/src/atoms/Image.tsx +6 -6
- package/src/atoms/Input.tsx +1 -1
- package/src/atoms/PressableScale.tsx +1 -1
- package/src/atoms/Skeleton.tsx +5 -5
- package/src/atoms/Spinner.tsx +4 -4
- package/src/atoms/Transition.tsx +10 -10
- package/src/atoms/index.ts +2 -0
- package/src/molecules/AvatarHeader.tsx +2 -2
- package/src/molecules/FormActions.tsx +1 -1
- package/src/molecules/OptionCard.tsx +2 -2
- package/src/molecules/Stepper.tsx +8 -8
- package/src/molecules/Steps.tsx +3 -3
- package/src/molecules/SymmetricGrid.tsx +4 -4
- package/src/organisms/BottomSheet.tsx +8 -8
- package/src/organisms/DateField.tsx +10 -10
- package/src/organisms/DatePicker.tsx +10 -10
- package/src/organisms/List.tsx +6 -6
- package/src/organisms/NavigationBar.tsx +2 -2
- package/src/organisms/ReorderableList.tsx +7 -7
- package/src/organisms/Screen.tsx +6 -6
- package/src/organisms/SearchList.tsx +8 -8
- package/src/organisms/Select.tsx +2 -2
- package/src/organisms/Slides.tsx +413 -0
- package/src/organisms/Toast.tsx +2 -2
- package/src/organisms/ToastProvider.tsx +2 -2
- package/src/organisms/Wizard.tsx +7 -7
- package/src/organisms/index.ts +8 -0
- package/src/organisms/useSlides.ts +220 -0
- package/src/theme/UIProvider.tsx +9 -9
|
@@ -19,7 +19,7 @@ import { IconButton } from '../atoms/IconButton';
|
|
|
19
19
|
import type { IconName } from '../icons/types';
|
|
20
20
|
|
|
21
21
|
export interface SearchListProps<T> {
|
|
22
|
-
/**
|
|
22
|
+
/** Optional chrome above the search bar (e.g. back button + title). */
|
|
23
23
|
header?: React.ReactNode;
|
|
24
24
|
|
|
25
25
|
searchValue: string;
|
|
@@ -34,23 +34,23 @@ export interface SearchListProps<T> {
|
|
|
34
34
|
keyExtractor: (item: T, index: number) => string;
|
|
35
35
|
renderItem: FlatListProps<T>['renderItem'];
|
|
36
36
|
|
|
37
|
-
/**
|
|
37
|
+
/** Initial load: while true and there's no data, placeholders are shown. */
|
|
38
38
|
isLoading?: boolean;
|
|
39
|
-
/** 'replace'
|
|
39
|
+
/** 'replace' swaps the whole list for a spinner/skeleton; 'overlay' shows it on top while keeping the layout. */
|
|
40
40
|
loadingMode?: 'replace' | 'overlay';
|
|
41
|
-
/**
|
|
41
|
+
/** Reload with data already on screen (pull-to-refresh). @default false */
|
|
42
42
|
isRefreshing?: boolean;
|
|
43
43
|
isLoadingMore?: boolean;
|
|
44
|
-
/**
|
|
44
|
+
/** Controlled by the consumer: while false, a fullscreen spinner is shown. @default true */
|
|
45
45
|
isReady?: boolean;
|
|
46
46
|
loadingText?: string;
|
|
47
47
|
loadingMoreText?: string;
|
|
48
48
|
hasMore?: boolean;
|
|
49
49
|
noMoreText?: string;
|
|
50
50
|
|
|
51
|
-
/**
|
|
51
|
+
/** Ghost rows for the initial load. @default 6 */
|
|
52
52
|
skeletonCount?: number;
|
|
53
|
-
/**
|
|
53
|
+
/** Custom placeholder, to match the real shape of the item. */
|
|
54
54
|
renderSkeletonItem?: () => React.ReactElement;
|
|
55
55
|
|
|
56
56
|
onLoadMore?: () => void;
|
|
@@ -68,7 +68,7 @@ export interface SearchListProps<T> {
|
|
|
68
68
|
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
69
69
|
containerStyle?: ViewStyle;
|
|
70
70
|
|
|
71
|
-
/**
|
|
71
|
+
/** List component to use (FlashList, etc). @default FlatList */
|
|
72
72
|
ListComponent?: React.ComponentType<any>;
|
|
73
73
|
listProps?: Record<string, unknown>;
|
|
74
74
|
}
|
package/src/organisms/Select.tsx
CHANGED
|
@@ -38,10 +38,10 @@ export interface SelectProps<T> {
|
|
|
38
38
|
error?: string;
|
|
39
39
|
isLoading?: boolean;
|
|
40
40
|
disabled?: boolean;
|
|
41
|
-
/**
|
|
41
|
+
/** Adds a search box inside the dropdown. @default false */
|
|
42
42
|
searchable?: boolean;
|
|
43
43
|
searchPlaceholder?: string;
|
|
44
|
-
/**
|
|
44
|
+
/** Maximum height of the dropdown, if the available space allows it. @default 280 */
|
|
45
45
|
maxHeight?: number;
|
|
46
46
|
onOpenChange?: (open: boolean) => void;
|
|
47
47
|
containerStyle?: StyleProp<ViewStyle>;
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import React, { memo, useEffect, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Easing,
|
|
5
|
+
ScrollView,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Text,
|
|
8
|
+
TouchableOpacity,
|
|
9
|
+
View,
|
|
10
|
+
type StyleProp,
|
|
11
|
+
type ViewStyle,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import { createStyles } from '../theme/createStyles';
|
|
14
|
+
import { useAllowFontScaling, useColors, useTheme } from '../theme/UIProvider';
|
|
15
|
+
import { useReduceMotion } from '../atoms/Transition';
|
|
16
|
+
import { Gradient, type GradientDirection } from '../atoms/Gradient';
|
|
17
|
+
import { useSlides, type SlidesOrientation } from './useSlides';
|
|
18
|
+
|
|
19
|
+
export type { SlidesOrientation } from './useSlides';
|
|
20
|
+
export type SlidesIndicators = 'dots' | 'numbers' | 'none';
|
|
21
|
+
export type SlidesIndicatorPlacement = 'overlay' | 'outside';
|
|
22
|
+
|
|
23
|
+
/** Un color sólido, o dos o más para un degradado (ver `Gradient`). */
|
|
24
|
+
export type SlideBackground = string | string[];
|
|
25
|
+
|
|
26
|
+
export interface SlidesProps<T> {
|
|
27
|
+
data: T[];
|
|
28
|
+
keyExtractor: (item: T, index: number) => string;
|
|
29
|
+
/** Contenido de la slide: imágenes, botones, lo que sea. Se dibuja sobre `background`. */
|
|
30
|
+
renderItem: (item: T, index: number) => React.ReactNode;
|
|
31
|
+
/** Fondo por slide: `'#101014'` pinta plano, `['#004cef', '#00cae1']` degrada. */
|
|
32
|
+
background?: (item: T, index: number) => SlideBackground | undefined;
|
|
33
|
+
/** Dirección del degradado cuando `background` devuelve varios colores. @default 'vertical' */
|
|
34
|
+
gradientDirection?: GradientDirection;
|
|
35
|
+
/** @default 'horizontal' */
|
|
36
|
+
orientation?: SlidesOrientation;
|
|
37
|
+
/** Círculos, círculos numerados, o ninguno. @default 'dots' */
|
|
38
|
+
indicators?: SlidesIndicators;
|
|
39
|
+
/**
|
|
40
|
+
* 'overlay' los apoya sobre la slide, en blanco; 'outside' los saca fuera
|
|
41
|
+
* del área deslizable, con los colores del tema.
|
|
42
|
+
* @default 'overlay'
|
|
43
|
+
*/
|
|
44
|
+
indicatorPlacement?: SlidesIndicatorPlacement;
|
|
45
|
+
/** Después de la última vuelve a la primera (y al revés). @default false */
|
|
46
|
+
loop?: boolean;
|
|
47
|
+
/** Avanza solo. Se pausa mientras el dedo arrastra. @default false */
|
|
48
|
+
autoPlay?: boolean;
|
|
49
|
+
/** ms entre avances de `autoPlay`. @default 4000 */
|
|
50
|
+
interval?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Botón para frenar el avance automático. Sólo aparece con `autoPlay`, y va
|
|
53
|
+
* en `true` porque contenido que se mueve solo necesita una forma de
|
|
54
|
+
* pausarlo (WCAG 2.2.2).
|
|
55
|
+
* @default true
|
|
56
|
+
*/
|
|
57
|
+
showAutoPlayToggle?: boolean;
|
|
58
|
+
/** Hueco entre slides, en px. En 0 quedan pegadas. @default 0 */
|
|
59
|
+
spacing?: number;
|
|
60
|
+
/** Índice controlado. Sin él, el componente maneja el suyo desde `defaultIndex`. */
|
|
61
|
+
index?: number;
|
|
62
|
+
/** @default 0 */
|
|
63
|
+
defaultIndex?: number;
|
|
64
|
+
onIndexChange?: (index: number) => void;
|
|
65
|
+
/** Alto del área deslizable. @default 240 */
|
|
66
|
+
height?: number;
|
|
67
|
+
/** @default true */
|
|
68
|
+
swipeEnabled?: boolean;
|
|
69
|
+
/** Estilos de la caja visible de cada slide — padding, alineación, radio. */
|
|
70
|
+
slideStyle?: StyleProp<ViewStyle>;
|
|
71
|
+
style?: StyleProp<ViewStyle>;
|
|
72
|
+
testID?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const DOT = 8;
|
|
76
|
+
const DOT_ACTIVE = 20;
|
|
77
|
+
const NUMBER_CIRCLE = 24;
|
|
78
|
+
const TOGGLE = 32;
|
|
79
|
+
const DEFAULT_HEIGHT = 240;
|
|
80
|
+
const DEFAULT_INTERVAL = 4000;
|
|
81
|
+
|
|
82
|
+
const useStyles = createStyles((theme) => ({
|
|
83
|
+
// Deja redondear el bloque por `style` sin que el degradado se salga.
|
|
84
|
+
root: { overflow: 'hidden' },
|
|
85
|
+
viewport: { flex: 1 },
|
|
86
|
+
// La slide visible es la caja de adentro: con `spacing` la página es más
|
|
87
|
+
// ancha que ella, y el fondo tiene que recortarse acá y no en la página, o
|
|
88
|
+
// el degradado pinta también el hueco.
|
|
89
|
+
slide: { flex: 1, overflow: 'hidden' },
|
|
90
|
+
// Con indicadores fuera y orientación vertical el contenedor es una fila:
|
|
91
|
+
// sin flex la ventana no tiene ancho propio y colapsa a 0.
|
|
92
|
+
viewportInRow: { flex: 1 },
|
|
93
|
+
slideContent: { flex: 1 },
|
|
94
|
+
|
|
95
|
+
indicatorsRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
|
|
96
|
+
indicatorsColumn: { flexDirection: 'column', alignItems: 'center', justifyContent: 'center' },
|
|
97
|
+
indicatorsOverlayHorizontal: { position: 'absolute', left: 0, right: 0, bottom: theme.tokens.spacing.md },
|
|
98
|
+
indicatorsOverlayVertical: { position: 'absolute', top: 0, bottom: 0, right: theme.tokens.spacing.md },
|
|
99
|
+
indicatorsOutsideHorizontal: { paddingTop: theme.tokens.spacing.md },
|
|
100
|
+
indicatorsOutsideVertical: { paddingLeft: theme.tokens.spacing.md },
|
|
101
|
+
indicatorSlot: { padding: theme.tokens.spacing.xs },
|
|
102
|
+
|
|
103
|
+
dot: { width: DOT, height: DOT, borderRadius: DOT / 2 },
|
|
104
|
+
|
|
105
|
+
autoPlayToggle: {
|
|
106
|
+
position: 'absolute',
|
|
107
|
+
top: theme.tokens.spacing.md,
|
|
108
|
+
right: theme.tokens.spacing.md,
|
|
109
|
+
width: TOGGLE,
|
|
110
|
+
height: TOGGLE,
|
|
111
|
+
borderRadius: TOGGLE / 2,
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
justifyContent: 'center',
|
|
114
|
+
flexDirection: 'row',
|
|
115
|
+
gap: 3,
|
|
116
|
+
},
|
|
117
|
+
pauseBar: { width: 3, height: 12, borderRadius: 1 },
|
|
118
|
+
// Triángulo con bordes: un "play" sin SVG ni set de íconos.
|
|
119
|
+
playTriangle: {
|
|
120
|
+
width: 0,
|
|
121
|
+
height: 0,
|
|
122
|
+
marginLeft: 2,
|
|
123
|
+
borderTopWidth: 6,
|
|
124
|
+
borderBottomWidth: 6,
|
|
125
|
+
borderLeftWidth: 10,
|
|
126
|
+
borderTopColor: 'transparent',
|
|
127
|
+
borderBottomColor: 'transparent',
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
numberCircle: {
|
|
131
|
+
minWidth: NUMBER_CIRCLE,
|
|
132
|
+
height: NUMBER_CIRCLE,
|
|
133
|
+
borderRadius: NUMBER_CIRCLE / 2,
|
|
134
|
+
borderWidth: 1,
|
|
135
|
+
paddingHorizontal: theme.tokens.spacing.xs,
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
justifyContent: 'center',
|
|
138
|
+
},
|
|
139
|
+
numberText: { fontSize: theme.tokens.fontSize.xs, fontWeight: theme.tokens.fontWeight.semibold },
|
|
140
|
+
|
|
141
|
+
outsideHorizontal: { flexDirection: 'column' },
|
|
142
|
+
outsideVertical: { flexDirection: 'row' },
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
interface DotProps {
|
|
146
|
+
selected: boolean;
|
|
147
|
+
isHorizontal: boolean;
|
|
148
|
+
/** En overlay el contraste lo da la opacidad; afuera, el color. */
|
|
149
|
+
overlay: boolean;
|
|
150
|
+
activeColor: string;
|
|
151
|
+
inactiveColor: string;
|
|
152
|
+
testID?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* El punto activo se estira en vez de saltar: el cambio de golpe se lee como
|
|
157
|
+
* "apareció otro punto", no como "el mismo se movió". Ancho y color son props
|
|
158
|
+
* de layout/pintura, así que van por el driver de JS.
|
|
159
|
+
*/
|
|
160
|
+
const Dot = memo(({ selected, isHorizontal, overlay, activeColor, inactiveColor, testID }: DotProps) => {
|
|
161
|
+
const styles = useStyles();
|
|
162
|
+
const theme = useTheme();
|
|
163
|
+
const reduceMotion = useReduceMotion();
|
|
164
|
+
const progress = useRef(new Animated.Value(selected ? 1 : 0)).current;
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
Animated.timing(progress, {
|
|
168
|
+
toValue: selected ? 1 : 0,
|
|
169
|
+
duration: reduceMotion ? 0 : theme.tokens.duration.base,
|
|
170
|
+
easing: Easing.out(Easing.quad),
|
|
171
|
+
useNativeDriver: false,
|
|
172
|
+
}).start();
|
|
173
|
+
}, [progress, reduceMotion, selected, theme]);
|
|
174
|
+
|
|
175
|
+
const length = progress.interpolate({ inputRange: [0, 1], outputRange: [DOT, DOT_ACTIVE] });
|
|
176
|
+
const backgroundColor = progress.interpolate({ inputRange: [0, 1], outputRange: [inactiveColor, activeColor] });
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<Animated.View
|
|
180
|
+
testID={testID}
|
|
181
|
+
style={[
|
|
182
|
+
styles.dot,
|
|
183
|
+
isHorizontal ? { width: length } : { height: length },
|
|
184
|
+
{ backgroundColor },
|
|
185
|
+
overlay && { opacity: progress.interpolate({ inputRange: [0, 1], outputRange: [0.45, 1] }) },
|
|
186
|
+
]}
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
Dot.displayName = 'SlidesDot';
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Slides
|
|
194
|
+
* Carrusel con fondo propio (color plano o degradado) y contenido libre:
|
|
195
|
+
* `renderItem` decide si adentro va una imagen, texto o botones. El estado
|
|
196
|
+
* vive en `useSlides`; acá sólo está el árbol.
|
|
197
|
+
*
|
|
198
|
+
* El deslizamiento es un `ScrollView` con `pagingEnabled` en vez de gestos a
|
|
199
|
+
* mano: es lo único que da snap nativo en iOS y Android sin
|
|
200
|
+
* `react-native-gesture-handler`, en todo el rango soportado (Expo SDK 54–57).
|
|
201
|
+
*/
|
|
202
|
+
export function Slides<T>({
|
|
203
|
+
data,
|
|
204
|
+
keyExtractor,
|
|
205
|
+
renderItem,
|
|
206
|
+
background,
|
|
207
|
+
gradientDirection = 'vertical',
|
|
208
|
+
orientation = 'horizontal',
|
|
209
|
+
indicators = 'dots',
|
|
210
|
+
indicatorPlacement = 'overlay',
|
|
211
|
+
loop = false,
|
|
212
|
+
autoPlay = false,
|
|
213
|
+
interval = DEFAULT_INTERVAL,
|
|
214
|
+
showAutoPlayToggle = true,
|
|
215
|
+
spacing = 0,
|
|
216
|
+
index,
|
|
217
|
+
defaultIndex = 0,
|
|
218
|
+
onIndexChange,
|
|
219
|
+
height = DEFAULT_HEIGHT,
|
|
220
|
+
swipeEnabled = true,
|
|
221
|
+
slideStyle,
|
|
222
|
+
style,
|
|
223
|
+
testID,
|
|
224
|
+
}: SlidesProps<T>) {
|
|
225
|
+
const colors = useColors();
|
|
226
|
+
const styles = useStyles();
|
|
227
|
+
const allowFontScaling = useAllowFontScaling();
|
|
228
|
+
|
|
229
|
+
const total = data.length;
|
|
230
|
+
const {
|
|
231
|
+
active,
|
|
232
|
+
pages,
|
|
233
|
+
isHorizontal,
|
|
234
|
+
size,
|
|
235
|
+
pageSize,
|
|
236
|
+
scrollRef,
|
|
237
|
+
autoPlayPaused,
|
|
238
|
+
motionReduced,
|
|
239
|
+
goTo,
|
|
240
|
+
toggleAutoPlay,
|
|
241
|
+
onLayout,
|
|
242
|
+
onMomentumScrollEnd,
|
|
243
|
+
onScrollBeginDrag,
|
|
244
|
+
onScrollEndDrag,
|
|
245
|
+
} = useSlides({ total, orientation, loop, autoPlay, interval, index, defaultIndex, onIndexChange });
|
|
246
|
+
|
|
247
|
+
const isOverlay = indicatorPlacement === 'overlay';
|
|
248
|
+
|
|
249
|
+
const renderBackground = (item: T, real: number) => {
|
|
250
|
+
const value = background?.(item, real);
|
|
251
|
+
if (!value) return null;
|
|
252
|
+
if (Array.isArray(value)) {
|
|
253
|
+
return <Gradient colors={value} direction={gradientDirection} style={StyleSheet.absoluteFill} />;
|
|
254
|
+
}
|
|
255
|
+
return <View style={[StyleSheet.absoluteFill, { backgroundColor: value }]} pointerEvents="none" />;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const renderIndicator = (position: number) => {
|
|
259
|
+
const selected = position === active;
|
|
260
|
+
|
|
261
|
+
if (indicators === 'numbers') {
|
|
262
|
+
const numberColors = isOverlay
|
|
263
|
+
? {
|
|
264
|
+
background: selected ? colors.white : 'transparent',
|
|
265
|
+
border: colors.white,
|
|
266
|
+
text: selected ? colors.black : colors.white,
|
|
267
|
+
}
|
|
268
|
+
: {
|
|
269
|
+
background: selected ? colors.primary : 'transparent',
|
|
270
|
+
border: selected ? colors.primary : colors.border,
|
|
271
|
+
text: selected ? colors.onPrimary : colors.textLight,
|
|
272
|
+
};
|
|
273
|
+
return (
|
|
274
|
+
<View
|
|
275
|
+
style={[
|
|
276
|
+
styles.numberCircle,
|
|
277
|
+
{ backgroundColor: numberColors.background, borderColor: numberColors.border },
|
|
278
|
+
]}
|
|
279
|
+
>
|
|
280
|
+
<Text allowFontScaling={allowFontScaling} style={[styles.numberText, { color: numberColors.text }]}>
|
|
281
|
+
{position + 1}
|
|
282
|
+
</Text>
|
|
283
|
+
</View>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<Dot
|
|
289
|
+
testID={`${testID ?? 'slides'}-dot-${position}`}
|
|
290
|
+
selected={selected}
|
|
291
|
+
isHorizontal={isHorizontal}
|
|
292
|
+
overlay={isOverlay}
|
|
293
|
+
activeColor={isOverlay ? colors.white : colors.primary}
|
|
294
|
+
inactiveColor={isOverlay ? colors.white : colors.border}
|
|
295
|
+
/>
|
|
296
|
+
);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Con reduce-motion el autoPlay ya está apagado: no hay nada que pausar.
|
|
300
|
+
const renderAutoPlayToggle = () => {
|
|
301
|
+
if (!autoPlay || !showAutoPlayToggle || motionReduced || total < 2) return null;
|
|
302
|
+
return (
|
|
303
|
+
<TouchableOpacity
|
|
304
|
+
testID={`${testID ?? 'slides'}-autoplay-toggle`}
|
|
305
|
+
style={[styles.autoPlayToggle, { backgroundColor: colors.black, opacity: 0.55 }]}
|
|
306
|
+
activeOpacity={0.7}
|
|
307
|
+
onPress={toggleAutoPlay}
|
|
308
|
+
accessibilityRole="button"
|
|
309
|
+
accessibilityLabel={autoPlayPaused ? 'Resume automatic slides' : 'Pause automatic slides'}
|
|
310
|
+
accessibilityState={{ selected: !autoPlayPaused }}
|
|
311
|
+
>
|
|
312
|
+
{autoPlayPaused ? (
|
|
313
|
+
<View style={[styles.playTriangle, { borderLeftColor: colors.white }]} />
|
|
314
|
+
) : (
|
|
315
|
+
<>
|
|
316
|
+
<View style={[styles.pauseBar, { backgroundColor: colors.white }]} />
|
|
317
|
+
<View style={[styles.pauseBar, { backgroundColor: colors.white }]} />
|
|
318
|
+
</>
|
|
319
|
+
)}
|
|
320
|
+
</TouchableOpacity>
|
|
321
|
+
);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const renderIndicators = () => {
|
|
325
|
+
if (indicators === 'none' || total < 2) return null;
|
|
326
|
+
return (
|
|
327
|
+
<View
|
|
328
|
+
testID={`${testID ?? 'slides'}-indicators`}
|
|
329
|
+
style={[
|
|
330
|
+
isHorizontal ? styles.indicatorsRow : styles.indicatorsColumn,
|
|
331
|
+
isOverlay
|
|
332
|
+
? isHorizontal
|
|
333
|
+
? styles.indicatorsOverlayHorizontal
|
|
334
|
+
: styles.indicatorsOverlayVertical
|
|
335
|
+
: isHorizontal
|
|
336
|
+
? styles.indicatorsOutsideHorizontal
|
|
337
|
+
: styles.indicatorsOutsideVertical,
|
|
338
|
+
]}
|
|
339
|
+
pointerEvents="box-none"
|
|
340
|
+
>
|
|
341
|
+
{data.map((item, position) => (
|
|
342
|
+
<TouchableOpacity
|
|
343
|
+
key={`indicator-${keyExtractor(item, position)}`}
|
|
344
|
+
testID={`${testID ?? 'slides'}-indicator-${position}`}
|
|
345
|
+
style={styles.indicatorSlot}
|
|
346
|
+
activeOpacity={0.7}
|
|
347
|
+
onPress={() => goTo(position)}
|
|
348
|
+
accessibilityRole="button"
|
|
349
|
+
accessibilityLabel={`Slide ${position + 1} of ${total}`}
|
|
350
|
+
accessibilityState={{ selected: position === active }}
|
|
351
|
+
>
|
|
352
|
+
{renderIndicator(position)}
|
|
353
|
+
</TouchableOpacity>
|
|
354
|
+
))}
|
|
355
|
+
</View>
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const viewport = (
|
|
360
|
+
<View
|
|
361
|
+
testID={testID}
|
|
362
|
+
style={[styles.root, { height }, !isOverlay && !isHorizontal && styles.viewportInRow, isOverlay && style]}
|
|
363
|
+
onLayout={onLayout}
|
|
364
|
+
>
|
|
365
|
+
<ScrollView
|
|
366
|
+
ref={scrollRef}
|
|
367
|
+
testID={`${testID ?? 'slides'}-scroll`}
|
|
368
|
+
style={styles.viewport}
|
|
369
|
+
horizontal={isHorizontal}
|
|
370
|
+
pagingEnabled
|
|
371
|
+
scrollEnabled={swipeEnabled && total > 1}
|
|
372
|
+
showsHorizontalScrollIndicator={false}
|
|
373
|
+
showsVerticalScrollIndicator={false}
|
|
374
|
+
onMomentumScrollEnd={onMomentumScrollEnd}
|
|
375
|
+
onScrollBeginDrag={onScrollBeginDrag}
|
|
376
|
+
onScrollEndDrag={onScrollEndDrag}
|
|
377
|
+
>
|
|
378
|
+
{pageSize > 0 &&
|
|
379
|
+
pages.map(({ real, clone }) => {
|
|
380
|
+
const item = data[real] as T;
|
|
381
|
+
return (
|
|
382
|
+
// El hueco se reparte a los dos lados de cada página: así la
|
|
383
|
+
// separación entre dos slides es `spacing` entero y la página
|
|
384
|
+
// sigue midiendo exactamente lo que pagina el ScrollView.
|
|
385
|
+
<View
|
|
386
|
+
key={clone ? `slides-clone-${clone}` : keyExtractor(item, real)}
|
|
387
|
+
style={[
|
|
388
|
+
{ width: size.width, height: size.height },
|
|
389
|
+
isHorizontal ? { paddingHorizontal: spacing / 2 } : { paddingVertical: spacing / 2 },
|
|
390
|
+
]}
|
|
391
|
+
>
|
|
392
|
+
<View style={[styles.slide, slideStyle]}>
|
|
393
|
+
{renderBackground(item, real)}
|
|
394
|
+
<View style={styles.slideContent}>{renderItem(item, real)}</View>
|
|
395
|
+
</View>
|
|
396
|
+
</View>
|
|
397
|
+
);
|
|
398
|
+
})}
|
|
399
|
+
</ScrollView>
|
|
400
|
+
{renderAutoPlayToggle()}
|
|
401
|
+
{isOverlay && renderIndicators()}
|
|
402
|
+
</View>
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
if (isOverlay) return viewport;
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<View style={[isHorizontal ? styles.outsideHorizontal : styles.outsideVertical, style]}>
|
|
409
|
+
{viewport}
|
|
410
|
+
{renderIndicators()}
|
|
411
|
+
</View>
|
|
412
|
+
);
|
|
413
|
+
}
|
package/src/organisms/Toast.tsx
CHANGED
|
@@ -12,10 +12,10 @@ export interface ToastProps {
|
|
|
12
12
|
title: string;
|
|
13
13
|
message?: string;
|
|
14
14
|
variant?: ToastVariant;
|
|
15
|
-
/**
|
|
15
|
+
/** Fires when the toast is tapped (in addition to closing it). */
|
|
16
16
|
onPress?: () => void;
|
|
17
17
|
onDismiss?: () => void;
|
|
18
|
-
/**
|
|
18
|
+
/** Hides the close X. The toast still auto-dismisses by timeout. */
|
|
19
19
|
hideCloseButton?: boolean;
|
|
20
20
|
style?: StyleProp<ViewStyle>;
|
|
21
21
|
testID?: string;
|
|
@@ -54,9 +54,9 @@ export interface ToastProviderProps {
|
|
|
54
54
|
children: React.ReactNode;
|
|
55
55
|
/** @default 'top' */
|
|
56
56
|
position?: 'top' | 'bottom';
|
|
57
|
-
/**
|
|
57
|
+
/** Maximum simultaneous toasts; the oldest ones are dropped. @default 3 */
|
|
58
58
|
maxVisible?: number;
|
|
59
|
-
/**
|
|
59
|
+
/** Default duration in ms. @default 4000 */
|
|
60
60
|
defaultDuration?: number;
|
|
61
61
|
}
|
|
62
62
|
|
package/src/organisms/Wizard.tsx
CHANGED
|
@@ -13,28 +13,28 @@ export interface WizardStep extends StepItem {
|
|
|
13
13
|
|
|
14
14
|
export interface WizardProps {
|
|
15
15
|
steps: WizardStep[];
|
|
16
|
-
/**
|
|
16
|
+
/** Current step (controlled). If omitted, the Wizard manages its own state. */
|
|
17
17
|
current?: number;
|
|
18
18
|
onStepChange?: (index: number) => void;
|
|
19
19
|
onFinish?: () => void;
|
|
20
20
|
/** @default 'horizontal' */
|
|
21
21
|
orientation?: 'horizontal' | 'vertical';
|
|
22
22
|
completedIndicator?: 'check' | 'number';
|
|
23
|
-
/**
|
|
23
|
+
/** Allows returning to an already-completed step by tapping it in the indicator. @default true */
|
|
24
24
|
allowStepNavigation?: boolean;
|
|
25
25
|
backLabel?: string;
|
|
26
26
|
nextLabel?: string;
|
|
27
27
|
finishLabel?: string;
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* VirtualizedList
|
|
29
|
+
* The step content scrolls when it doesn't fit. Set to false only if
|
|
30
|
+
* a step renders its own virtualized list (nesting a
|
|
31
|
+
* VirtualizedList inside a ScrollView breaks virtualization).
|
|
32
32
|
* @default true
|
|
33
33
|
*/
|
|
34
34
|
scrollableContent?: boolean;
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* The step content slides in following the navigation direction.
|
|
37
|
+
* Only opacity and transform, so the animation runs on the native thread.
|
|
38
38
|
* @default true
|
|
39
39
|
*/
|
|
40
40
|
animated?: boolean;
|
package/src/organisms/index.ts
CHANGED
|
@@ -30,3 +30,11 @@ export { NavigationBar } from './NavigationBar';
|
|
|
30
30
|
export type { NavigationBarProps, NavigationBarItem } from './NavigationBar';
|
|
31
31
|
export { ReorderableList } from './ReorderableList';
|
|
32
32
|
export type { ReorderableListProps } from './ReorderableList';
|
|
33
|
+
export { Slides } from './Slides';
|
|
34
|
+
export type {
|
|
35
|
+
SlidesProps,
|
|
36
|
+
SlidesOrientation,
|
|
37
|
+
SlidesIndicators,
|
|
38
|
+
SlidesIndicatorPlacement,
|
|
39
|
+
SlideBackground,
|
|
40
|
+
} from './Slides';
|