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,22 +1,29 @@
1
1
  import React, { memo } from 'react';
2
- import { Text, TouchableOpacity, type StyleProp, type ViewStyle } from 'react-native';
2
+ import { 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
5
  import { Icon } from '../icons/Icon';
6
+ import { PressableScale } from '../atoms/PressableScale';
6
7
  import type { IconName } from '../icons/types';
7
8
 
8
9
  export interface OptionCardProps {
9
10
  label: string;
10
- iconName: IconName;
11
+ /** Línea secundaria: el detalle que decide la elección ("llega en 3 días"). */
12
+ description?: string;
13
+ iconName?: IconName;
11
14
  isSelected: boolean;
12
15
  onPress: () => void;
13
- /** 'vertical': ícono arriba, texto abajo centrado. 'horizontal': ícono y texto en fila. */
16
+ disabled?: boolean;
17
+ /** 'vertical': ícono arriba, texto abajo centrado. 'horizontal': en fila. @default 'horizontal' */
14
18
  layout?: 'vertical' | 'horizontal';
15
19
  style?: StyleProp<ViewStyle>;
20
+ testID?: string;
16
21
  }
17
22
 
18
23
  const useStyles = createStyles((theme) => ({
19
24
  card: {
25
+ // Las tarjetas se reparten la fila que las contenga, sean dos o cinco.
26
+ flex: 1,
20
27
  alignItems: 'center',
21
28
  justifyContent: 'center',
22
29
  backgroundColor: theme.colors.surface,
@@ -25,57 +32,87 @@ const useStyles = createStyles((theme) => ({
25
32
  borderColor: theme.colors.border,
26
33
  ...theme.tokens.shadow.sm,
27
34
  },
28
- cardHorizontal: { flex: 1, flexDirection: 'row', padding: theme.tokens.spacing.md - 2, gap: theme.tokens.spacing.sm },
29
- cardVertical: { width: '31%', padding: theme.tokens.spacing.sm, gap: 5, minHeight: 100 },
35
+ cardHorizontal: { flexDirection: 'row', padding: theme.tokens.spacing.md - 2, gap: theme.tokens.spacing.sm },
36
+ cardVertical: { padding: theme.tokens.spacing.md - 2, gap: 6, minHeight: 100 },
30
37
  cardSelected: {
31
38
  borderColor: theme.colors.primary,
32
- backgroundColor: theme.colors.primary + '15',
39
+ backgroundColor: theme.colors.primarySoft,
33
40
  borderWidth: 1.5,
34
41
  elevation: 0,
35
42
  shadowOpacity: 0,
36
43
  },
44
+ cardDisabled: { opacity: 0.5 },
45
+ text: { flexShrink: 1 },
46
+ textVertical: { alignItems: 'center' },
37
47
  label: { color: theme.colors.textLight, fontWeight: theme.tokens.fontWeight.medium },
38
48
  labelVertical: { fontSize: theme.tokens.fontSize.xs, textAlign: 'center' },
39
- labelSelected: { color: theme.colors.primary },
49
+ labelSelected: { color: theme.colors.primaryText },
50
+ description: { fontSize: theme.tokens.fontSize.xs, color: theme.colors.textLight, marginTop: 2 },
51
+ descriptionVertical: { textAlign: 'center' },
40
52
  }));
41
53
 
42
54
  /**
43
55
  * OptionCard
44
- * Card con ícono+label que resalta con borde/fondo primary cuando
45
- * `isSelected`. Para elegir entre opciones excluyentes (forma de pago, tipo
46
- * de documento, etc).
56
+ * Opción excluyente con ícono y label que resalta cuando `isSelected`. Es un
57
+ * radio con cara de tarjeta, así que se anuncia como tal: envolver el grupo en
58
+ * una View con `accessibilityRole="radiogroup"` para que el lector de pantalla
59
+ * lea "1 de 3".
47
60
  */
48
61
  export const OptionCard = memo(
49
- ({ label, iconName, isSelected, onPress, layout = 'horizontal', style }: OptionCardProps) => {
62
+ ({
63
+ label,
64
+ description,
65
+ iconName,
66
+ isSelected,
67
+ onPress,
68
+ disabled = false,
69
+ layout = 'horizontal',
70
+ style,
71
+ testID,
72
+ }: OptionCardProps) => {
50
73
  const colors = useColors();
51
74
  const styles = useStyles();
52
75
  const allowFontScaling = useAllowFontScaling();
76
+ const isVertical = layout === 'vertical';
53
77
 
78
+ // Feedback por transform y no por opacity: con elevation, animar la opacity
79
+ // dispara un bug de Android que redibuja la sombra como un borde oscuro.
54
80
  return (
55
- <TouchableOpacity
56
- // activeOpacity=1: esta tarjeta tiene elevation (sombra Android); animar su
57
- // propia opacity en el press dispara un bug de Android que redibuja la
58
- // sombra como un borde oscuro pegado al borde del componente.
59
- activeOpacity={1}
81
+ <PressableScale
60
82
  style={[
61
83
  styles.card,
62
- layout === 'vertical' ? styles.cardVertical : styles.cardHorizontal,
84
+ isVertical ? styles.cardVertical : styles.cardHorizontal,
63
85
  isSelected && styles.cardSelected,
86
+ disabled && styles.cardDisabled,
64
87
  style,
65
88
  ]}
66
89
  onPress={onPress}
67
- accessibilityRole="button"
68
- accessibilityState={{ selected: isSelected }}
90
+ disabled={disabled}
91
+ accessibilityRole="radio"
92
+ accessibilityState={{ selected: isSelected, checked: isSelected, disabled }}
69
93
  accessibilityLabel={label}
94
+ accessibilityHint={description}
95
+ testID={testID}
70
96
  >
71
- <Icon name={iconName} size={24} color={isSelected ? colors.primary : colors.textLight} />
72
- <Text
73
- allowFontScaling={allowFontScaling}
74
- style={[styles.label, layout === 'vertical' && styles.labelVertical, isSelected && styles.labelSelected]}
75
- >
76
- {label}
77
- </Text>
78
- </TouchableOpacity>
97
+ {!!iconName && <Icon name={iconName} size={24} color={isSelected ? colors.primary : colors.textLight} />}
98
+
99
+ <View style={[styles.text, isVertical && styles.textVertical]}>
100
+ <Text
101
+ allowFontScaling={allowFontScaling}
102
+ style={[styles.label, isVertical && styles.labelVertical, isSelected && styles.labelSelected]}
103
+ >
104
+ {label}
105
+ </Text>
106
+ {!!description && (
107
+ <Text
108
+ allowFontScaling={allowFontScaling}
109
+ style={[styles.description, isVertical && styles.descriptionVertical]}
110
+ >
111
+ {description}
112
+ </Text>
113
+ )}
114
+ </View>
115
+ </PressableScale>
79
116
  );
80
117
  }
81
118
  );
@@ -0,0 +1,84 @@
1
+ import React, { memo } from 'react';
2
+ import { Text, TouchableOpacity, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { createStyles } from '../theme/createStyles';
4
+ import { useAllowFontScaling } from '../theme/UIProvider';
5
+
6
+ export interface SegmentedControlOption<T extends string = string> {
7
+ value: T;
8
+ label: string;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ export interface SegmentedControlProps<T extends string = string> {
13
+ options: SegmentedControlOption<T>[];
14
+ value: T;
15
+ onChange: (value: T) => void;
16
+ disabled?: boolean;
17
+ style?: StyleProp<ViewStyle>;
18
+ testID?: string;
19
+ }
20
+
21
+ const useStyles = createStyles((theme) => ({
22
+ track: {
23
+ flexDirection: 'row',
24
+ backgroundColor: theme.colors.inputBackground,
25
+ borderRadius: theme.tokens.radius.md,
26
+ padding: 3,
27
+ },
28
+ segment: {
29
+ flex: 1,
30
+ paddingVertical: theme.tokens.spacing.sm,
31
+ borderRadius: theme.tokens.radius.sm,
32
+ alignItems: 'center',
33
+ },
34
+ segmentActive: { backgroundColor: theme.colors.surface, ...theme.tokens.shadow.sm },
35
+ segmentDisabled: { opacity: 0.5 },
36
+ label: { fontSize: theme.tokens.fontSize.sm, fontWeight: theme.tokens.fontWeight.medium, color: theme.colors.textLight },
37
+ labelActive: { color: theme.colors.primaryText, fontWeight: theme.tokens.fontWeight.semibold },
38
+ }));
39
+
40
+ /**
41
+ * SegmentedControl
42
+ * Elige una opción entre pocas, todas visibles. No conoce ningún estado
43
+ * global: recibe `value` y avisa por `onChange`.
44
+ */
45
+ function SegmentedControlInner<T extends string>({
46
+ options,
47
+ value,
48
+ onChange,
49
+ disabled = false,
50
+ style,
51
+ testID,
52
+ }: SegmentedControlProps<T>) {
53
+ const styles = useStyles();
54
+ const allowFontScaling = useAllowFontScaling();
55
+
56
+ return (
57
+ <View style={[styles.track, style]} accessibilityRole="radiogroup" testID={testID}>
58
+ {options.map((option) => {
59
+ const active = option.value === value;
60
+ const isDisabled = disabled || !!option.disabled;
61
+ return (
62
+ <TouchableOpacity
63
+ key={option.value}
64
+ style={[styles.segment, active && styles.segmentActive, isDisabled && styles.segmentDisabled]}
65
+ onPress={() => onChange(option.value)}
66
+ disabled={isDisabled}
67
+ accessibilityRole="radio"
68
+ accessibilityState={{ selected: active, checked: active, disabled: isDisabled }}
69
+ accessibilityLabel={option.label}
70
+ testID={testID && `${testID}-${option.value}`}
71
+ >
72
+ <Text allowFontScaling={allowFontScaling} style={[styles.label, active && styles.labelActive]}>
73
+ {option.label}
74
+ </Text>
75
+ </TouchableOpacity>
76
+ );
77
+ })}
78
+ </View>
79
+ );
80
+ }
81
+
82
+ // memo() pierde los genéricos al envolver: el cast los devuelve para que
83
+ // `value` y `onChange` sigan atados al union de `options`.
84
+ export const SegmentedControl = memo(SegmentedControlInner) as typeof SegmentedControlInner;
@@ -1,29 +1,89 @@
1
- import React, { memo } from 'react';
1
+ import React, { memo, useState } from 'react';
2
2
  import { TextInput, TouchableOpacity, View, type StyleProp, type ViewStyle } from 'react-native';
3
3
  import { createStyles } from '../theme/createStyles';
4
- import { useAllowFontScaling, useColors } from '../theme/UIProvider';
4
+ import { useAllowFontScaling, useColors, useTheme } from '../theme/UIProvider';
5
5
  import { Icon } from '../icons/Icon';
6
6
 
7
+ export type StepperSize = 'sm' | 'md' | 'lg';
8
+
7
9
  export interface StepperProps {
8
10
  value: string;
9
11
  onChangeText: (text: string) => void;
10
12
  onIncrement: () => void;
11
13
  onDecrement: () => void;
12
- size?: 'sm' | 'md';
14
+ /** Escala del control: alto, botones, ícono y tipografía. @default 'md' */
15
+ size?: StepperSize;
16
+ /** Ocupa todo el ancho disponible y el input se lleva el sobrante. @default false */
17
+ block?: boolean;
18
+ /**
19
+ * En false el valor sólo se cambia con los botones: el teclado no se abre y
20
+ * el campo pierde el fondo que lo anuncia como editable. @default true
21
+ */
22
+ editable?: boolean;
23
+ /**
24
+ * Mínimo permitido. Deshabilita "-" al alcanzarlo y acota lo tipeado al
25
+ * salir del campo. Los negativos se aceptan sólo si `min` es negativo.
26
+ */
27
+ min?: number;
28
+ /** Máximo permitido. Deshabilita "+" al alcanzarlo y acota lo tipeado. */
29
+ max?: number;
30
+ /** Acepta un separador decimal, normalizando la coma a punto. @default false */
31
+ allowDecimals?: boolean;
13
32
  style?: StyleProp<ViewStyle>;
14
33
  incrementAccessibilityLabel?: string;
15
34
  decrementAccessibilityLabel?: string;
35
+ testID?: string;
16
36
  }
17
37
 
38
+ const SIZES: Record<StepperSize, { height: number; icon: number; inputWidth: number }> = {
39
+ sm: { height: 36, icon: 18, inputWidth: 48 },
40
+ md: { height: 44, icon: 20, inputWidth: 56 },
41
+ lg: { height: 52, icon: 22, inputWidth: 64 },
42
+ };
43
+
18
44
  const useStyles = createStyles((theme) => ({
19
- control: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderColor: theme.colors.border, borderRadius: theme.tokens.radius.md },
20
- btn: { height: '100%', justifyContent: 'center', alignItems: 'center', backgroundColor: theme.colors.inputBackground },
21
- btnSm: { backgroundColor: 'transparent' },
22
- input: { flex: 1, textAlign: 'center', fontSize: theme.tokens.fontSize.lg, color: theme.colors.text, height: '100%' },
23
- inputSm: { flex: 0, width: 50, borderLeftWidth: 1, borderRightWidth: 1, borderColor: theme.colors.border },
45
+ control: {
46
+ flexDirection: 'row',
47
+ alignItems: 'center',
48
+ alignSelf: 'flex-start',
49
+ borderWidth: 1,
50
+ borderColor: theme.colors.border,
51
+ borderRadius: theme.tokens.radius.md,
52
+ },
53
+ controlBlock: { alignSelf: 'stretch' },
54
+ btn: { height: '100%', justifyContent: 'center', alignItems: 'center' },
55
+ input: {
56
+ textAlign: 'center',
57
+ height: '100%',
58
+ color: theme.colors.text,
59
+ borderLeftWidth: 1,
60
+ borderRightWidth: 1,
61
+ borderColor: theme.colors.border,
62
+ },
63
+ // El fondo distingue el campo de un valor de sólo lectura: es la única
64
+ // señal de que ahí se puede tipear antes de tocarlo.
65
+ editable: { backgroundColor: theme.colors.inputBackground },
24
66
  }));
25
67
 
26
- /** Stepper: botón "-" + input numérico editable + botón "+". */
68
+ /** Deja sólo lo que puede formar parte del número permitido por el rango. */
69
+ export function sanitizeNumeric(text: string, { allowDecimals = false, allowNegative = false } = {}): string {
70
+ const normalized = allowDecimals ? text.replace(',', '.') : text;
71
+ const negative = allowNegative && normalized.trimStart().startsWith('-');
72
+ const digits = normalized.replace(allowDecimals ? /[^0-9.]/g : /[^0-9]/g, '');
73
+ const [whole = '', ...decimals] = digits.split('.');
74
+ const body = decimals.length > 0 ? `${whole}.${decimals.join('')}` : whole;
75
+ return negative ? `-${body}` : body;
76
+ }
77
+
78
+ /**
79
+ * Stepper
80
+ * Botón "-" + input numérico editable + botón "+". `size` controla sólo la
81
+ * escala y `block` sólo el ancho, así que los dos ejes se combinan libremente.
82
+ *
83
+ * El componente no hace la aritmética de los botones —eso vive en
84
+ * `onIncrement`/`onDecrement`—, pero sí limpia lo que se tipea y avisa cuándo
85
+ * el valor llegó a un extremo del rango deshabilitando el botón que lo pasaría.
86
+ */
27
87
  export const Stepper = memo(
28
88
  ({
29
89
  value,
@@ -31,44 +91,99 @@ export const Stepper = memo(
31
91
  onIncrement,
32
92
  onDecrement,
33
93
  size = 'md',
94
+ block = false,
95
+ editable = true,
96
+ min,
97
+ max,
98
+ allowDecimals = false,
34
99
  style,
35
100
  incrementAccessibilityLabel = 'Increment',
36
101
  decrementAccessibilityLabel = 'Decrement',
102
+ testID,
37
103
  }: StepperProps) => {
104
+ const theme = useTheme();
38
105
  const colors = useColors();
39
106
  const styles = useStyles();
40
107
  const allowFontScaling = useAllowFontScaling();
41
- const isSmall = size === 'sm';
42
- const btnSize = isSmall ? 36 : 48;
43
- const iconSize = isSmall ? 18 : 20;
108
+ const [focused, setFocused] = useState(false);
109
+
110
+ const { height, icon, inputWidth } = SIZES[size];
111
+ const fontSize =
112
+ size === 'sm' ? theme.tokens.fontSize.md : size === 'md' ? theme.tokens.fontSize.lg : theme.tokens.fontSize.xl;
113
+
114
+ const allowNegative = min !== undefined && min < 0;
115
+ const numeric = Number(value);
116
+ const isNumber = value.trim() !== '' && Number.isFinite(numeric);
117
+ const atMin = isNumber && min !== undefined && numeric <= min;
118
+ const atMax = isNumber && max !== undefined && numeric >= max;
119
+
120
+ const handleChangeText = (text: string) => {
121
+ onChangeText(sanitizeNumeric(text, { allowDecimals, allowNegative }));
122
+ };
123
+
124
+ // El recorte al rango espera al blur: hacerlo mientras se tipea impide
125
+ // llegar a "12" cuando el mínimo es 5, porque el "1" ya se corregiría.
126
+ const handleBlur = () => {
127
+ setFocused(false);
128
+ if (!isNumber) return;
129
+ const clamped = Math.min(max ?? numeric, Math.max(min ?? numeric, numeric));
130
+ if (clamped !== numeric) onChangeText(String(clamped));
131
+ };
132
+
133
+ const stepButton = (
134
+ direction: 'decrement' | 'increment',
135
+ onPress: () => void,
136
+ disabled: boolean,
137
+ label: string
138
+ ) => (
139
+ <TouchableOpacity
140
+ onPress={onPress}
141
+ disabled={disabled}
142
+ style={[styles.btn, { width: height }]}
143
+ accessibilityRole="button"
144
+ accessibilityLabel={label}
145
+ accessibilityState={{ disabled }}
146
+ testID={testID && `${testID}-${direction}`}
147
+ >
148
+ <Icon
149
+ name={direction === 'increment' ? 'plus' : 'minus'}
150
+ size={icon}
151
+ color={disabled ? colors.textLight : colors.primary}
152
+ />
153
+ </TouchableOpacity>
154
+ );
44
155
 
45
156
  return (
46
- <View style={[styles.control, { height: isSmall ? 36 : 48 }, style]}>
47
- <TouchableOpacity
48
- onPress={onDecrement}
49
- style={[styles.btn, { width: btnSize }, isSmall && styles.btnSm]}
50
- accessibilityRole="button"
51
- accessibilityLabel={decrementAccessibilityLabel}
52
- >
53
- <Icon name="minus" size={iconSize} color={colors.primary} />
54
- </TouchableOpacity>
157
+ <View
158
+ testID={testID}
159
+ style={[
160
+ styles.control,
161
+ { height },
162
+ block && styles.controlBlock,
163
+ focused && { borderColor: colors.primary },
164
+ style,
165
+ ]}
166
+ >
167
+ {stepButton('decrement', onDecrement, atMin, decrementAccessibilityLabel)}
55
168
  <TextInput
56
169
  allowFontScaling={allowFontScaling}
57
- style={[styles.input, isSmall && styles.inputSm]}
170
+ style={[
171
+ styles.input,
172
+ { fontSize },
173
+ editable && styles.editable,
174
+ focused && { borderColor: colors.primary },
175
+ block ? { flex: 1 } : { width: inputWidth },
176
+ ]}
58
177
  value={value}
59
- onChangeText={onChangeText}
60
- keyboardType="numeric"
178
+ onChangeText={handleChangeText}
179
+ onFocus={() => setFocused(true)}
180
+ onBlur={handleBlur}
181
+ editable={editable}
182
+ keyboardType={allowDecimals ? 'decimal-pad' : 'number-pad'}
61
183
  selectTextOnFocus
62
184
  accessibilityLabel="Quantity"
63
185
  />
64
- <TouchableOpacity
65
- onPress={onIncrement}
66
- style={[styles.btn, { width: btnSize }, isSmall && styles.btnSm]}
67
- accessibilityRole="button"
68
- accessibilityLabel={incrementAccessibilityLabel}
69
- >
70
- <Icon name="plus" size={iconSize} color={colors.primary} />
71
- </TouchableOpacity>
186
+ {stepButton('increment', onIncrement, atMax, incrementAccessibilityLabel)}
72
187
  </View>
73
188
  );
74
189
  }
@@ -82,8 +82,8 @@ export const Steps = memo(
82
82
  const isVertical = orientation === 'vertical';
83
83
 
84
84
  const colorsFor = (status: StepStatus) => {
85
- if (status === 'completed') return { bg: colors.primary, border: colors.primary, fg: colors.white };
86
- if (status === 'current') return { bg: 'transparent', border: colors.primary, fg: colors.primary };
85
+ if (status === 'completed') return { bg: colors.primary, border: colors.primary, fg: colors.onPrimary };
86
+ if (status === 'current') return { bg: 'transparent', border: colors.primary, fg: colors.primaryText };
87
87
  return { bg: 'transparent', border: colors.border, fg: colors.textLight };
88
88
  };
89
89
 
@@ -1,5 +1,5 @@
1
- export { Stepper } from './Stepper';
2
- export type { StepperProps } from './Stepper';
1
+ export { Stepper, sanitizeNumeric } from './Stepper';
2
+ export type { StepperProps, StepperSize } from './Stepper';
3
3
  export { OptionCard } from './OptionCard';
4
4
  export type { OptionCardProps } from './OptionCard';
5
5
  export { InfoRow } from './InfoRow';
@@ -8,7 +8,7 @@ export { FormActions } from './FormActions';
8
8
  export type { FormActionsProps } from './FormActions';
9
9
  export { AvatarHeader } from './AvatarHeader';
10
10
  export type { AvatarHeaderProps } from './AvatarHeader';
11
- export { ThemeToggle } from './ThemeToggle';
12
- export type { ThemeToggleProps } from './ThemeToggle';
11
+ export { SegmentedControl } from './SegmentedControl';
12
+ export type { SegmentedControlProps, SegmentedControlOption } from './SegmentedControl';
13
13
  export { Steps } from './Steps';
14
14
  export type { StepsProps, StepItem, StepStatus } from './Steps';
@@ -91,11 +91,12 @@ export const Alert = memo((props: AlertProps) => {
91
91
  if (!visible) return null;
92
92
 
93
93
  const iconColor: Record<AlertType, string> = {
94
- success: colors.success,
95
- error: colors.error,
96
- warning: colors.warning,
97
- question: colors.primary,
98
- info: colors.secondary,
94
+ // Íconos sobre la superficie del diálogo: rol `*Text`, no el acento pleno.
95
+ success: colors.successText,
96
+ error: colors.errorText,
97
+ warning: colors.warningText,
98
+ question: colors.primaryText,
99
+ info: colors.secondaryText,
99
100
  };
100
101
 
101
102
  const effectiveButtons = resolveButtons(props, { confirm: labels.confirm, cancel: labels.cancel, close: labels.close });
@@ -1,5 +1,6 @@
1
- import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
1
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { Alert, type AlertButton, type AlertType } from './Alert';
3
+ import { createImperativeBridge } from './imperativeBridge';
3
4
 
4
5
  export interface AlertOptions {
5
6
  title: string;
@@ -23,6 +24,25 @@ interface AlertContextValue {
23
24
  }
24
25
 
25
26
  const AlertContext = createContext<AlertContextValue | undefined>(undefined);
27
+ const bridge = createImperativeBridge<AlertContextValue>('AlertProvider');
28
+
29
+ /**
30
+ * Abre un alert desde fuera del árbol de React —un servicio, un interceptor—
31
+ * sin pasar por el hook. Sin <AlertProvider> montado resuelve de inmediato, así
32
+ * que un `await` nunca queda colgado.
33
+ */
34
+ export function showAlert(options: AlertOptions): Promise<void> {
35
+ return bridge.get()?.alert(options) ?? Promise.resolve();
36
+ }
37
+
38
+ /** Igual que showAlert, pero resuelve true/false. Sin provider, false. */
39
+ export function showConfirm(options: ConfirmOptions): Promise<boolean> {
40
+ return bridge.get()?.confirm(options) ?? Promise.resolve(false);
41
+ }
42
+
43
+ export function hideAlert(): void {
44
+ bridge.get()?.hide();
45
+ }
26
46
 
27
47
  interface InternalState {
28
48
  visible: boolean;
@@ -108,6 +128,11 @@ export function AlertProvider({ children }: { children: React.ReactNode }) {
108
128
 
109
129
  const value = useMemo<AlertContextValue>(() => ({ alert: alertFn, confirm: confirmFn, hide }), [alertFn, confirmFn, hide]);
110
130
 
131
+ useEffect(() => {
132
+ bridge.set(value);
133
+ return () => bridge.set(null);
134
+ }, [value]);
135
+
111
136
  return (
112
137
  <AlertContext.Provider value={value}>
113
138
  {children}