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,6 +1,5 @@
1
- import React, { memo, useEffect, useRef, useState } from 'react';
1
+ import React, { memo, useEffect, useState } from 'react';
2
2
  import {
3
- Animated,
4
3
  Dimensions,
5
4
  KeyboardAvoidingView,
6
5
  Modal as RNModal,
@@ -15,10 +14,11 @@ import { createStyles } from '../theme/createStyles';
15
14
  import { useColors, useInsets } from '../theme/UIProvider';
16
15
  import { Title } from '../atoms/Text';
17
16
  import { IconButton } from '../atoms/IconButton';
17
+ import { Transition, type TransitionPreset } from '../atoms/Transition';
18
18
 
19
19
  const { height: SCREEN_HEIGHT } = Dimensions.get('window');
20
20
 
21
- export type ModalVariant = 'full' | 'overlay' | 'fullScreen' | 'bottomSheet';
21
+ export type ModalVariant = 'full' | 'overlay' | 'fullScreen';
22
22
 
23
23
  export interface ModalProps
24
24
  extends Pick<RNModalProps, 'statusBarTranslucent' | 'onRequestClose' | 'hardwareAccelerated'> {
@@ -38,7 +38,6 @@ const useStyles = createStyles((theme) => ({
38
38
  flex1: { flex: 1 },
39
39
  fullWrapper: { flex: 1, backgroundColor: theme.colors.background },
40
40
  overlayContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: theme.tokens.spacing.xxl - 4 },
41
- bottomSheetContainer: { justifyContent: 'flex-end', padding: 0 },
42
41
  backdrop: { backgroundColor: theme.colors.overlay },
43
42
  overlayCard: {
44
43
  width: '100%',
@@ -48,15 +47,6 @@ const useStyles = createStyles((theme) => ({
48
47
  overflow: 'hidden',
49
48
  ...theme.tokens.shadow.lg,
50
49
  },
51
- bottomSheetCard: {
52
- width: '100%',
53
- maxHeight: '90%',
54
- backgroundColor: theme.colors.surface,
55
- borderTopLeftRadius: theme.tokens.radius.xxl,
56
- borderTopRightRadius: theme.tokens.radius.xxl,
57
- overflow: 'hidden',
58
- ...theme.tokens.shadow.lg,
59
- },
60
50
  header: {
61
51
  flexDirection: 'row',
62
52
  justifyContent: 'space-between',
@@ -83,10 +73,8 @@ const useStyles = createStyles((theme) => ({
83
73
  /**
84
74
  * Modal
85
75
  * Un solo componente para todos los modales de la app: maneja scroll,
86
- * teclado, insets y layout según `variant`. overlay/bottomSheet/full animan
87
- * su entrada/salida con Animated (solo transform, nunca opacity sobre la
88
- * tarjeta: animar opacity ahí dispara un bug de Android donde los hijos con
89
- * elevation parpadean/redibujan). El fondo semitransparente sí anima opacity.
76
+ * teclado, insets y layout según `variant`. Para una hoja desde abajo está
77
+ * BottomSheet, que además trae arrastre para cerrar.
90
78
  */
91
79
  export const Modal = memo(
92
80
  ({
@@ -107,43 +95,22 @@ export const Modal = memo(
107
95
  const isFull = variant === 'full';
108
96
  const isOverlay = variant === 'overlay';
109
97
  const isFullScreen = variant === 'fullScreen';
110
- const isBottomSheet = variant === 'bottomSheet';
111
- const isAnimatedVariant = isOverlay || isBottomSheet || isFull;
98
+ const isAnimatedVariant = isOverlay || isFull;
112
99
  const insets = useInsets();
113
100
  const colors = useColors();
114
101
  const styles = useStyles();
115
102
 
116
103
  const [mounted, setMounted] = useState(!isAnimatedVariant && visible);
117
- const backdropOpacity = useRef(new Animated.Value(0)).current;
118
- const cardProgress = useRef(new Animated.Value(0)).current;
119
104
 
120
105
  useEffect(() => {
121
- if (!isAnimatedVariant) {
122
- setMounted(visible);
123
- return;
124
- }
125
- if (visible) {
126
- setMounted(true);
127
- Animated.timing(backdropOpacity, { toValue: 1, duration: 200, useNativeDriver: true }).start();
128
- Animated.timing(cardProgress, { toValue: 1, duration: 250, useNativeDriver: true }).start();
129
- } else {
130
- Animated.timing(backdropOpacity, { toValue: 0, duration: 180, useNativeDriver: true }).start();
131
- Animated.timing(cardProgress, { toValue: 0, duration: 200, useNativeDriver: true }).start(({ finished }) => {
132
- if (finished) setMounted(false);
133
- });
134
- }
135
- // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ if (!isAnimatedVariant) setMounted(visible);
107
+ else if (visible) setMounted(true);
136
108
  }, [visible, isAnimatedVariant]);
137
109
 
138
- const cardAnimatedStyle =
139
- isBottomSheet || isFull
140
- ? { transform: [{ translateY: cardProgress.interpolate({ inputRange: [0, 1], outputRange: [SCREEN_HEIGHT, 0] }) }] }
141
- : {
142
- transform: [
143
- { translateY: cardProgress.interpolate({ inputRange: [0, 1], outputRange: [-40, 0] }) },
144
- { scale: cardProgress.interpolate({ inputRange: [0, 1], outputRange: [0.9, 1] }) },
145
- ],
146
- };
110
+ // La tarjeta nunca anima opacity: hacerlo dispara un bug de Android donde
111
+ // los hijos con elevation parpadean. El fondo semitransparente sí.
112
+ const cardPreset: TransitionPreset[] = isFull ? ['slide-up'] : ['slide-down', 'scale'];
113
+ const cardDistance = isFull ? SCREEN_HEIGHT : 40;
147
114
 
148
115
  const behavior = Platform.OS === 'ios' ? (isFull ? undefined : 'padding') : 'height';
149
116
  const keyboardOffset = Platform.OS === 'ios' ? 0 : 20;
@@ -204,42 +171,63 @@ export const Modal = memo(
204
171
  presentationStyle={
205
172
  Platform.OS === 'ios' ? (isFullScreen ? 'fullScreen' : 'overFullScreen') : undefined
206
173
  }
207
- transparent={isOverlay || isBottomSheet || isFull}
174
+ transparent={isOverlay || isFull}
208
175
  statusBarTranslucent={statusBarTranslucent}
209
176
  hardwareAccelerated={hardwareAccelerated}
210
177
  >
211
178
  <KeyboardAvoidingView behavior={behavior} keyboardVerticalOffset={keyboardOffset} style={styles.flex1}>
212
- {isOverlay || isBottomSheet ? (
213
- <View style={[styles.overlayContainer, isBottomSheet && styles.bottomSheetContainer]}>
179
+ {isOverlay ? (
180
+ <View style={styles.overlayContainer}>
214
181
  {/* Sin accessibilityLabel propio: el header ya expone un botón "Close"
215
182
  accesible; duplicar el label aquí generaría dos elementos con el
216
183
  mismo nombre para lectores de pantalla. Tocar afuera es un atajo
217
184
  puramente táctil. */}
218
- <Animated.View style={[{ ...StyleSheetAbsoluteFill }, styles.backdrop, { opacity: backdropOpacity }]}>
185
+ <Transition
186
+ visible={visible}
187
+ preset="fade"
188
+ duration={200}
189
+ keepMounted
190
+ style={[{ ...StyleSheetAbsoluteFill }, styles.backdrop]}
191
+ >
219
192
  <Pressable style={StyleSheetAbsoluteFill} onPress={onClose} testID="modal-backdrop" />
220
- </Animated.View>
221
-
222
- <Animated.View
223
- style={[
224
- isOverlay ? styles.overlayCard : styles.bottomSheetCard,
225
- isBottomSheet && { paddingBottom: Math.max(insets.bottom, Platform.OS === 'ios' ? 20 : 0) },
226
- containerStyle,
227
- cardAnimatedStyle,
228
- ]}
193
+ </Transition>
194
+
195
+ <Transition
196
+ visible={visible}
197
+ preset={cardPreset}
198
+ distance={cardDistance}
199
+ duration={250}
200
+ onExited={() => setMounted(false)}
201
+ style={[styles.overlayCard, containerStyle]}
229
202
  >
230
203
  {renderHeader()}
231
204
  {renderMainContent()}
232
205
  {renderFooter()}
233
- </Animated.View>
206
+ </Transition>
234
207
  </View>
208
+ ) : isFull ? (
209
+ <Transition
210
+ visible={visible}
211
+ preset={cardPreset}
212
+ distance={cardDistance}
213
+ duration={250}
214
+ onExited={() => setMounted(false)}
215
+ style={[styles.fullWrapper, containerStyle]}
216
+ >
217
+ <View style={[styles.flex1, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
218
+ {renderHeader()}
219
+ {renderMainContent()}
220
+ {renderFooter()}
221
+ </View>
222
+ </Transition>
235
223
  ) : (
236
- <Animated.View style={[styles.fullWrapper, containerStyle, isFull && cardAnimatedStyle]}>
224
+ <View style={[styles.fullWrapper, containerStyle]}>
237
225
  <View style={[styles.flex1, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
238
226
  {renderHeader()}
239
227
  {renderMainContent()}
240
228
  {renderFooter()}
241
229
  </View>
242
- </Animated.View>
230
+ </View>
243
231
  )}
244
232
  </KeyboardAvoidingView>
245
233
  </RNModal>
@@ -0,0 +1,223 @@
1
+ import React, { memo, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Animated,
4
+ StyleSheet,
5
+ Text,
6
+ TouchableOpacity,
7
+ View,
8
+ type LayoutChangeEvent,
9
+ type StyleProp,
10
+ type ViewStyle,
11
+ } from 'react-native';
12
+ import { createStyles } from '../theme/createStyles';
13
+ import { useAllowFontScaling, useColors, useInsets, useTheme } from '../theme/UIProvider';
14
+ import { Icon } from '../icons/Icon';
15
+ import type { IconName } from '../icons/types';
16
+
17
+ export interface NavigationBarItem {
18
+ key: string;
19
+ label: string;
20
+ iconName?: IconName;
21
+ /** Contador sobre el ícono. `true` pinta sólo el punto. */
22
+ badge?: number | string | true;
23
+ }
24
+
25
+ export interface NavigationBarProps {
26
+ items: NavigationBarItem[];
27
+ activeKey: string;
28
+ onChange: (key: string) => void;
29
+ /** Borde de la pantalla donde vive la barra. @default 'bottom' */
30
+ position?: 'bottom' | 'top';
31
+ /** @default true */
32
+ showLabels?: boolean;
33
+ /** Suma el inset de safe area del borde donde vive la barra. @default true */
34
+ safeArea?: boolean;
35
+ style?: StyleProp<ViewStyle>;
36
+ testID?: string;
37
+ }
38
+
39
+ const useStyles = createStyles((theme) => ({
40
+ bar: {
41
+ flexDirection: 'row',
42
+ backgroundColor: theme.colors.surface,
43
+ borderColor: theme.colors.border,
44
+ paddingHorizontal: theme.tokens.spacing.xs,
45
+ paddingVertical: theme.tokens.spacing.sm,
46
+ },
47
+ barBottom: { borderTopWidth: StyleSheet.hairlineWidth },
48
+ barTop: { borderBottomWidth: StyleSheet.hairlineWidth },
49
+ indicator: {
50
+ position: 'absolute',
51
+ top: theme.tokens.spacing.xs,
52
+ bottom: theme.tokens.spacing.xs,
53
+ left: theme.tokens.spacing.sm,
54
+ borderRadius: theme.tokens.radius.lg,
55
+ backgroundColor: theme.colors.primarySoft,
56
+ },
57
+ item: { flex: 1, paddingVertical: theme.tokens.spacing.xs },
58
+ itemContent: { alignItems: 'center', justifyContent: 'center', gap: 2 },
59
+ label: { fontSize: theme.tokens.fontSize.xs, fontWeight: theme.tokens.fontWeight.medium },
60
+ badge: {
61
+ position: 'absolute',
62
+ top: -4,
63
+ left: '50%',
64
+ marginLeft: 4,
65
+ minWidth: 16,
66
+ height: 16,
67
+ paddingHorizontal: 4,
68
+ borderRadius: theme.tokens.radius.full,
69
+ backgroundColor: theme.colors.error,
70
+ alignItems: 'center',
71
+ justifyContent: 'center',
72
+ },
73
+ badgeDot: { minWidth: 10, height: 10, top: -2, paddingHorizontal: 0 },
74
+ badgeText: {
75
+ fontSize: 10,
76
+ fontWeight: theme.tokens.fontWeight.bold,
77
+ color: theme.colors.onError,
78
+ },
79
+ }));
80
+
81
+ const ICON_SIZE = 22;
82
+
83
+ /**
84
+ * NavigationBar
85
+ * Barra de navegación por pestañas con la misma apariencia y las mismas
86
+ * transiciones en iOS y Android: el indicador se desliza hasta la pestaña
87
+ * activa y su contenido crece y toma el color de acento con un cross-fade.
88
+ * No navega por su cuenta — reporta la pestaña tocada y el consumidor decide.
89
+ */
90
+ export const NavigationBar = memo(
91
+ ({
92
+ items,
93
+ activeKey,
94
+ onChange,
95
+ position = 'bottom',
96
+ showLabels = true,
97
+ safeArea = true,
98
+ style,
99
+ testID,
100
+ }: NavigationBarProps) => {
101
+ const theme = useTheme();
102
+ const colors = useColors();
103
+ const styles = useStyles();
104
+ const insets = useInsets();
105
+ const allowFontScaling = useAllowFontScaling();
106
+
107
+ const [barWidth, setBarWidth] = useState(0);
108
+ const slide = useRef(new Animated.Value(0)).current;
109
+ const placed = useRef(false);
110
+
111
+ const activeIndex = Math.max(
112
+ 0,
113
+ items.findIndex((item) => item.key === activeKey)
114
+ );
115
+ const itemWidth = items.length > 0 ? barWidth / items.length : 0;
116
+
117
+ useEffect(() => {
118
+ if (itemWidth <= 0) return;
119
+ const to = activeIndex * itemWidth;
120
+ // En el primer layout el indicador aparece en su lugar; animarlo desde 0
121
+ // sería un deslizamiento que el usuario no pidió.
122
+ if (!placed.current) {
123
+ placed.current = true;
124
+ slide.setValue(to);
125
+ return;
126
+ }
127
+ Animated.spring(slide, { toValue: to, useNativeDriver: true, friction: 9, tension: 90 }).start();
128
+ }, [activeIndex, itemWidth, slide]);
129
+
130
+ const onLayout = (e: LayoutChangeEvent) => {
131
+ const next = e.nativeEvent.layout.width - theme.tokens.spacing.xs * 2;
132
+ if (next !== barWidth) setBarWidth(next);
133
+ };
134
+
135
+ const renderContent = (item: NavigationBarItem, color: string) => (
136
+ <>
137
+ {!!item.iconName && <Icon name={item.iconName} size={ICON_SIZE} color={color} />}
138
+ {showLabels && (
139
+ <Text allowFontScaling={allowFontScaling} numberOfLines={1} style={[styles.label, { color }]}>
140
+ {item.label}
141
+ </Text>
142
+ )}
143
+ </>
144
+ );
145
+
146
+ return (
147
+ <View
148
+ testID={testID}
149
+ onLayout={onLayout}
150
+ accessibilityRole="tablist"
151
+ style={[
152
+ styles.bar,
153
+ position === 'bottom' ? styles.barBottom : styles.barTop,
154
+ safeArea && position === 'bottom' && { paddingBottom: theme.tokens.spacing.sm + insets.bottom },
155
+ safeArea && position === 'top' && { paddingTop: theme.tokens.spacing.sm + insets.top },
156
+ style,
157
+ ]}
158
+ >
159
+ {itemWidth > 0 && (
160
+ <Animated.View
161
+ pointerEvents="none"
162
+ style={[
163
+ styles.indicator,
164
+ {
165
+ width: itemWidth - theme.tokens.spacing.sm * 2,
166
+ transform: [{ translateX: slide }],
167
+ },
168
+ ]}
169
+ />
170
+ )}
171
+
172
+ {items.map((item, index) => {
173
+ const active = index === activeIndex;
174
+ const range = [(index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth];
175
+ const scale =
176
+ itemWidth > 0
177
+ ? slide.interpolate({ inputRange: range, outputRange: [1, 1.08, 1], extrapolate: 'clamp' })
178
+ : 1;
179
+ const activeOpacity =
180
+ itemWidth > 0
181
+ ? slide.interpolate({ inputRange: range, outputRange: [0, 1, 0], extrapolate: 'clamp' })
182
+ : 0;
183
+
184
+ return (
185
+ <TouchableOpacity
186
+ key={item.key}
187
+ style={styles.item}
188
+ activeOpacity={0.7}
189
+ onPress={() => onChange(item.key)}
190
+ accessibilityRole="tab"
191
+ accessibilityLabel={item.label}
192
+ accessibilityState={{ selected: active }}
193
+ >
194
+ <Animated.View style={[styles.itemContent, { transform: [{ scale }] }]}>
195
+ {renderContent(item, colors.textLight)}
196
+ {/* Copia decorativa: sin ocultarla, el lector lee el label dos veces. */}
197
+ <Animated.View
198
+ pointerEvents="none"
199
+ accessibilityElementsHidden
200
+ importantForAccessibility="no-hide-descendants"
201
+ style={[StyleSheet.absoluteFill, styles.itemContent, { opacity: activeOpacity }]}
202
+ >
203
+ {renderContent(item, colors.primaryText)}
204
+ </Animated.View>
205
+
206
+ {item.badge !== undefined && (
207
+ <View style={[styles.badge, item.badge === true && styles.badgeDot]}>
208
+ {item.badge !== true && (
209
+ <Text allowFontScaling={false} style={styles.badgeText}>
210
+ {item.badge}
211
+ </Text>
212
+ )}
213
+ </View>
214
+ )}
215
+ </Animated.View>
216
+ </TouchableOpacity>
217
+ );
218
+ })}
219
+ </View>
220
+ );
221
+ }
222
+ );
223
+ NavigationBar.displayName = 'NavigationBar';
@@ -13,7 +13,8 @@ export interface ScreenProps {
13
13
  /** Qué insets de safe area aplicar. @default ['top', 'bottom'] */
14
14
  edges?: ScreenEdge[];
15
15
  /**
16
- * Compensa el teclado con un KeyboardAvoidingView. Ponelo en false cuando
16
+ * Compensa el teclado: en iOS con scroll lo hace el propio ScrollView, en el
17
+ * resto de los casos con un KeyboardAvoidingView. Ponelo en false cuando
17
18
  * el contenido ya trae su propio scroller que maneja el teclado (Wizard,
18
19
  * List, SearchList): las dos compensaciones se suman y el campo enfocado
19
20
  * termina empujado fuera de la pantalla.
@@ -46,8 +47,15 @@ export const Screen = memo(({
46
47
  const applyTop = edges.includes('top');
47
48
  const applyBottom = edges.includes('bottom');
48
49
 
49
- const KeyboardWrapper: React.ComponentType<any> = keyboardAvoiding ? KeyboardAvoidingView : View;
50
- const keyboardProps = keyboardAvoiding
50
+ // En iOS el ScrollView de abajo ya sube el campo enfocado con
51
+ // automaticallyAdjustKeyboardInsets; envolverlo además en un
52
+ // KeyboardAvoidingView compensa el teclado dos veces y el input termina
53
+ // empujado fuera del borde superior. Android no tiene ese prop, así que ahí
54
+ // el KeyboardAvoidingView sigue siendo el único mecanismo.
55
+ const avoidWithView = keyboardAvoiding && !(scrollable && Platform.OS === 'ios');
56
+
57
+ const KeyboardWrapper: React.ComponentType<any> = avoidWithView ? KeyboardAvoidingView : View;
58
+ const keyboardProps = avoidWithView
51
59
  ? {
52
60
  behavior: Platform.OS === 'ios' ? ('padding' as const) : ('height' as const),
53
61
  keyboardVerticalOffset: Platform.OS === 'ios' ? 0 : 20,