orn-ui 0.1.0 → 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.
Files changed (307) hide show
  1. package/README.md +9 -2
  2. package/lib/commonjs/atoms/Avatar.js +1 -1
  3. package/lib/commonjs/atoms/Avatar.js.map +1 -1
  4. package/lib/commonjs/atoms/Badge.js +10 -8
  5. package/lib/commonjs/atoms/Badge.js.map +1 -1
  6. package/lib/commonjs/atoms/Button.js +8 -6
  7. package/lib/commonjs/atoms/Button.js.map +1 -1
  8. package/lib/commonjs/atoms/Checkbox.js +4 -1
  9. package/lib/commonjs/atoms/Checkbox.js.map +1 -1
  10. package/lib/commonjs/atoms/Fab.js +1 -1
  11. package/lib/commonjs/atoms/Fab.js.map +1 -1
  12. package/lib/commonjs/atoms/Image.js +2 -2
  13. package/lib/commonjs/atoms/Image.js.map +1 -1
  14. package/lib/commonjs/atoms/Input.js +2 -2
  15. package/lib/commonjs/atoms/Input.js.map +1 -1
  16. package/lib/commonjs/atoms/PressableScale.js +9 -10
  17. package/lib/commonjs/atoms/PressableScale.js.map +1 -1
  18. package/lib/commonjs/atoms/Skeleton.js +96 -0
  19. package/lib/commonjs/atoms/Skeleton.js.map +1 -0
  20. package/lib/commonjs/atoms/Transition.js +171 -0
  21. package/lib/commonjs/atoms/Transition.js.map +1 -0
  22. package/lib/commonjs/atoms/index.js +20 -0
  23. package/lib/commonjs/atoms/index.js.map +1 -1
  24. package/lib/commonjs/molecules/AvatarHeader.js +2 -2
  25. package/lib/commonjs/molecules/AvatarHeader.js.map +1 -1
  26. package/lib/commonjs/molecules/OptionCard.js +57 -24
  27. package/lib/commonjs/molecules/OptionCard.js.map +1 -1
  28. package/lib/commonjs/molecules/{ThemeToggle.js → SegmentedControl.js} +37 -37
  29. package/lib/commonjs/molecules/SegmentedControl.js.map +1 -0
  30. package/lib/commonjs/molecules/Stepper.js +124 -47
  31. package/lib/commonjs/molecules/Stepper.js.map +1 -1
  32. package/lib/commonjs/molecules/Steps.js +2 -2
  33. package/lib/commonjs/molecules/Steps.js.map +1 -1
  34. package/lib/commonjs/molecules/index.js +9 -3
  35. package/lib/commonjs/molecules/index.js.map +1 -1
  36. package/lib/commonjs/organisms/Alert.js +6 -5
  37. package/lib/commonjs/organisms/Alert.js.map +1 -1
  38. package/lib/commonjs/organisms/AlertProvider.js +26 -0
  39. package/lib/commonjs/organisms/AlertProvider.js.map +1 -1
  40. package/lib/commonjs/organisms/BottomSheet.js +118 -39
  41. package/lib/commonjs/organisms/BottomSheet.js.map +1 -1
  42. package/lib/commonjs/organisms/DateField.js +2 -2
  43. package/lib/commonjs/organisms/DateField.js.map +1 -1
  44. package/lib/commonjs/organisms/DatePicker.js +3 -3
  45. package/lib/commonjs/organisms/DatePicker.js.map +1 -1
  46. package/lib/commonjs/organisms/List.js +66 -44
  47. package/lib/commonjs/organisms/List.js.map +1 -1
  48. package/lib/commonjs/organisms/Modal.js +42 -87
  49. package/lib/commonjs/organisms/Modal.js.map +1 -1
  50. package/lib/commonjs/organisms/NavigationBar.js +205 -0
  51. package/lib/commonjs/organisms/NavigationBar.js.map +1 -0
  52. package/lib/commonjs/organisms/Screen.js +9 -2
  53. package/lib/commonjs/organisms/Screen.js.map +1 -1
  54. package/lib/commonjs/organisms/SearchList.js +99 -30
  55. package/lib/commonjs/organisms/SearchList.js.map +1 -1
  56. package/lib/commonjs/organisms/Select.js +226 -54
  57. package/lib/commonjs/organisms/Select.js.map +1 -1
  58. package/lib/commonjs/organisms/ThemeToggle.js +52 -0
  59. package/lib/commonjs/organisms/ThemeToggle.js.map +1 -0
  60. package/lib/commonjs/organisms/Toast.js +20 -25
  61. package/lib/commonjs/organisms/Toast.js.map +1 -1
  62. package/lib/commonjs/organisms/ToastProvider.js +24 -0
  63. package/lib/commonjs/organisms/ToastProvider.js.map +1 -1
  64. package/lib/commonjs/organisms/Wizard.js +22 -2
  65. package/lib/commonjs/organisms/Wizard.js.map +1 -1
  66. package/lib/commonjs/organisms/imperativeBridge.js +28 -0
  67. package/lib/commonjs/organisms/imperativeBridge.js.map +1 -0
  68. package/lib/commonjs/organisms/index.js +50 -0
  69. package/lib/commonjs/organisms/index.js.map +1 -1
  70. package/lib/commonjs/safe-area/SafeAreaUIProvider.js +65 -0
  71. package/lib/commonjs/safe-area/SafeAreaUIProvider.js.map +1 -0
  72. package/lib/commonjs/safe-area/index.js +13 -0
  73. package/lib/commonjs/safe-area/index.js.map +1 -0
  74. package/lib/commonjs/theme/UIProvider.js +11 -0
  75. package/lib/commonjs/theme/UIProvider.js.map +1 -1
  76. package/lib/commonjs/theme/colors.js +207 -0
  77. package/lib/commonjs/theme/colors.js.map +1 -0
  78. package/lib/commonjs/theme/createTheme.js +25 -2
  79. package/lib/commonjs/theme/createTheme.js.map +1 -1
  80. package/lib/commonjs/theme/index.js +25 -0
  81. package/lib/commonjs/theme/index.js.map +1 -1
  82. package/lib/commonjs/theme/palettes.js +73 -11
  83. package/lib/commonjs/theme/palettes.js.map +1 -1
  84. package/lib/module/atoms/Avatar.js +1 -1
  85. package/lib/module/atoms/Avatar.js.map +1 -1
  86. package/lib/module/atoms/Badge.js +10 -8
  87. package/lib/module/atoms/Badge.js.map +1 -1
  88. package/lib/module/atoms/Button.js +8 -6
  89. package/lib/module/atoms/Button.js.map +1 -1
  90. package/lib/module/atoms/Checkbox.js +4 -1
  91. package/lib/module/atoms/Checkbox.js.map +1 -1
  92. package/lib/module/atoms/Fab.js +1 -1
  93. package/lib/module/atoms/Fab.js.map +1 -1
  94. package/lib/module/atoms/Image.js +2 -2
  95. package/lib/module/atoms/Image.js.map +1 -1
  96. package/lib/module/atoms/Input.js +2 -2
  97. package/lib/module/atoms/Input.js.map +1 -1
  98. package/lib/module/atoms/PressableScale.js +9 -10
  99. package/lib/module/atoms/PressableScale.js.map +1 -1
  100. package/lib/module/atoms/Skeleton.js +91 -0
  101. package/lib/module/atoms/Skeleton.js.map +1 -0
  102. package/lib/module/atoms/Transition.js +165 -0
  103. package/lib/module/atoms/Transition.js.map +1 -0
  104. package/lib/module/atoms/index.js +2 -0
  105. package/lib/module/atoms/index.js.map +1 -1
  106. package/lib/module/molecules/AvatarHeader.js +2 -2
  107. package/lib/module/molecules/AvatarHeader.js.map +1 -1
  108. package/lib/module/molecules/OptionCard.js +58 -25
  109. package/lib/module/molecules/OptionCard.js.map +1 -1
  110. package/lib/module/molecules/{ThemeToggle.js → SegmentedControl.js} +37 -37
  111. package/lib/module/molecules/SegmentedControl.js.map +1 -0
  112. package/lib/module/molecules/Stepper.js +125 -49
  113. package/lib/module/molecules/Stepper.js.map +1 -1
  114. package/lib/module/molecules/Steps.js +2 -2
  115. package/lib/module/molecules/Steps.js.map +1 -1
  116. package/lib/module/molecules/index.js +2 -2
  117. package/lib/module/molecules/index.js.map +1 -1
  118. package/lib/module/organisms/Alert.js +6 -5
  119. package/lib/module/organisms/Alert.js.map +1 -1
  120. package/lib/module/organisms/AlertProvider.js +24 -1
  121. package/lib/module/organisms/AlertProvider.js.map +1 -1
  122. package/lib/module/organisms/BottomSheet.js +120 -41
  123. package/lib/module/organisms/BottomSheet.js.map +1 -1
  124. package/lib/module/organisms/DateField.js +2 -2
  125. package/lib/module/organisms/DateField.js.map +1 -1
  126. package/lib/module/organisms/DatePicker.js +3 -3
  127. package/lib/module/organisms/DatePicker.js.map +1 -1
  128. package/lib/module/organisms/List.js +68 -46
  129. package/lib/module/organisms/List.js.map +1 -1
  130. package/lib/module/organisms/Modal.js +44 -89
  131. package/lib/module/organisms/Modal.js.map +1 -1
  132. package/lib/module/organisms/NavigationBar.js +200 -0
  133. package/lib/module/organisms/NavigationBar.js.map +1 -0
  134. package/lib/module/organisms/Screen.js +9 -2
  135. package/lib/module/organisms/Screen.js.map +1 -1
  136. package/lib/module/organisms/SearchList.js +100 -31
  137. package/lib/module/organisms/SearchList.js.map +1 -1
  138. package/lib/module/organisms/Select.js +228 -56
  139. package/lib/module/organisms/Select.js.map +1 -1
  140. package/lib/module/organisms/ThemeToggle.js +47 -0
  141. package/lib/module/organisms/ThemeToggle.js.map +1 -0
  142. package/lib/module/organisms/Toast.js +22 -27
  143. package/lib/module/organisms/Toast.js.map +1 -1
  144. package/lib/module/organisms/ToastProvider.js +21 -0
  145. package/lib/module/organisms/ToastProvider.js.map +1 -1
  146. package/lib/module/organisms/Wizard.js +23 -3
  147. package/lib/module/organisms/Wizard.js.map +1 -1
  148. package/lib/module/organisms/imperativeBridge.js +24 -0
  149. package/lib/module/organisms/imperativeBridge.js.map +1 -0
  150. package/lib/module/organisms/index.js +4 -2
  151. package/lib/module/organisms/index.js.map +1 -1
  152. package/lib/module/safe-area/SafeAreaUIProvider.js +60 -0
  153. package/lib/module/safe-area/SafeAreaUIProvider.js.map +1 -0
  154. package/lib/module/safe-area/index.js +4 -0
  155. package/lib/module/safe-area/index.js.map +1 -0
  156. package/lib/module/theme/UIProvider.js +11 -0
  157. package/lib/module/theme/UIProvider.js.map +1 -1
  158. package/lib/module/theme/colors.js +194 -0
  159. package/lib/module/theme/colors.js.map +1 -0
  160. package/lib/module/theme/createTheme.js +25 -2
  161. package/lib/module/theme/createTheme.js.map +1 -1
  162. package/lib/module/theme/index.js +1 -0
  163. package/lib/module/theme/index.js.map +1 -1
  164. package/lib/module/theme/palettes.js +73 -11
  165. package/lib/module/theme/palettes.js.map +1 -1
  166. package/lib/typescript/atoms/Badge.d.ts.map +1 -1
  167. package/lib/typescript/atoms/Button.d.ts.map +1 -1
  168. package/lib/typescript/atoms/Checkbox.d.ts.map +1 -1
  169. package/lib/typescript/atoms/PressableScale.d.ts +3 -2
  170. package/lib/typescript/atoms/PressableScale.d.ts.map +1 -1
  171. package/lib/typescript/atoms/Skeleton.d.ts +29 -0
  172. package/lib/typescript/atoms/Skeleton.d.ts.map +1 -0
  173. package/lib/typescript/atoms/Transition.d.ts +51 -0
  174. package/lib/typescript/atoms/Transition.d.ts.map +1 -0
  175. package/lib/typescript/atoms/index.d.ts +4 -0
  176. package/lib/typescript/atoms/index.d.ts.map +1 -1
  177. package/lib/typescript/molecules/OptionCard.d.ts +11 -6
  178. package/lib/typescript/molecules/OptionCard.d.ts.map +1 -1
  179. package/lib/typescript/molecules/SegmentedControl.d.ts +24 -0
  180. package/lib/typescript/molecules/SegmentedControl.d.ts.map +1 -0
  181. package/lib/typescript/molecules/Stepper.d.ts +35 -3
  182. package/lib/typescript/molecules/Stepper.d.ts.map +1 -1
  183. package/lib/typescript/molecules/index.d.ts +4 -4
  184. package/lib/typescript/molecules/index.d.ts.map +1 -1
  185. package/lib/typescript/organisms/Alert.d.ts.map +1 -1
  186. package/lib/typescript/organisms/AlertProvider.d.ts +9 -0
  187. package/lib/typescript/organisms/AlertProvider.d.ts.map +1 -1
  188. package/lib/typescript/organisms/BottomSheet.d.ts +26 -4
  189. package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -1
  190. package/lib/typescript/organisms/List.d.ts +9 -2
  191. package/lib/typescript/organisms/List.d.ts.map +1 -1
  192. package/lib/typescript/organisms/Modal.d.ts +3 -5
  193. package/lib/typescript/organisms/Modal.d.ts.map +1 -1
  194. package/lib/typescript/organisms/NavigationBar.d.ts +32 -0
  195. package/lib/typescript/organisms/NavigationBar.d.ts.map +1 -0
  196. package/lib/typescript/organisms/Screen.d.ts +2 -1
  197. package/lib/typescript/organisms/Screen.d.ts.map +1 -1
  198. package/lib/typescript/organisms/SearchList.d.ts +21 -5
  199. package/lib/typescript/organisms/SearchList.d.ts.map +1 -1
  200. package/lib/typescript/organisms/Select.d.ts +19 -4
  201. package/lib/typescript/organisms/Select.d.ts.map +1 -1
  202. package/lib/typescript/{molecules → organisms}/ThemeToggle.d.ts +4 -4
  203. package/lib/typescript/organisms/ThemeToggle.d.ts.map +1 -0
  204. package/lib/typescript/organisms/Toast.d.ts.map +1 -1
  205. package/lib/typescript/organisms/ToastProvider.d.ts +8 -0
  206. package/lib/typescript/organisms/ToastProvider.d.ts.map +1 -1
  207. package/lib/typescript/organisms/Wizard.d.ts +7 -1
  208. package/lib/typescript/organisms/Wizard.d.ts.map +1 -1
  209. package/lib/typescript/organisms/imperativeBridge.d.ts +12 -0
  210. package/lib/typescript/organisms/imperativeBridge.d.ts.map +1 -0
  211. package/lib/typescript/organisms/index.d.ts +7 -3
  212. package/lib/typescript/organisms/index.d.ts.map +1 -1
  213. package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts +46 -0
  214. package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts.map +1 -0
  215. package/lib/typescript/safe-area/index.d.ts +3 -0
  216. package/lib/typescript/safe-area/index.d.ts.map +1 -0
  217. package/lib/typescript/theme/UIProvider.d.ts.map +1 -1
  218. package/lib/typescript/theme/colors.d.ts +44 -0
  219. package/lib/typescript/theme/colors.d.ts.map +1 -0
  220. package/lib/typescript/theme/createTheme.d.ts +3 -0
  221. package/lib/typescript/theme/createTheme.d.ts.map +1 -1
  222. package/lib/typescript/theme/index.d.ts +3 -1
  223. package/lib/typescript/theme/index.d.ts.map +1 -1
  224. package/lib/typescript/theme/palettes.d.ts +41 -3
  225. package/lib/typescript/theme/palettes.d.ts.map +1 -1
  226. package/lib/typescript/theme/types.d.ts +9 -0
  227. package/lib/typescript/theme/types.d.ts.map +1 -1
  228. package/package.json +101 -18
  229. package/registry/alert-provider.json +9 -2
  230. package/registry/alert.json +1 -1
  231. package/registry/avatar-header.json +1 -1
  232. package/registry/avatar.json +1 -1
  233. package/registry/badge.json +1 -1
  234. package/registry/bottom-sheet.json +6 -2
  235. package/registry/button.json +1 -1
  236. package/registry/checkbox.json +1 -1
  237. package/registry/core.json +9 -5
  238. package/registry/date-field.json +2 -1
  239. package/registry/date-picker.json +1 -1
  240. package/registry/fab.json +1 -1
  241. package/registry/image.json +1 -1
  242. package/registry/index.json +108 -39
  243. package/registry/input.json +1 -1
  244. package/registry/list.json +2 -1
  245. package/registry/manifest.json +44 -6
  246. package/registry/modal.json +3 -2
  247. package/registry/navigation-bar.json +18 -0
  248. package/registry/option-card.json +3 -2
  249. package/registry/pressable-scale.json +1 -1
  250. package/registry/screen.json +1 -1
  251. package/registry/search-list.json +2 -1
  252. package/registry/segmented-control.json +18 -0
  253. package/registry/select.json +1 -1
  254. package/registry/skeleton.json +18 -0
  255. package/registry/stepper.json +3 -2
  256. package/registry/steps.json +1 -1
  257. package/registry/theme-toggle.json +5 -4
  258. package/registry/toast-provider.json +11 -3
  259. package/registry/toast.json +3 -2
  260. package/registry/transition.json +19 -0
  261. package/registry/wizard.json +3 -2
  262. package/src/atoms/Avatar.tsx +1 -1
  263. package/src/atoms/Badge.tsx +6 -4
  264. package/src/atoms/Button.tsx +8 -6
  265. package/src/atoms/Checkbox.tsx +4 -1
  266. package/src/atoms/Fab.tsx +1 -1
  267. package/src/atoms/Image.tsx +2 -2
  268. package/src/atoms/Input.tsx +2 -2
  269. package/src/atoms/PressableScale.tsx +16 -6
  270. package/src/atoms/Skeleton.tsx +114 -0
  271. package/src/atoms/Transition.tsx +189 -0
  272. package/src/atoms/index.ts +4 -0
  273. package/src/molecules/AvatarHeader.tsx +2 -2
  274. package/src/molecules/OptionCard.tsx +64 -27
  275. package/src/molecules/SegmentedControl.tsx +84 -0
  276. package/src/molecules/Stepper.tsx +147 -32
  277. package/src/molecules/Steps.tsx +2 -2
  278. package/src/molecules/index.ts +4 -4
  279. package/src/organisms/Alert.tsx +6 -5
  280. package/src/organisms/AlertProvider.tsx +26 -1
  281. package/src/organisms/BottomSheet.tsx +214 -62
  282. package/src/organisms/DateField.tsx +2 -2
  283. package/src/organisms/DatePicker.tsx +3 -3
  284. package/src/organisms/List.tsx +73 -41
  285. package/src/organisms/Modal.tsx +49 -61
  286. package/src/organisms/NavigationBar.tsx +223 -0
  287. package/src/organisms/Screen.tsx +11 -3
  288. package/src/organisms/SearchList.tsx +134 -38
  289. package/src/organisms/Select.tsx +227 -49
  290. package/src/organisms/ThemeToggle.tsx +30 -0
  291. package/src/organisms/Toast.tsx +19 -24
  292. package/src/organisms/ToastProvider.tsx +24 -0
  293. package/src/organisms/Wizard.tsx +36 -3
  294. package/src/organisms/imperativeBridge.ts +28 -0
  295. package/src/organisms/index.ts +7 -3
  296. package/src/safe-area/SafeAreaUIProvider.tsx +67 -0
  297. package/src/safe-area/index.ts +2 -0
  298. package/src/theme/UIProvider.tsx +15 -0
  299. package/src/theme/colors.ts +175 -0
  300. package/src/theme/createTheme.ts +38 -4
  301. package/src/theme/index.ts +3 -1
  302. package/src/theme/palettes.ts +108 -11
  303. package/src/theme/types.ts +10 -0
  304. package/lib/commonjs/molecules/ThemeToggle.js.map +0 -1
  305. package/lib/module/molecules/ThemeToggle.js.map +0 -1
  306. package/lib/typescript/molecules/ThemeToggle.d.ts.map +0 -1
  307. 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';
@@ -30,7 +30,7 @@ function defaultFormat(date: Date, monthNames: string[]): string {
30
30
  const useStyles = createStyles((theme) => ({
31
31
  container: { marginBottom: theme.tokens.spacing.lg, width: '100%', gap: theme.tokens.spacing.sm },
32
32
  label: { fontSize: theme.tokens.fontSize.md, fontWeight: theme.tokens.fontWeight.medium, color: theme.colors.text },
33
- required: { color: theme.colors.error },
33
+ required: { color: theme.colors.errorText },
34
34
  field: {
35
35
  flexDirection: 'row',
36
36
  alignItems: 'center',
@@ -45,7 +45,7 @@ const useStyles = createStyles((theme) => ({
45
45
  fieldError: { borderColor: theme.colors.error },
46
46
  fieldText: { fontSize: theme.tokens.fontSize.lg, color: theme.colors.text },
47
47
  placeholder: { color: theme.colors.textLight },
48
- errorText: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.error },
48
+ errorText: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.errorText },
49
49
  }));
50
50
 
51
51
  /**
@@ -110,13 +110,13 @@ const useStyles = createStyles((theme) => ({
110
110
  dayButton: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center' },
111
111
  dayText: { fontSize: theme.tokens.fontSize.md, color: theme.colors.text },
112
112
  daySelected: { backgroundColor: theme.colors.primary },
113
- dayTextSelected: { color: theme.colors.white, fontWeight: theme.tokens.fontWeight.bold },
113
+ dayTextSelected: { color: theme.colors.onPrimary, fontWeight: theme.tokens.fontWeight.bold },
114
114
  // El resaltado del tramo intermedio va en la celda (cuadrada, sin gaps) y
115
115
  // no en el botón redondo, así la barra del rango se ve continua.
116
- cellInRange: { backgroundColor: theme.colors.primary + '1f' },
116
+ cellInRange: { backgroundColor: theme.colors.primarySoft },
117
117
  cellRangeStart: { borderTopLeftRadius: 18, borderBottomLeftRadius: 18 },
118
118
  cellRangeEnd: { borderTopRightRadius: 18, borderBottomRightRadius: 18 },
119
- dayTextInRange: { color: theme.colors.primary, fontWeight: theme.tokens.fontWeight.semibold },
119
+ dayTextInRange: { color: theme.colors.primaryText, fontWeight: theme.tokens.fontWeight.semibold },
120
120
  dayToday: { borderWidth: 1, borderColor: theme.colors.primary },
121
121
  dayTextDisabled: { color: theme.colors.textLight, opacity: 0.4 },
122
122
  }));
@@ -1,32 +1,32 @@
1
- import React, { memo, useCallback } from 'react';
2
- import {
3
- ActivityIndicator,
4
- FlatList,
5
- Platform,
6
- RefreshControl,
7
- Text,
8
- View,
9
- type FlatListProps,
10
- type ViewStyle,
11
- } from 'react-native';
1
+ import React, { useCallback } from 'react';
2
+ import { FlatList, RefreshControl, View, type FlatListProps, type ViewStyle } from 'react-native';
12
3
  import { createStyles } from '../theme/createStyles';
13
4
  import { useColors, useLabels } from '../theme/UIProvider';
14
5
  import { EmptyState } from '../atoms/EmptyState';
6
+ import { Skeleton } from '../atoms/Skeleton';
15
7
  import { Spinner } from '../atoms/Spinner';
16
8
  import type { IconName } from '../icons/types';
17
9
 
18
10
  export interface ListProps<T> {
19
11
  data: T[];
20
- keyExtractor: (item: T) => string;
12
+ keyExtractor: (item: T, index: number) => string;
21
13
  renderItem: FlatListProps<T>['renderItem'];
22
14
 
15
+ /** Carga inicial: mientras sea true y no haya datos, se muestran placeholders. */
23
16
  isLoading: boolean;
17
+ /** Recarga con datos ya en pantalla (pull-to-refresh). @default false */
18
+ isRefreshing?: boolean;
24
19
  isLoadingMore?: boolean;
25
20
  /** Controlado por el consumidor: mientras es false, se muestra un spinner de pantalla completa. @default true */
26
21
  isReady?: boolean;
27
22
  loadingText?: string;
28
23
  loadingMoreText?: string;
29
24
 
25
+ /** Filas fantasma de la carga inicial. @default 6 */
26
+ skeletonCount?: number;
27
+ /** Placeholder propio, para que calce con la forma real del ítem. */
28
+ renderSkeletonItem?: () => React.ReactElement;
29
+
30
30
  onEndReached?: () => void;
31
31
  onEndReachedThreshold?: number;
32
32
  onRefresh?: () => void;
@@ -48,20 +48,15 @@ export interface ListProps<T> {
48
48
 
49
49
  const useStyles = createStyles((theme) => ({
50
50
  container: { flex: 1, backgroundColor: theme.colors.background },
51
- loadingOverlay: {
52
- position: 'absolute',
53
- top: 0,
54
- left: 0,
55
- right: 0,
56
- bottom: 0,
57
- backgroundColor: theme.colors.background + 'CC',
58
- zIndex: 10,
59
- justifyContent: 'center',
51
+ listContent: { padding: theme.tokens.spacing.md },
52
+ footerLoader: { paddingVertical: theme.tokens.spacing.xl, alignItems: 'center' },
53
+ skeletonRow: {
54
+ flexDirection: 'row',
60
55
  alignItems: 'center',
56
+ gap: theme.tokens.spacing.md,
57
+ padding: theme.tokens.spacing.md,
61
58
  },
62
- loadingText: { marginTop: theme.tokens.spacing.md, color: theme.colors.textLight, fontWeight: theme.tokens.fontWeight.medium },
63
- listContent: { padding: theme.tokens.spacing.md, paddingBottom: Platform.OS === 'android' ? 100 : 80 },
64
- footerLoader: { paddingVertical: theme.tokens.spacing.xl, alignItems: 'center' },
59
+ skeletonText: { flex: 1, gap: theme.tokens.spacing.sm },
65
60
  }));
66
61
 
67
62
  /**
@@ -75,10 +70,13 @@ export function List<T>({
75
70
  keyExtractor,
76
71
  renderItem,
77
72
  isLoading,
73
+ isRefreshing = false,
78
74
  isLoadingMore = false,
79
75
  isReady = true,
80
76
  loadingText,
81
77
  loadingMoreText,
78
+ skeletonCount = 6,
79
+ renderSkeletonItem,
82
80
  onEndReached,
83
81
  onEndReachedThreshold = 0.3,
84
82
  onRefresh,
@@ -98,22 +96,36 @@ export function List<T>({
98
96
  const resolvedLoadingText = loadingText ?? labels.loading;
99
97
  const resolvedLoadingMoreText = loadingMoreText ?? labels.loadingMore;
100
98
 
99
+ const isInitialLoading = isLoading && data.length === 0;
100
+
101
+ // El footer propio y el loader de paginación conviven: antes el primero
102
+ // tapaba al segundo y la lista dejaba de avisar que estaba trayendo más.
101
103
  const renderFooter = useCallback(() => {
102
- if (ListFooterComponent) {
103
- return typeof ListFooterComponent === 'function' ? <ListFooterComponent /> : (ListFooterComponent as React.ReactElement);
104
- }
105
- if (!isLoadingMore) return null;
104
+ const custom =
105
+ typeof ListFooterComponent === 'function' ? (
106
+ <ListFooterComponent />
107
+ ) : (
108
+ (ListFooterComponent as React.ReactElement | undefined) ?? null
109
+ );
110
+
106
111
  return (
107
- <View style={styles.footerLoader}>
108
- <Spinner text={resolvedLoadingMoreText} fullscreen={false} />
109
- </View>
112
+ <>
113
+ {custom}
114
+ {isLoadingMore && (
115
+ <View style={styles.footerLoader}>
116
+ <Spinner text={resolvedLoadingMoreText} fullscreen={false} />
117
+ </View>
118
+ )}
119
+ </>
110
120
  );
111
121
  }, [isLoadingMore, resolvedLoadingMoreText, ListFooterComponent, styles.footerLoader]);
112
122
 
123
+ // Vacío sólo cuando ya se sabe que no hay nada: durante la carga inicial se
124
+ // muestran placeholders, no "sin resultados".
113
125
  const renderEmpty = useCallback(() => {
114
- if (isLoading) return null;
126
+ if (isLoading || isRefreshing) return null;
115
127
  return <EmptyState title={emptyTitle} description={emptyDescription} iconName={emptyIconName} style={{ marginTop: 50 }} />;
116
- }, [isLoading, emptyTitle, emptyDescription, emptyIconName]);
128
+ }, [isLoading, isRefreshing, emptyTitle, emptyDescription, emptyIconName]);
117
129
 
118
130
  if (!isReady) {
119
131
  return (
@@ -123,15 +135,30 @@ export function List<T>({
123
135
  );
124
136
  }
125
137
 
126
- return (
127
- <View style={[styles.container, containerStyle]}>
128
- {isLoading && data.length === 0 && (
129
- <View style={styles.loadingOverlay} pointerEvents="none">
130
- <ActivityIndicator size="large" color={colors.primary} />
131
- <Text style={styles.loadingText}>{resolvedLoadingText}</Text>
138
+ if (isInitialLoading) {
139
+ return (
140
+ <View style={[styles.container, containerStyle]} testID="list-skeleton">
141
+ <View style={[styles.listContent, contentContainerStyle]}>
142
+ {Array.from({ length: skeletonCount }, (_, i) => (
143
+ <React.Fragment key={i}>
144
+ {renderSkeletonItem?.() ?? (
145
+ <View style={styles.skeletonRow}>
146
+ <Skeleton variant="circle" width={40} />
147
+ <View style={styles.skeletonText}>
148
+ <Skeleton width="60%" height={14} />
149
+ <Skeleton width="35%" height={12} />
150
+ </View>
151
+ </View>
152
+ )}
153
+ </React.Fragment>
154
+ ))}
132
155
  </View>
133
- )}
156
+ </View>
157
+ );
158
+ }
134
159
 
160
+ return (
161
+ <View style={[styles.container, containerStyle]}>
135
162
  <ListComponent
136
163
  data={data}
137
164
  keyExtractor={keyExtractor}
@@ -144,7 +171,12 @@ export function List<T>({
144
171
  ListEmptyComponent={renderEmpty}
145
172
  refreshControl={
146
173
  onRefresh ? (
147
- <RefreshControl refreshing={isLoading} onRefresh={onRefresh} colors={[colors.primary]} tintColor={colors.primary} />
174
+ <RefreshControl
175
+ refreshing={isRefreshing}
176
+ onRefresh={onRefresh}
177
+ colors={[colors.primary]}
178
+ tintColor={colors.primary}
179
+ />
148
180
  ) : undefined
149
181
  }
150
182
  {...listProps}