orn-ui 0.1.1 → 0.2.1

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.
Files changed (240) hide show
  1. package/README.md +20 -2
  2. package/lib/commonjs/atoms/PressableScale.js +9 -10
  3. package/lib/commonjs/atoms/PressableScale.js.map +1 -1
  4. package/lib/commonjs/atoms/Skeleton.js +96 -0
  5. package/lib/commonjs/atoms/Skeleton.js.map +1 -0
  6. package/lib/commonjs/atoms/Transition.js +171 -0
  7. package/lib/commonjs/atoms/Transition.js.map +1 -0
  8. package/lib/commonjs/atoms/index.js +20 -0
  9. package/lib/commonjs/atoms/index.js.map +1 -1
  10. package/lib/commonjs/molecules/OptionCard.js +55 -22
  11. package/lib/commonjs/molecules/OptionCard.js.map +1 -1
  12. package/lib/commonjs/molecules/{ThemeToggle.js → SegmentedControl.js} +36 -36
  13. package/lib/commonjs/molecules/SegmentedControl.js.map +1 -0
  14. package/lib/commonjs/molecules/Stepper.js +124 -47
  15. package/lib/commonjs/molecules/Stepper.js.map +1 -1
  16. package/lib/commonjs/molecules/Steps.js +36 -6
  17. package/lib/commonjs/molecules/Steps.js.map +1 -1
  18. package/lib/commonjs/molecules/index.js +9 -3
  19. package/lib/commonjs/molecules/index.js.map +1 -1
  20. package/lib/commonjs/organisms/AlertProvider.js +26 -0
  21. package/lib/commonjs/organisms/AlertProvider.js.map +1 -1
  22. package/lib/commonjs/organisms/BottomSheet.js +118 -39
  23. package/lib/commonjs/organisms/BottomSheet.js.map +1 -1
  24. package/lib/commonjs/organisms/DateField.js +46 -15
  25. package/lib/commonjs/organisms/DateField.js.map +1 -1
  26. package/lib/commonjs/organisms/DatePicker.js +129 -50
  27. package/lib/commonjs/organisms/DatePicker.js.map +1 -1
  28. package/lib/commonjs/organisms/List.js +66 -44
  29. package/lib/commonjs/organisms/List.js.map +1 -1
  30. package/lib/commonjs/organisms/Modal.js +42 -87
  31. package/lib/commonjs/organisms/Modal.js.map +1 -1
  32. package/lib/commonjs/organisms/NavigationBar.js +210 -0
  33. package/lib/commonjs/organisms/NavigationBar.js.map +1 -0
  34. package/lib/commonjs/organisms/Screen.js +9 -2
  35. package/lib/commonjs/organisms/Screen.js.map +1 -1
  36. package/lib/commonjs/organisms/SearchList.js +95 -27
  37. package/lib/commonjs/organisms/SearchList.js.map +1 -1
  38. package/lib/commonjs/organisms/Select.js +222 -50
  39. package/lib/commonjs/organisms/Select.js.map +1 -1
  40. package/lib/commonjs/organisms/ThemeToggle.js +52 -0
  41. package/lib/commonjs/organisms/ThemeToggle.js.map +1 -0
  42. package/lib/commonjs/organisms/Toast.js +8 -22
  43. package/lib/commonjs/organisms/Toast.js.map +1 -1
  44. package/lib/commonjs/organisms/ToastProvider.js +24 -0
  45. package/lib/commonjs/organisms/ToastProvider.js.map +1 -1
  46. package/lib/commonjs/organisms/Wizard.js +23 -2
  47. package/lib/commonjs/organisms/Wizard.js.map +1 -1
  48. package/lib/commonjs/organisms/imperativeBridge.js +28 -0
  49. package/lib/commonjs/organisms/imperativeBridge.js.map +1 -0
  50. package/lib/commonjs/organisms/index.js +50 -0
  51. package/lib/commonjs/organisms/index.js.map +1 -1
  52. package/lib/commonjs/theme/colors.js +207 -0
  53. package/lib/commonjs/theme/colors.js.map +1 -0
  54. package/lib/commonjs/theme/createTheme.js +25 -2
  55. package/lib/commonjs/theme/createTheme.js.map +1 -1
  56. package/lib/commonjs/theme/index.js +25 -0
  57. package/lib/commonjs/theme/index.js.map +1 -1
  58. package/lib/commonjs/theme/labels.js +5 -1
  59. package/lib/commonjs/theme/labels.js.map +1 -1
  60. package/lib/commonjs/theme/palettes.js +5 -2
  61. package/lib/commonjs/theme/palettes.js.map +1 -1
  62. package/lib/module/atoms/PressableScale.js +9 -10
  63. package/lib/module/atoms/PressableScale.js.map +1 -1
  64. package/lib/module/atoms/Skeleton.js +91 -0
  65. package/lib/module/atoms/Skeleton.js.map +1 -0
  66. package/lib/module/atoms/Transition.js +165 -0
  67. package/lib/module/atoms/Transition.js.map +1 -0
  68. package/lib/module/atoms/index.js +2 -0
  69. package/lib/module/atoms/index.js.map +1 -1
  70. package/lib/module/molecules/OptionCard.js +56 -23
  71. package/lib/module/molecules/OptionCard.js.map +1 -1
  72. package/lib/module/molecules/{ThemeToggle.js → SegmentedControl.js} +36 -36
  73. package/lib/module/molecules/SegmentedControl.js.map +1 -0
  74. package/lib/module/molecules/Stepper.js +125 -49
  75. package/lib/module/molecules/Stepper.js.map +1 -1
  76. package/lib/module/molecules/Steps.js +37 -7
  77. package/lib/module/molecules/Steps.js.map +1 -1
  78. package/lib/module/molecules/index.js +2 -2
  79. package/lib/module/molecules/index.js.map +1 -1
  80. package/lib/module/organisms/AlertProvider.js +24 -1
  81. package/lib/module/organisms/AlertProvider.js.map +1 -1
  82. package/lib/module/organisms/BottomSheet.js +120 -41
  83. package/lib/module/organisms/BottomSheet.js.map +1 -1
  84. package/lib/module/organisms/DateField.js +49 -18
  85. package/lib/module/organisms/DateField.js.map +1 -1
  86. package/lib/module/organisms/DatePicker.js +131 -52
  87. package/lib/module/organisms/DatePicker.js.map +1 -1
  88. package/lib/module/organisms/List.js +68 -46
  89. package/lib/module/organisms/List.js.map +1 -1
  90. package/lib/module/organisms/Modal.js +44 -89
  91. package/lib/module/organisms/Modal.js.map +1 -1
  92. package/lib/module/organisms/NavigationBar.js +205 -0
  93. package/lib/module/organisms/NavigationBar.js.map +1 -0
  94. package/lib/module/organisms/Screen.js +9 -2
  95. package/lib/module/organisms/Screen.js.map +1 -1
  96. package/lib/module/organisms/SearchList.js +96 -28
  97. package/lib/module/organisms/SearchList.js.map +1 -1
  98. package/lib/module/organisms/Select.js +224 -52
  99. package/lib/module/organisms/Select.js.map +1 -1
  100. package/lib/module/organisms/ThemeToggle.js +47 -0
  101. package/lib/module/organisms/ThemeToggle.js.map +1 -0
  102. package/lib/module/organisms/Toast.js +10 -24
  103. package/lib/module/organisms/Toast.js.map +1 -1
  104. package/lib/module/organisms/ToastProvider.js +21 -0
  105. package/lib/module/organisms/ToastProvider.js.map +1 -1
  106. package/lib/module/organisms/Wizard.js +24 -3
  107. package/lib/module/organisms/Wizard.js.map +1 -1
  108. package/lib/module/organisms/imperativeBridge.js +24 -0
  109. package/lib/module/organisms/imperativeBridge.js.map +1 -0
  110. package/lib/module/organisms/index.js +4 -2
  111. package/lib/module/organisms/index.js.map +1 -1
  112. package/lib/module/theme/colors.js +194 -0
  113. package/lib/module/theme/colors.js.map +1 -0
  114. package/lib/module/theme/createTheme.js +25 -2
  115. package/lib/module/theme/createTheme.js.map +1 -1
  116. package/lib/module/theme/index.js +1 -0
  117. package/lib/module/theme/index.js.map +1 -1
  118. package/lib/module/theme/labels.js +5 -1
  119. package/lib/module/theme/labels.js.map +1 -1
  120. package/lib/module/theme/palettes.js +5 -2
  121. package/lib/module/theme/palettes.js.map +1 -1
  122. package/lib/typescript/atoms/PressableScale.d.ts +3 -2
  123. package/lib/typescript/atoms/PressableScale.d.ts.map +1 -1
  124. package/lib/typescript/atoms/Skeleton.d.ts +29 -0
  125. package/lib/typescript/atoms/Skeleton.d.ts.map +1 -0
  126. package/lib/typescript/atoms/Transition.d.ts +51 -0
  127. package/lib/typescript/atoms/Transition.d.ts.map +1 -0
  128. package/lib/typescript/atoms/index.d.ts +4 -0
  129. package/lib/typescript/atoms/index.d.ts.map +1 -1
  130. package/lib/typescript/molecules/OptionCard.d.ts +11 -6
  131. package/lib/typescript/molecules/OptionCard.d.ts.map +1 -1
  132. package/lib/typescript/molecules/SegmentedControl.d.ts +24 -0
  133. package/lib/typescript/molecules/SegmentedControl.d.ts.map +1 -0
  134. package/lib/typescript/molecules/Stepper.d.ts +35 -3
  135. package/lib/typescript/molecules/Stepper.d.ts.map +1 -1
  136. package/lib/typescript/molecules/Steps.d.ts.map +1 -1
  137. package/lib/typescript/molecules/index.d.ts +4 -4
  138. package/lib/typescript/molecules/index.d.ts.map +1 -1
  139. package/lib/typescript/organisms/AlertProvider.d.ts +9 -0
  140. package/lib/typescript/organisms/AlertProvider.d.ts.map +1 -1
  141. package/lib/typescript/organisms/BottomSheet.d.ts +26 -4
  142. package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -1
  143. package/lib/typescript/organisms/DateField.d.ts +23 -5
  144. package/lib/typescript/organisms/DateField.d.ts.map +1 -1
  145. package/lib/typescript/organisms/DatePicker.d.ts +5 -1
  146. package/lib/typescript/organisms/DatePicker.d.ts.map +1 -1
  147. package/lib/typescript/organisms/List.d.ts +9 -2
  148. package/lib/typescript/organisms/List.d.ts.map +1 -1
  149. package/lib/typescript/organisms/Modal.d.ts +3 -5
  150. package/lib/typescript/organisms/Modal.d.ts.map +1 -1
  151. package/lib/typescript/organisms/NavigationBar.d.ts +32 -0
  152. package/lib/typescript/organisms/NavigationBar.d.ts.map +1 -0
  153. package/lib/typescript/organisms/Screen.d.ts +2 -1
  154. package/lib/typescript/organisms/Screen.d.ts.map +1 -1
  155. package/lib/typescript/organisms/SearchList.d.ts +21 -5
  156. package/lib/typescript/organisms/SearchList.d.ts.map +1 -1
  157. package/lib/typescript/organisms/Select.d.ts +19 -4
  158. package/lib/typescript/organisms/Select.d.ts.map +1 -1
  159. package/lib/typescript/{molecules → organisms}/ThemeToggle.d.ts +4 -4
  160. package/lib/typescript/organisms/ThemeToggle.d.ts.map +1 -0
  161. package/lib/typescript/organisms/Toast.d.ts.map +1 -1
  162. package/lib/typescript/organisms/ToastProvider.d.ts +8 -0
  163. package/lib/typescript/organisms/ToastProvider.d.ts.map +1 -1
  164. package/lib/typescript/organisms/Wizard.d.ts +7 -1
  165. package/lib/typescript/organisms/Wizard.d.ts.map +1 -1
  166. package/lib/typescript/organisms/imperativeBridge.d.ts +12 -0
  167. package/lib/typescript/organisms/imperativeBridge.d.ts.map +1 -0
  168. package/lib/typescript/organisms/index.d.ts +7 -3
  169. package/lib/typescript/organisms/index.d.ts.map +1 -1
  170. package/lib/typescript/theme/colors.d.ts +44 -0
  171. package/lib/typescript/theme/colors.d.ts.map +1 -0
  172. package/lib/typescript/theme/createTheme.d.ts +3 -0
  173. package/lib/typescript/theme/createTheme.d.ts.map +1 -1
  174. package/lib/typescript/theme/index.d.ts +3 -1
  175. package/lib/typescript/theme/index.d.ts.map +1 -1
  176. package/lib/typescript/theme/labels.d.ts +6 -0
  177. package/lib/typescript/theme/labels.d.ts.map +1 -1
  178. package/lib/typescript/theme/palettes.d.ts.map +1 -1
  179. package/lib/typescript/theme/types.d.ts +9 -0
  180. package/lib/typescript/theme/types.d.ts.map +1 -1
  181. package/package.json +67 -6
  182. package/registry/alert-provider.json +9 -2
  183. package/registry/bottom-sheet.json +6 -2
  184. package/registry/core.json +9 -5
  185. package/registry/date-field.json +2 -1
  186. package/registry/date-picker.json +1 -1
  187. package/registry/index.json +99 -30
  188. package/registry/list.json +2 -1
  189. package/registry/manifest.json +44 -6
  190. package/registry/modal.json +3 -2
  191. package/registry/navigation-bar.json +18 -0
  192. package/registry/option-card.json +3 -2
  193. package/registry/pressable-scale.json +1 -1
  194. package/registry/screen.json +1 -1
  195. package/registry/search-list.json +2 -1
  196. package/registry/segmented-control.json +18 -0
  197. package/registry/select.json +1 -1
  198. package/registry/skeleton.json +18 -0
  199. package/registry/stepper.json +3 -2
  200. package/registry/steps.json +1 -1
  201. package/registry/theme-toggle.json +5 -4
  202. package/registry/toast-provider.json +11 -3
  203. package/registry/toast.json +3 -2
  204. package/registry/transition.json +19 -0
  205. package/registry/wizard.json +3 -2
  206. package/src/atoms/PressableScale.tsx +16 -6
  207. package/src/atoms/Skeleton.tsx +114 -0
  208. package/src/atoms/Transition.tsx +189 -0
  209. package/src/atoms/index.ts +4 -0
  210. package/src/molecules/OptionCard.tsx +62 -25
  211. package/src/molecules/SegmentedControl.tsx +84 -0
  212. package/src/molecules/Stepper.tsx +147 -32
  213. package/src/molecules/Steps.tsx +27 -8
  214. package/src/molecules/index.ts +4 -4
  215. package/src/organisms/AlertProvider.tsx +26 -1
  216. package/src/organisms/BottomSheet.tsx +214 -62
  217. package/src/organisms/DateField.tsx +79 -19
  218. package/src/organisms/DatePicker.tsx +198 -80
  219. package/src/organisms/List.tsx +73 -41
  220. package/src/organisms/Modal.tsx +49 -61
  221. package/src/organisms/NavigationBar.tsx +226 -0
  222. package/src/organisms/Screen.tsx +11 -3
  223. package/src/organisms/SearchList.tsx +131 -35
  224. package/src/organisms/Select.tsx +223 -45
  225. package/src/organisms/ThemeToggle.tsx +30 -0
  226. package/src/organisms/Toast.tsx +8 -21
  227. package/src/organisms/ToastProvider.tsx +24 -0
  228. package/src/organisms/Wizard.tsx +37 -3
  229. package/src/organisms/imperativeBridge.ts +28 -0
  230. package/src/organisms/index.ts +7 -3
  231. package/src/theme/colors.ts +175 -0
  232. package/src/theme/createTheme.ts +38 -4
  233. package/src/theme/index.ts +3 -1
  234. package/src/theme/labels.ts +13 -0
  235. package/src/theme/palettes.ts +5 -2
  236. package/src/theme/types.ts +10 -0
  237. package/lib/commonjs/molecules/ThemeToggle.js.map +0 -1
  238. package/lib/module/molecules/ThemeToggle.js.map +0 -1
  239. package/lib/typescript/molecules/ThemeToggle.d.ts.map +0 -1
  240. package/src/molecules/ThemeToggle.tsx +0 -67
@@ -1,16 +1,52 @@
1
1
  import React, { memo, useEffect, useRef, useState } from 'react';
2
- import { Animated, Dimensions, Modal, PanResponder, Pressable, View, type ViewStyle } from 'react-native';
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
- * Modal que aparece desde abajo: animación con Animated (no la del Modal
35
- * nativo) y arrastre para cerrar con PanResponder — no un gesture-handler
36
- * externo, para mantener la librería en cero dependencias.
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(({ visible, onClose, children, closeAccessibilityLabel = 'Close' }: BottomSheetProps) => {
39
- const styles = useStyles();
40
- const insets = useInsets();
41
- const [mounted, setMounted] = useState(visible);
42
- const translateY = useRef(new Animated.Value(SCREEN_HEIGHT)).current;
43
- const backdropOpacity = useRef(new Animated.Value(0)).current;
44
-
45
- useEffect(() => {
46
- if (visible) {
47
- setMounted(true);
48
- Animated.timing(translateY, { toValue: 0, duration: 250, useNativeDriver: true }).start();
49
- Animated.timing(backdropOpacity, { toValue: 1, duration: 250, useNativeDriver: true }).start();
50
- } else {
51
- Animated.timing(translateY, { toValue: SCREEN_HEIGHT, duration: 200, useNativeDriver: true }).start(({ finished }) => {
52
- if (finished) setMounted(false);
53
- });
54
- Animated.timing(backdropOpacity, { toValue: 0, duration: 200, useNativeDriver: true }).start();
55
- }
56
- }, [visible]);
57
-
58
- const panResponder = useRef(
59
- PanResponder.create({
60
- onMoveShouldSetPanResponder: (_, gesture) => gesture.dy > 5 && Math.abs(gesture.dy) > Math.abs(gesture.dx),
61
- onPanResponderMove: (_, gesture) => {
62
- if (gesture.dy > 0) translateY.setValue(gesture.dy);
63
- },
64
- onPanResponderRelease: (_, gesture) => {
65
- if (gesture.dy > DISMISS_THRESHOLD) {
66
- onClose();
67
- } else {
68
- Animated.timing(translateY, { toValue: 0, duration: 150, useNativeDriver: true }).start();
69
- }
70
- },
71
- })
72
- ).current;
73
-
74
- if (!mounted) return null;
75
-
76
- return (
77
- <Modal visible transparent animationType="none" statusBarTranslucent onRequestClose={onClose}>
78
- <View style={styles.container}>
79
- <Animated.View style={[absoluteFill, styles.backdrop, { opacity: backdropOpacity }]}>
80
- <Pressable style={absoluteFill} onPress={onClose} accessibilityRole="button" accessibilityLabel={closeAccessibilityLabel} />
81
- </Animated.View>
82
-
83
- <Animated.View
84
- style={[styles.sheet, { paddingBottom: Math.max(insets.bottom, 20) }, { transform: [{ translateY }] }]}
85
- >
86
- <View style={styles.handleArea} {...panResponder.panHandlers}>
87
- <View style={styles.handle} />
88
- </View>
89
- {children}
90
- </Animated.View>
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
- </Modal>
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';
@@ -1,26 +1,44 @@
1
- import React, { memo, useState } from 'react';
1
+ import React, { memo, useCallback, useState } from 'react';
2
2
  import { Text, TouchableOpacity, View, type StyleProp, type ViewStyle } from 'react-native';
3
3
  import { createStyles } from '../theme/createStyles';
4
- import { useAllowFontScaling, useColors } from '../theme/UIProvider';
4
+ import { useAllowFontScaling, useColors, useLabels } from '../theme/UIProvider';
5
5
  import { Modal } from './Modal';
6
- import { DatePicker, DEFAULT_MONTH_NAMES, type DatePickerProps } from './DatePicker';
6
+ import { DatePicker, type DatePickerProps, type DateRange } from './DatePicker';
7
7
  import { Button } from '../atoms/Button';
8
8
 
9
9
  export interface DateFieldProps
10
10
  extends Pick<DatePickerProps, 'minDate' | 'maxDate' | 'monthNames' | 'weekdayNames' | 'firstDayOfWeek'> {
11
11
  label?: string;
12
12
  required?: boolean;
13
+ /**
14
+ * 'single' elige una fecha; 'range' elige un intervalo con dos toques.
15
+ * @default 'single'
16
+ */
17
+ mode?: 'single' | 'range';
18
+ /** Fecha elegida en mode="single". */
13
19
  value?: Date;
14
- onChange: (date: Date) => void;
20
+ /** Se llama en mode="single". */
21
+ onChange?: (date: Date) => void;
22
+ /** Rango elegido en mode="range". */
23
+ range?: DateRange;
24
+ /** Se llama en mode="range", tanto al fijar el inicio como al cerrar el rango. */
25
+ onRangeChange?: (range: DateRange) => void;
15
26
  placeholder?: string;
16
27
  error?: string;
17
28
  disabled?: boolean;
18
- /** Formatea la fecha en el campo. @default "D de Month de YYYY" en inglés */
29
+ /** Formatea la fecha en el campo. @default "Month D, YYYY" */
19
30
  format?: (date: Date) => string;
20
- /** Título del modal del calendario. @default 'Select date' */
31
+ /** Título del modal del calendario. @default el `selectDate` de `useLabels()` */
21
32
  modalTitle?: string;
33
+ /**
34
+ * Si se pasa, el modal muestra un botón que borra la selección y cierra.
35
+ * Sin esto no hay botón: el "Clear" anterior sólo cerraba, sin limpiar nada.
36
+ */
37
+ onClear?: () => void;
38
+ /** Texto de ese botón. @default el `clear` de `useLabels()` */
22
39
  clearLabel?: string;
23
40
  containerStyle?: StyleProp<ViewStyle>;
41
+ testID?: string;
24
42
  }
25
43
 
26
44
  function defaultFormat(date: Date, monthNames: string[]): string {
@@ -58,27 +76,67 @@ export const DateField = memo(
58
76
  ({
59
77
  label,
60
78
  required = false,
79
+ mode = 'single',
61
80
  value,
62
81
  onChange,
82
+ range,
83
+ onRangeChange,
63
84
  placeholder = 'Select a date',
64
85
  error,
65
86
  disabled = false,
66
87
  format,
67
- modalTitle = 'Select date',
68
- clearLabel = 'Clear',
88
+ modalTitle,
89
+ onClear,
90
+ clearLabel,
69
91
  containerStyle,
70
- monthNames = DEFAULT_MONTH_NAMES,
92
+ monthNames,
93
+ testID,
71
94
  ...pickerProps
72
95
  }: DateFieldProps) => {
73
96
  const colors = useColors();
74
97
  const styles = useStyles();
98
+ const labels = useLabels();
75
99
  const allowFontScaling = useAllowFontScaling();
76
100
  const [open, setOpen] = useState(false);
77
101
 
78
- const display = value ? (format ? format(value) : defaultFormat(value, monthNames)) : placeholder;
102
+ const months = monthNames ?? labels.months;
103
+ const isRange = mode === 'range';
104
+
105
+ const formatDate = (date: Date) => (format ? format(date) : defaultFormat(date, months));
106
+
107
+ // El calendario se cierra solo cuando ya no falta nada por elegir: en
108
+ // rango, recién cuando el segundo toque cierra el intervalo.
109
+ const handleChange = useCallback(
110
+ (date: Date) => {
111
+ onChange?.(date);
112
+ setOpen(false);
113
+ },
114
+ [onChange]
115
+ );
116
+
117
+ const handleRangeChange = useCallback(
118
+ (next: DateRange) => {
119
+ onRangeChange?.(next);
120
+ if (next.end) setOpen(false);
121
+ },
122
+ [onRangeChange]
123
+ );
124
+
125
+ const handleClear = useCallback(() => {
126
+ onClear?.();
127
+ setOpen(false);
128
+ }, [onClear]);
129
+
130
+ const hasValue = isRange ? !!range?.start : !!value;
131
+ let display = placeholder;
132
+ if (isRange && range?.start) {
133
+ display = range.end ? `${formatDate(range.start)} — ${formatDate(range.end)}` : formatDate(range.start);
134
+ } else if (!isRange && value) {
135
+ display = formatDate(value);
136
+ }
79
137
 
80
138
  return (
81
- <View style={[styles.container, containerStyle]}>
139
+ <View style={[styles.container, containerStyle]} testID={testID}>
82
140
  {!!label && (
83
141
  <Text allowFontScaling={allowFontScaling} style={styles.label}>
84
142
  {label}
@@ -95,7 +153,7 @@ export const DateField = memo(
95
153
  accessibilityLabel={label ?? placeholder}
96
154
  accessibilityState={{ disabled, expanded: open }}
97
155
  >
98
- <Text allowFontScaling={allowFontScaling} style={[styles.fieldText, !value && styles.placeholder]}>
156
+ <Text allowFontScaling={allowFontScaling} style={[styles.fieldText, !hasValue && styles.placeholder]}>
99
157
  {display}
100
158
  </Text>
101
159
  <Text allowFontScaling={allowFontScaling} style={{ color: colors.textLight }}>
@@ -112,19 +170,21 @@ export const DateField = memo(
112
170
  <Modal
113
171
  visible={open}
114
172
  onClose={() => setOpen(false)}
115
- title={modalTitle}
173
+ title={modalTitle ?? labels.selectDate}
116
174
  variant="overlay"
117
175
  scrollable={false}
118
- footer={<Button title={clearLabel} variant="ghost" onPress={() => setOpen(false)} />}
176
+ footer={
177
+ onClear ? <Button title={clearLabel ?? labels.clear} variant="ghost" onPress={handleClear} /> : undefined
178
+ }
119
179
  >
120
180
  <DatePicker
181
+ mode={mode}
121
182
  value={value}
122
- monthNames={monthNames}
183
+ range={range}
184
+ monthNames={months}
123
185
  {...pickerProps}
124
- onChange={(date) => {
125
- onChange(date);
126
- setOpen(false);
127
- }}
186
+ onChange={handleChange}
187
+ onRangeChange={handleRangeChange}
128
188
  />
129
189
  </Modal>
130
190
  </View>