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
@@ -0,0 +1,114 @@
1
+ import React, { memo, useEffect, useRef } from 'react';
2
+ import {
3
+ Animated,
4
+ Easing,
5
+ View,
6
+ type DimensionValue,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+ import { createStyles } from '../theme/createStyles';
11
+ import { useTheme } from '../theme/UIProvider';
12
+
13
+ export type SkeletonVariant = 'rect' | 'text' | 'circle';
14
+
15
+ export interface SkeletonProps {
16
+ /** @default '100%' ('circle' lo usa también como diámetro) */
17
+ width?: DimensionValue;
18
+ /** @default 16 ('text' usa el alto de línea; 'circle', el ancho) */
19
+ height?: DimensionValue;
20
+ /** @default 'rect' */
21
+ variant?: SkeletonVariant;
22
+ /** Override del radius del theme. */
23
+ radius?: number;
24
+ /** Líneas apiladas, sólo para variant="text". @default 1 */
25
+ lines?: number;
26
+ /** Ancho de la última línea, para que el párrafo no termine en bloque. @default '60%' */
27
+ lastLineWidth?: DimensionValue;
28
+ /** @default true */
29
+ animated?: boolean;
30
+ style?: StyleProp<ViewStyle>;
31
+ testID?: string;
32
+ }
33
+
34
+ const useStyles = createStyles((theme) => ({
35
+ block: { backgroundColor: theme.colors.skeleton },
36
+ lines: { gap: theme.tokens.spacing.sm },
37
+ }));
38
+
39
+ /**
40
+ * Skeleton
41
+ * Placeholder que late mientras llega el contenido real. Reserva el espacio
42
+ * exacto de lo que reemplaza, así la pantalla no salta cuando aparecen los
43
+ * datos — que es justamente lo que un Spinner centrado no puede hacer.
44
+ */
45
+ export const Skeleton = memo(
46
+ ({
47
+ width = '100%',
48
+ height = 16,
49
+ variant = 'rect',
50
+ radius,
51
+ lines = 1,
52
+ lastLineWidth = '60%',
53
+ animated = true,
54
+ style,
55
+ testID,
56
+ }: SkeletonProps) => {
57
+ const theme = useTheme();
58
+ const styles = useStyles();
59
+ const pulse = useRef(new Animated.Value(0)).current;
60
+
61
+ useEffect(() => {
62
+ if (!animated) return;
63
+ const loop = Animated.loop(
64
+ Animated.sequence([
65
+ Animated.timing(pulse, {
66
+ toValue: 1,
67
+ duration: 700,
68
+ useNativeDriver: true,
69
+ easing: Easing.inOut(Easing.quad),
70
+ }),
71
+ Animated.timing(pulse, {
72
+ toValue: 0,
73
+ duration: 700,
74
+ useNativeDriver: true,
75
+ easing: Easing.inOut(Easing.quad),
76
+ }),
77
+ ])
78
+ );
79
+ loop.start();
80
+ return () => loop.stop();
81
+ }, [animated, pulse]);
82
+
83
+ const isCircle = variant === 'circle';
84
+ const isText = variant === 'text';
85
+ const opacity = animated ? pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 0.4] }) : 1;
86
+ const resolvedHeight = isCircle ? width : isText ? theme.tokens.fontSize.md : height;
87
+ const resolvedRadius =
88
+ radius ?? (isCircle ? theme.tokens.radius.full : isText ? theme.tokens.radius.xs : theme.tokens.radius.sm);
89
+
90
+ const barStyle = (barWidth: DimensionValue): StyleProp<ViewStyle> => [
91
+ styles.block,
92
+ { width: barWidth, height: resolvedHeight, borderRadius: resolvedRadius, opacity },
93
+ ];
94
+
95
+ const a11y = { accessibilityElementsHidden: true, importantForAccessibility: 'no-hide-descendants' as const };
96
+
97
+ if (isText && lines > 1) {
98
+ return (
99
+ <View style={[styles.lines, style]} testID={testID} {...a11y}>
100
+ {Array.from({ length: lines }, (_, i) => (
101
+ <Animated.View
102
+ key={i}
103
+ testID={testID && `${testID}-${i}`}
104
+ style={barStyle(i === lines - 1 ? lastLineWidth : width)}
105
+ />
106
+ ))}
107
+ </View>
108
+ );
109
+ }
110
+
111
+ return <Animated.View style={[barStyle(width), style]} testID={testID} {...a11y} />;
112
+ }
113
+ );
114
+ Skeleton.displayName = 'Skeleton';
@@ -0,0 +1,189 @@
1
+ import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { AccessibilityInfo, Animated, Easing, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { useTheme } from '../theme/UIProvider';
4
+
5
+ export type TransitionPreset =
6
+ | 'fade'
7
+ | 'scale'
8
+ | 'pop'
9
+ | 'slide-up'
10
+ | 'slide-down'
11
+ | 'slide-left'
12
+ | 'slide-right';
13
+
14
+ export interface TransitionProps {
15
+ children: React.ReactNode;
16
+ /** Entra en true, sale (y desmonta) en false. @default true */
17
+ visible?: boolean;
18
+ /** Uno o varios: 'fade scale' combina opacidad y escala. @default 'fade' */
19
+ preset?: TransitionPreset | TransitionPreset[];
20
+ /** ms del timing. @default tokens.duration.base */
21
+ duration?: number;
22
+ /** ms antes de arrancar. Un índice por delay escalona una lista. @default 0 */
23
+ delay?: number;
24
+ /** Píxeles que recorren los presets de slide. @default 24 */
25
+ distance?: number;
26
+ /** Spring en vez de timing (implícito en 'pop'). */
27
+ spring?: boolean;
28
+ /** Anima también el primer render. En false arranca ya presente. @default true */
29
+ appear?: boolean;
30
+ /** Deja el nodo montado al salir, sólo lo esconde. @default false */
31
+ keepMounted?: boolean;
32
+ /**
33
+ * Progreso 0→1 manejado desde afuera. Con esto el Transition sólo interpola:
34
+ * quien lo pasa decide cuándo y cómo avanza (un PanResponder, por ejemplo).
35
+ */
36
+ value?: Animated.Value;
37
+ onEntered?: () => void;
38
+ onExited?: () => void;
39
+ style?: StyleProp<ViewStyle>;
40
+ testID?: string;
41
+ }
42
+
43
+ const SCALE_FROM: Record<'scale' | 'pop', number> = { scale: 0.92, pop: 0.8 };
44
+
45
+ /**
46
+ * Sigue el ajuste de "reducir movimiento" del sistema: con él activo las
47
+ * animaciones saltan al estado final en vez de desactivarse, así que los
48
+ * callbacks y el desmontaje siguen ocurriendo igual.
49
+ */
50
+ export function useReduceMotion(): boolean {
51
+ const [reduceMotion, setReduceMotion] = useState(false);
52
+
53
+ useEffect(() => {
54
+ let alive = true;
55
+ // Sólo se actualiza el estado si el ajuste está activo: el valor inicial ya
56
+ // es `false` y un setState de false a false costaría un render por montaje.
57
+ AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
58
+ if (alive && enabled) setReduceMotion(true);
59
+ });
60
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduceMotion);
61
+ return () => {
62
+ alive = false;
63
+ sub.remove();
64
+ };
65
+ }, []);
66
+
67
+ return reduceMotion;
68
+ }
69
+
70
+ /**
71
+ * Transition
72
+ * Envuelve cualquier árbol y lo anima al entrar y al salir. Todos los presets
73
+ * se reducen a `opacity` y `transform` sobre un único Animated.Value, que es
74
+ * lo que `useNativeDriver` sabe mandar al hilo nativo: la animación corre ahí
75
+ * y el hilo de JS no participa de ningún frame — por eso no hay presets de
76
+ * alto, ancho o color, que obligarían a volver a JS en cada frame.
77
+ *
78
+ * El nodo se mantiene montado hasta que termina la animación de salida, y el
79
+ * comportamiento es idéntico en iOS y Android: no hay ninguna rama por
80
+ * plataforma.
81
+ */
82
+ export const Transition = memo(
83
+ ({
84
+ children,
85
+ visible = true,
86
+ preset = 'fade',
87
+ duration,
88
+ delay = 0,
89
+ distance = 24,
90
+ spring,
91
+ appear = true,
92
+ keepMounted = false,
93
+ value,
94
+ onEntered,
95
+ onExited,
96
+ style,
97
+ testID,
98
+ }: TransitionProps) => {
99
+ const theme = useTheme();
100
+ const reduceMotion = useReduceMotion();
101
+ const internal = useRef(new Animated.Value(visible && !appear ? 1 : 0)).current;
102
+ const progress = value ?? internal;
103
+ const driven = value === undefined;
104
+
105
+ const [mounted, setMounted] = useState(visible || keepMounted);
106
+
107
+ // Los callbacks viven en un ref: pasados inline cambian de identidad en
108
+ // cada render y, como dependencias del efecto, reiniciarían la animación.
109
+ const callbacks = useRef({ onEntered, onExited });
110
+ useEffect(() => {
111
+ callbacks.current = { onEntered, onExited };
112
+ });
113
+
114
+ const presetKey = Array.isArray(preset) ? preset.join(' ') : preset;
115
+ const presets = useMemo(() => presetKey.split(' ') as TransitionPreset[], [presetKey]);
116
+
117
+ useEffect(() => {
118
+ if (visible) setMounted(true);
119
+ }, [visible]);
120
+
121
+ useEffect(() => {
122
+ if (!driven) return;
123
+ const toValue = visible ? 1 : 0;
124
+ const useSpring = spring ?? presets.includes('pop');
125
+
126
+ const animation = reduceMotion
127
+ ? Animated.timing(progress, { toValue, duration: 0, delay, useNativeDriver: true })
128
+ : useSpring
129
+ ? Animated.spring(progress, { toValue, delay, friction: 9, tension: 90, useNativeDriver: true })
130
+ : Animated.timing(progress, {
131
+ toValue,
132
+ delay,
133
+ duration: duration ?? theme.tokens.duration.base,
134
+ easing: visible ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic),
135
+ useNativeDriver: true,
136
+ });
137
+
138
+ animation.start(({ finished }) => {
139
+ if (!finished) return;
140
+ if (visible) {
141
+ callbacks.current.onEntered?.();
142
+ return;
143
+ }
144
+ callbacks.current.onExited?.();
145
+ if (!keepMounted) setMounted(false);
146
+ });
147
+
148
+ return () => animation.stop();
149
+ }, [visible, driven, reduceMotion, spring, delay, duration, keepMounted, presets, progress, theme]);
150
+
151
+ const animatedStyle = useMemo(() => {
152
+ type Interpolated = Animated.AnimatedInterpolation<number>;
153
+ const transform: Array<{ scale: Interpolated } | { translateX: Interpolated } | { translateY: Interpolated }> = [];
154
+ let opacity: Animated.Value | undefined;
155
+
156
+ for (const item of presets) {
157
+ if (item === 'fade') {
158
+ opacity = progress;
159
+ continue;
160
+ }
161
+ if (item === 'scale' || item === 'pop') {
162
+ transform.push({
163
+ scale: progress.interpolate({ inputRange: [0, 1], outputRange: [SCALE_FROM[item], 1] }),
164
+ });
165
+ continue;
166
+ }
167
+ const vertical = item === 'slide-up' || item === 'slide-down';
168
+ const from = item === 'slide-up' || item === 'slide-left' ? distance : -distance;
169
+ const offset = progress.interpolate({ inputRange: [0, 1], outputRange: [from, 0] });
170
+ transform.push(vertical ? { translateY: offset } : { translateX: offset });
171
+ }
172
+
173
+ return { opacity, transform };
174
+ }, [presets, progress, distance]);
175
+
176
+ if (!mounted) return null;
177
+
178
+ return (
179
+ <Animated.View
180
+ testID={testID}
181
+ pointerEvents={visible ? undefined : 'none'}
182
+ style={[style, animatedStyle]}
183
+ >
184
+ {children}
185
+ </Animated.View>
186
+ );
187
+ }
188
+ );
189
+ Transition.displayName = 'Transition';
@@ -20,6 +20,10 @@ export { Image, prefetchImage } from './Image';
20
20
  export type { ImageProps } from './Image';
21
21
  export { Spinner } from './Spinner';
22
22
  export type { SpinnerProps, SpinnerVariant } from './Spinner';
23
+ export { Skeleton } from './Skeleton';
24
+ export type { SkeletonProps, SkeletonVariant } from './Skeleton';
25
+ export { Transition, useReduceMotion } from './Transition';
26
+ export type { TransitionProps, TransitionPreset } from './Transition';
23
27
  export { EmptyState } from './EmptyState';
24
28
  export type { EmptyStateProps } from './EmptyState';
25
29
  export { KeyValueRow } from './KeyValueRow';
@@ -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,8 +32,8 @@ 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
39
  backgroundColor: theme.colors.primarySoft,
@@ -34,48 +41,78 @@ const useStyles = createStyles((theme) => ({
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
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;