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,12 +1,31 @@
1
- import React, { memo, useState } from 'react';
2
- import { ActivityIndicator, Pressable, Text, View, type StyleProp, type ViewStyle } from 'react-native';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Keyboard,
5
+ Modal,
6
+ Platform,
7
+ Pressable,
8
+ ScrollView,
9
+ Text,
10
+ TextInput,
11
+ View,
12
+ useWindowDimensions,
13
+ type StyleProp,
14
+ type ViewStyle,
15
+ } from 'react-native';
3
16
  import { createStyles } from '../theme/createStyles';
4
17
  import { useAllowFontScaling, useColors, useLabels } from '../theme/UIProvider';
5
18
  import { Icon } from '../icons/Icon';
19
+ import type { IconName } from '../icons/types';
6
20
 
7
21
  export interface SelectOption<T> {
8
22
  label: string;
9
23
  value: T;
24
+ /** Glifo del set de la librería, a la izquierda del label. */
25
+ iconName?: IconName;
26
+ /** Emoji a la izquierda del label. Gana sobre `iconName`. */
27
+ emoji?: string;
28
+ disabled?: boolean;
10
29
  }
11
30
 
12
31
  export interface SelectProps<T> {
@@ -18,12 +37,24 @@ export interface SelectProps<T> {
18
37
  placeholder?: string;
19
38
  error?: string;
20
39
  isLoading?: boolean;
40
+ disabled?: boolean;
41
+ /** Agrega un buscador dentro del desplegable. @default false */
42
+ searchable?: boolean;
43
+ searchPlaceholder?: string;
44
+ /** Alto máximo del desplegable, si el espacio disponible lo permite. @default 280 */
45
+ maxHeight?: number;
46
+ onOpenChange?: (open: boolean) => void;
21
47
  containerStyle?: StyleProp<ViewStyle>;
48
+ testID?: string;
22
49
  }
23
50
 
51
+ /** Aire entre el campo y el desplegable. */
52
+ const GAP = 8;
53
+ /** Por debajo de esto el desplegable no muestra ni dos filas. */
54
+ const MIN_HEIGHT = 120;
55
+
24
56
  const useStyles = createStyles((theme) => ({
25
57
  container: { gap: theme.tokens.spacing.sm },
26
- containerOpen: { zIndex: 20, elevation: 20 },
27
58
  label: { fontWeight: theme.tokens.fontWeight.bold, fontSize: theme.tokens.fontSize.md, color: theme.colors.text },
28
59
  required: { color: theme.colors.errorText },
29
60
  field: {
@@ -37,23 +68,29 @@ const useStyles = createStyles((theme) => ({
37
68
  minHeight: 50,
38
69
  paddingHorizontal: theme.tokens.spacing.lg,
39
70
  },
40
- fieldText: { fontSize: theme.tokens.fontSize.lg, color: theme.colors.text },
71
+ fieldOpen: { borderColor: theme.colors.primary },
72
+ fieldDisabled: { opacity: 0.5 },
73
+ fieldValue: { flexDirection: 'row', alignItems: 'center', gap: theme.tokens.spacing.sm, flexShrink: 1 },
74
+ fieldText: { fontSize: theme.tokens.fontSize.lg, color: theme.colors.text, flexShrink: 1 },
41
75
  fieldPlaceholder: { color: theme.colors.textLight },
42
- optionsList: {
76
+ backdrop: { flex: 1 },
77
+ sheet: {
43
78
  position: 'absolute',
44
- top: '100%',
45
- left: 0,
46
- right: 0,
47
- marginTop: theme.tokens.spacing.sm,
48
79
  borderWidth: 2,
49
80
  borderColor: theme.colors.border,
50
81
  borderRadius: theme.tokens.radius.lg,
51
82
  backgroundColor: theme.colors.surface,
52
83
  overflow: 'hidden',
53
- zIndex: 20,
54
- elevation: 20,
55
84
  ...theme.tokens.shadow.md,
56
85
  },
86
+ search: {
87
+ fontSize: theme.tokens.fontSize.md,
88
+ color: theme.colors.text,
89
+ paddingHorizontal: theme.tokens.spacing.lg,
90
+ paddingVertical: theme.tokens.spacing.md,
91
+ borderBottomWidth: 1,
92
+ borderBottomColor: theme.colors.border,
93
+ },
57
94
  optionRow: {
58
95
  flexDirection: 'row',
59
96
  alignItems: 'center',
@@ -65,16 +102,33 @@ const useStyles = createStyles((theme) => ({
65
102
  },
66
103
  optionRowLast: { borderBottomWidth: 0 },
67
104
  optionRowSelected: { backgroundColor: theme.colors.primarySoft },
105
+ optionRowDisabled: { opacity: 0.4 },
106
+ optionLabel: { flexDirection: 'row', alignItems: 'center', gap: theme.tokens.spacing.sm, flexShrink: 1 },
68
107
  optionText: { fontSize: theme.tokens.fontSize.lg - 1, color: theme.colors.text },
69
108
  optionTextSelected: { fontWeight: theme.tokens.fontWeight.semibold, color: theme.colors.primaryText },
109
+ emoji: { fontSize: theme.tokens.fontSize.lg },
110
+ empty: {
111
+ padding: theme.tokens.spacing.lg,
112
+ textAlign: 'center',
113
+ color: theme.colors.textLight,
114
+ fontSize: theme.tokens.fontSize.md,
115
+ },
70
116
  errorText: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.errorText },
71
117
  }));
72
118
 
119
+ interface Anchor {
120
+ x: number;
121
+ y: number;
122
+ width: number;
123
+ height: number;
124
+ }
125
+
73
126
  /**
74
127
  * Select
75
- * Dropdown que expande la lista de opciones flotando sobre el resto del
76
- * contenido (position: absolute), en vez de empujarlo hacia abajo o abrir
77
- * un picker nativo del sistema.
128
+ * Dropdown de una sola opción. La lista se mide contra la ventana y se dibuja
129
+ * dentro de un Modal, no en el árbol del campo: así ningún `overflow: hidden`
130
+ * la recorta, no depende del zIndex (poco confiable en Android) y el fondo del
131
+ * Modal resuelve cerrar tocando afuera y con el botón atrás.
78
132
  */
79
133
  export function Select<T extends string | number>({
80
134
  label,
@@ -85,17 +139,93 @@ export function Select<T extends string | number>({
85
139
  placeholder,
86
140
  error,
87
141
  isLoading = false,
142
+ disabled = false,
143
+ searchable = false,
144
+ searchPlaceholder,
145
+ maxHeight = 280,
146
+ onOpenChange,
88
147
  containerStyle,
148
+ testID,
89
149
  }: SelectProps<T>) {
90
150
  const colors = useColors();
91
151
  const styles = useStyles();
92
152
  const labels = useLabels();
93
153
  const allowFontScaling = useAllowFontScaling();
154
+ const { height: windowHeight } = useWindowDimensions();
155
+
156
+ const fieldRef = useRef<View>(null);
94
157
  const [open, setOpen] = useState(false);
158
+ const [anchor, setAnchor] = useState<Anchor | null>(null);
159
+ const [query, setQuery] = useState('');
160
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
161
+ const isDisabled = disabled || isLoading;
95
162
  const selectedOption = options.find((o) => o.value === selectedValue);
96
163
 
164
+ const close = useCallback(() => {
165
+ setOpen(false);
166
+ setAnchor(null);
167
+ setQuery('');
168
+ onOpenChange?.(false);
169
+ }, [onOpenChange]);
170
+
171
+ // Abrir no espera a la medición: si `measureInWindow` no responde, la lista
172
+ // igual aparece (centrada) en vez de que el campo deje de funcionar.
173
+ const openList = () => {
174
+ fieldRef.current?.measureInWindow((x, y, width, height) => setAnchor({ x, y, width, height }));
175
+ setOpen(true);
176
+ onOpenChange?.(true);
177
+ };
178
+
179
+ // El buscador abre el teclado encima del desplegable: sin descontarlo, la
180
+ // lista se dibuja donde ya no se ve.
181
+ useEffect(() => {
182
+ if (!open) return;
183
+ setKeyboardHeight(Keyboard.metrics()?.height ?? 0);
184
+ const [show, hide] =
185
+ Platform.OS === 'ios' ? ['keyboardWillShow', 'keyboardWillHide'] : ['keyboardDidShow', 'keyboardDidHide'];
186
+ const subs = [
187
+ Keyboard.addListener(show as 'keyboardDidShow', (e) => setKeyboardHeight(e.endCoordinates.height)),
188
+ Keyboard.addListener(hide as 'keyboardDidHide', () => setKeyboardHeight(0)),
189
+ ];
190
+ return () => subs.forEach((sub) => sub.remove());
191
+ }, [open]);
192
+
193
+ const visibleOptions = useMemo(() => {
194
+ if (!searchable || query.trim() === '') return options;
195
+ const needle = query.trim().toLowerCase();
196
+ return options.filter((option) => option.label.toLowerCase().includes(needle));
197
+ }, [options, query, searchable]);
198
+
199
+ // Se abre hacia donde haya más aire: contra el borde inferior de la pantalla,
200
+ // hacia arriba es la única forma de que entre.
201
+ const placement = useMemo((): ViewStyle => {
202
+ if (!anchor) return { left: GAP * 2, right: GAP * 2, top: windowHeight * 0.25, maxHeight };
203
+ const usableHeight = windowHeight - keyboardHeight;
204
+ const below = usableHeight - (anchor.y + anchor.height) - GAP * 2;
205
+ const above = anchor.y - GAP * 2;
206
+ const downwards = below >= Math.min(maxHeight, above);
207
+ return {
208
+ left: anchor.x,
209
+ width: anchor.width,
210
+ maxHeight: Math.max(Math.min(maxHeight, downwards ? below : above), MIN_HEIGHT),
211
+ ...(downwards ? { top: anchor.y + anchor.height + GAP } : { bottom: windowHeight - anchor.y + GAP }),
212
+ };
213
+ }, [anchor, keyboardHeight, maxHeight, windowHeight]);
214
+
215
+ const renderLeading = (option: SelectOption<T> | undefined, color: string) => {
216
+ if (!option) return null;
217
+ if (option.emoji) {
218
+ return (
219
+ <Text allowFontScaling={allowFontScaling} style={styles.emoji}>
220
+ {option.emoji}
221
+ </Text>
222
+ );
223
+ }
224
+ return option.iconName ? <Icon name={option.iconName} size={20} color={color} /> : null;
225
+ };
226
+
97
227
  return (
98
- <View style={[styles.container, open && styles.containerOpen, containerStyle]}>
228
+ <View style={[styles.container, containerStyle]} testID={testID}>
99
229
  {label && (
100
230
  <Text allowFontScaling={allowFontScaling} style={styles.label}>
101
231
  {label}
@@ -104,16 +234,25 @@ export function Select<T extends string | number>({
104
234
  )}
105
235
 
106
236
  <Pressable
107
- style={styles.field}
108
- onPress={() => !isLoading && setOpen((o) => !o)}
109
- disabled={isLoading}
110
- accessibilityRole="button"
237
+ ref={fieldRef}
238
+ style={[styles.field, open && styles.fieldOpen, isDisabled && styles.fieldDisabled]}
239
+ onPress={openList}
240
+ disabled={isDisabled}
241
+ accessibilityRole="combobox"
111
242
  accessibilityLabel={label ?? placeholder ?? labels.selectPlaceholder}
112
- accessibilityState={{ expanded: open, disabled: isLoading }}
243
+ accessibilityValue={{ text: selectedOption?.label ?? '' }}
244
+ accessibilityState={{ expanded: open, disabled: isDisabled }}
113
245
  >
114
- <Text allowFontScaling={allowFontScaling} style={[styles.fieldText, !selectedOption && styles.fieldPlaceholder]}>
115
- {selectedOption?.label || placeholder || labels.selectPlaceholder}
116
- </Text>
246
+ <View style={styles.fieldValue}>
247
+ {renderLeading(selectedOption, colors.textLight)}
248
+ <Text
249
+ allowFontScaling={allowFontScaling}
250
+ numberOfLines={1}
251
+ style={[styles.fieldText, !selectedOption && styles.fieldPlaceholder]}
252
+ >
253
+ {selectedOption?.label || placeholder || labels.selectPlaceholder}
254
+ </Text>
255
+ </View>
117
256
  {isLoading ? (
118
257
  <ActivityIndicator size="small" color={colors.primary} />
119
258
  ) : (
@@ -121,29 +260,68 @@ export function Select<T extends string | number>({
121
260
  )}
122
261
  </Pressable>
123
262
 
124
- {open && !isLoading && (
125
- <View style={styles.optionsList}>
126
- {options.map((option, index) => {
127
- const isSelected = option.value === selectedValue;
128
- return (
129
- <Pressable
130
- key={String(option.value)}
131
- style={[styles.optionRow, index === options.length - 1 && styles.optionRowLast, isSelected && styles.optionRowSelected]}
132
- onPress={() => {
133
- onSelect(option.value);
134
- setOpen(false);
135
- }}
136
- accessibilityRole="menuitem"
137
- accessibilityState={{ selected: isSelected }}
138
- >
139
- <Text allowFontScaling={allowFontScaling} style={[styles.optionText, isSelected && styles.optionTextSelected]}>
140
- {option.label}
263
+ {open && (
264
+ <Modal visible transparent animationType="fade" statusBarTranslucent onRequestClose={close}>
265
+ <Pressable style={styles.backdrop} onPress={close} testID="select-backdrop" />
266
+
267
+ <View style={[styles.sheet, placement]} accessibilityRole="radiogroup" testID="select-list">
268
+ {searchable && (
269
+ <TextInput
270
+ allowFontScaling={allowFontScaling}
271
+ style={styles.search}
272
+ value={query}
273
+ onChangeText={setQuery}
274
+ placeholder={searchPlaceholder ?? labels.search}
275
+ placeholderTextColor={colors.textLight}
276
+ autoFocus
277
+ accessibilityLabel={searchPlaceholder ?? labels.search}
278
+ />
279
+ )}
280
+
281
+ <ScrollView keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false} bounces={false}>
282
+ {visibleOptions.map((option, index) => {
283
+ const isSelected = option.value === selectedValue;
284
+ return (
285
+ <Pressable
286
+ key={String(option.value)}
287
+ style={[
288
+ styles.optionRow,
289
+ index === visibleOptions.length - 1 && styles.optionRowLast,
290
+ isSelected && styles.optionRowSelected,
291
+ option.disabled && styles.optionRowDisabled,
292
+ ]}
293
+ onPress={() => {
294
+ onSelect(option.value);
295
+ close();
296
+ }}
297
+ disabled={option.disabled}
298
+ accessibilityRole="radio"
299
+ accessibilityLabel={option.label}
300
+ accessibilityState={{ selected: isSelected, checked: isSelected, disabled: !!option.disabled }}
301
+ >
302
+ <View style={styles.optionLabel}>
303
+ {renderLeading(option, isSelected ? colors.primary : colors.textLight)}
304
+ <Text
305
+ allowFontScaling={allowFontScaling}
306
+ numberOfLines={1}
307
+ style={[styles.optionText, isSelected && styles.optionTextSelected]}
308
+ >
309
+ {option.label}
310
+ </Text>
311
+ </View>
312
+ {isSelected && <Icon name="check" size={18} color={colors.primary} />}
313
+ </Pressable>
314
+ );
315
+ })}
316
+
317
+ {visibleOptions.length === 0 && (
318
+ <Text allowFontScaling={allowFontScaling} style={styles.empty}>
319
+ {labels.noResultsTitle}
141
320
  </Text>
142
- {isSelected && <Icon name="check" size={18} color={colors.primary} />}
143
- </Pressable>
144
- );
145
- })}
146
- </View>
321
+ )}
322
+ </ScrollView>
323
+ </View>
324
+ </Modal>
147
325
  )}
148
326
 
149
327
  {!!error && (
@@ -0,0 +1,30 @@
1
+ import React, { memo, useMemo } from 'react';
2
+ import { type StyleProp, type ViewStyle } from 'react-native';
3
+ import { useThemeMode, type ThemeMode } from '../theme/UIProvider';
4
+ import { SegmentedControl } from '../molecules/SegmentedControl';
5
+
6
+ export interface ThemeToggleProps {
7
+ style?: StyleProp<ViewStyle>;
8
+ labels?: Partial<Record<ThemeMode, string>>;
9
+ testID?: string;
10
+ }
11
+
12
+ const MODES: ThemeMode[] = ['system', 'light', 'dark'];
13
+ const DEFAULT_LABELS: Record<ThemeMode, string> = { system: 'Auto', light: 'Light', dark: 'Dark' };
14
+
15
+ /**
16
+ * ThemeToggle
17
+ * Conecta un SegmentedControl con UIProvider.setMode. Sin persistencia — el
18
+ * consumidor la resuelve vía onModeChange en <UIProvider>.
19
+ */
20
+ export const ThemeToggle = memo(({ style, labels: labelOverrides, testID }: ThemeToggleProps) => {
21
+ const { mode, setMode } = useThemeMode();
22
+
23
+ const options = useMemo(() => {
24
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
25
+ return MODES.map((value) => ({ value, label: labels[value] }));
26
+ }, [labelOverrides]);
27
+
28
+ return <SegmentedControl options={options} value={mode} onChange={setMode} style={style} testID={testID} />;
29
+ });
30
+ ThemeToggle.displayName = 'ThemeToggle';
@@ -1,7 +1,8 @@
1
- import React, { memo, useEffect, useRef } from 'react';
2
- import { Animated, Pressable, Text, View, type StyleProp, type ViewStyle } from 'react-native';
1
+ import React, { memo } from 'react';
2
+ import { Pressable, Text, View, type StyleProp, type ViewStyle } from 'react-native';
3
3
  import { createStyles } from '../theme/createStyles';
4
4
  import { useAllowFontScaling, useColors } from '../theme/UIProvider';
5
+ import { Transition } from '../atoms/Transition';
5
6
  import { Icon } from '../icons/Icon';
6
7
  import type { IconName } from '../icons/types';
7
8
 
@@ -120,24 +121,10 @@ Toast.displayName = 'Toast';
120
121
 
121
122
  /** Envuelve un Toast con su animación de entrada/salida. Uso interno del provider. */
122
123
  export const AnimatedToast = memo(
123
- ({ children, onHeight }: { children: React.ReactNode; onHeight?: (h: number) => void }) => {
124
- const progress = useRef(new Animated.Value(0)).current;
125
-
126
- useEffect(() => {
127
- Animated.spring(progress, { toValue: 1, useNativeDriver: true, speed: 14, bounciness: 4 }).start();
128
- }, [progress]);
129
-
130
- return (
131
- <Animated.View
132
- onLayout={(e) => onHeight?.(e.nativeEvent.layout.height)}
133
- style={{
134
- opacity: progress,
135
- transform: [{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [-16, 0] }) }],
136
- }}
137
- >
138
- {children}
139
- </Animated.View>
140
- );
141
- }
124
+ ({ children, onHeight }: { children: React.ReactNode; onHeight?: (h: number) => void }) => (
125
+ <Transition preset={['fade', 'slide-down']} distance={16} spring>
126
+ <View onLayout={(e) => onHeight?.(e.nativeEvent.layout.height)}>{children}</View>
127
+ </Transition>
128
+ )
142
129
  );
143
130
  AnimatedToast.displayName = 'AnimatedToast';
@@ -3,6 +3,7 @@ import { View } from 'react-native';
3
3
  import { AnimatedToast, Toast, type ToastVariant } from './Toast';
4
4
  import { useInsets } from '../theme/UIProvider';
5
5
  import { createStyles } from '../theme/createStyles';
6
+ import { createImperativeBridge } from './imperativeBridge';
6
7
 
7
8
  export interface ToastOptions {
8
9
  title: string;
@@ -26,6 +27,24 @@ interface ToastContextValue {
26
27
  }
27
28
 
28
29
  const ToastContext = createContext<ToastContextValue | undefined>(undefined);
30
+ const bridge = createImperativeBridge<ToastContextValue>('ToastProvider');
31
+
32
+ /**
33
+ * Muestra un toast desde fuera del árbol de React —un servicio, un
34
+ * interceptor— sin pasar por el hook. Devuelve el id, o '' si no hay
35
+ * <ToastProvider> montado.
36
+ */
37
+ export function showToast(options: ToastOptions): string {
38
+ return bridge.get()?.show(options) ?? '';
39
+ }
40
+
41
+ export function hideToast(id: string): void {
42
+ bridge.get()?.hide(id);
43
+ }
44
+
45
+ export function hideAllToasts(): void {
46
+ bridge.get()?.hideAll();
47
+ }
29
48
 
30
49
  const useStyles = createStyles((theme) => ({
31
50
  host: { position: 'absolute', left: 0, right: 0, paddingHorizontal: theme.tokens.spacing.lg, gap: theme.tokens.spacing.sm },
@@ -122,6 +141,11 @@ export function ToastProvider({
122
141
 
123
142
  const value = useMemo<ToastContextValue>(() => ({ show, hide, hideAll }), [show, hide, hideAll]);
124
143
 
144
+ useEffect(() => {
145
+ bridge.set(value);
146
+ return () => bridge.set(null);
147
+ }, [value]);
148
+
125
149
  const hostPosition =
126
150
  position === 'top'
127
151
  ? { top: insets.top + 8 }
@@ -1,8 +1,9 @@
1
- import React, { useCallback, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { ScrollView, View, type StyleProp, type ViewStyle } from 'react-native';
3
3
  import { createStyles } from '../theme/createStyles';
4
4
  import { Steps, type StepItem } from '../molecules/Steps';
5
5
  import { Button } from '../atoms/Button';
6
+ import { Transition } from '../atoms/Transition';
6
7
 
7
8
  export interface WizardStep extends StepItem {
8
9
  content: React.ReactNode;
@@ -31,6 +32,12 @@ export interface WizardProps {
31
32
  * @default true
32
33
  */
33
34
  scrollableContent?: boolean;
35
+ /**
36
+ * El contenido del paso entra deslizándose en el sentido de la navegación.
37
+ * Sólo opacity y transform, así que la animación corre en el hilo nativo.
38
+ * @default true
39
+ */
40
+ animated?: boolean;
34
41
  style?: StyleProp<ViewStyle>;
35
42
  }
36
43
 
@@ -61,6 +68,7 @@ export function Wizard({
61
68
  nextLabel = 'Next',
62
69
  finishLabel = 'Finish',
63
70
  scrollableContent = true,
71
+ animated = true,
64
72
  style,
65
73
  }: WizardProps) {
66
74
  const styles = useStyles();
@@ -82,6 +90,15 @@ export function Wizard({
82
90
  [steps]
83
91
  );
84
92
 
93
+ // El ref todavía guarda el paso anterior durante el render que trae el nuevo.
94
+ const previous = useRef(current);
95
+ const forward = current >= previous.current;
96
+ const entered = useRef(false);
97
+ useEffect(() => {
98
+ previous.current = current;
99
+ entered.current = true;
100
+ }, [current]);
101
+
85
102
  const activeStep = steps[current];
86
103
  const isFirst = current === 0;
87
104
  const isLast = current === steps.length - 1;
@@ -92,6 +109,22 @@ export function Wizard({
92
109
  else goTo(current + 1);
93
110
  };
94
111
 
112
+ // El paso saliente no se anima: mantener los dos montados a la vez duplicaría
113
+ // los formularios y su estado.
114
+ const renderContent = () =>
115
+ animated ? (
116
+ <Transition
117
+ key={current}
118
+ testID="wizard-step"
119
+ preset={['fade', forward ? 'slide-left' : 'slide-right']}
120
+ appear={entered.current}
121
+ >
122
+ {activeStep?.content}
123
+ </Transition>
124
+ ) : (
125
+ activeStep?.content
126
+ );
127
+
95
128
  return (
96
129
  <View style={[styles.container, style]}>
97
130
  <Steps
@@ -109,16 +142,17 @@ export function Wizard({
109
142
  de los campos. */}
110
143
  {scrollableContent ? (
111
144
  <ScrollView
145
+ testID="wizard-content"
112
146
  style={styles.content}
113
147
  contentContainerStyle={styles.contentInner}
114
148
  keyboardShouldPersistTaps="handled"
115
149
  automaticallyAdjustKeyboardInsets
116
150
  showsVerticalScrollIndicator={false}
117
151
  >
118
- {activeStep?.content}
152
+ {renderContent()}
119
153
  </ScrollView>
120
154
  ) : (
121
- <View style={styles.content}>{activeStep?.content}</View>
155
+ <View style={styles.content}>{renderContent()}</View>
122
156
  )}
123
157
 
124
158
  <View style={styles.footer}>
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Puente entre el proveedor montado y el código que no es un componente
3
+ * (servicios, capa de API, interceptores). El proveedor se registra al montar y
4
+ * se borra al desmontar, así que fuera del árbol nunca queda una referencia
5
+ * viva a un contexto muerto.
6
+ */
7
+ export interface ImperativeBridge<T> {
8
+ set: (value: T | null) => void;
9
+ get: () => T | null;
10
+ }
11
+
12
+ export function createImperativeBridge<T>(providerName: string): ImperativeBridge<T> {
13
+ let current: T | null = null;
14
+
15
+ return {
16
+ set: (value) => {
17
+ current = value;
18
+ },
19
+ get: () => {
20
+ if (!current && __DEV__) {
21
+ console.warn(
22
+ `orn-ui: se llamó a la API imperativa sin un <${providerName}> montado. La llamada se ignoró.`
23
+ );
24
+ }
25
+ return current;
26
+ },
27
+ };
28
+ }
@@ -1,12 +1,12 @@
1
1
  export { Modal } from './Modal';
2
2
  export type { ModalProps, ModalVariant } from './Modal';
3
3
  export { BottomSheet } from './BottomSheet';
4
- export type { BottomSheetProps } from './BottomSheet';
4
+ export type { BottomSheetProps, FooterPlacement } from './BottomSheet';
5
5
  export { Select } from './Select';
6
6
  export type { SelectProps, SelectOption } from './Select';
7
7
  export { Alert } from './Alert';
8
8
  export type { AlertProps, AlertButton, AlertType } from './Alert';
9
- export { AlertProvider, useAlert } from './AlertProvider';
9
+ export { AlertProvider, useAlert, showAlert, showConfirm, hideAlert } from './AlertProvider';
10
10
  export type { AlertOptions, ConfirmOptions } from './AlertProvider';
11
11
  export { Screen } from './Screen';
12
12
  export type { ScreenProps, ScreenEdge } from './Screen';
@@ -16,7 +16,7 @@ export { SearchList } from './SearchList';
16
16
  export type { SearchListProps } from './SearchList';
17
17
  export { Toast } from './Toast';
18
18
  export type { ToastProps, ToastVariant } from './Toast';
19
- export { ToastProvider, useToast } from './ToastProvider';
19
+ export { ToastProvider, useToast, showToast, hideToast, hideAllToasts } from './ToastProvider';
20
20
  export type { ToastProviderProps, ToastOptions } from './ToastProvider';
21
21
  export { DatePicker, buildMonthGrid, DEFAULT_MONTH_NAMES, DEFAULT_WEEKDAY_NAMES } from './DatePicker';
22
22
  export type { DatePickerProps, DateRange } from './DatePicker';
@@ -24,3 +24,7 @@ export { DateField } from './DateField';
24
24
  export type { DateFieldProps } from './DateField';
25
25
  export { Wizard } from './Wizard';
26
26
  export type { WizardProps, WizardStep } from './Wizard';
27
+ export { ThemeToggle } from './ThemeToggle';
28
+ export type { ThemeToggleProps } from './ThemeToggle';
29
+ export { NavigationBar } from './NavigationBar';
30
+ export type { NavigationBarProps, NavigationBarItem } from './NavigationBar';