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
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "Transition",
3
+ "slug": "transition",
4
+ "type": "component",
5
+ "category": "atoms",
6
+ "exportedNames": [
7
+ "Transition",
8
+ "useReduceMotion"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "atoms/Transition.tsx",
13
+ "content": "import React, { memo, useEffect, useMemo, useRef, useState } from 'react';\nimport { AccessibilityInfo, Animated, Easing, type StyleProp, type ViewStyle } from 'react-native';\nimport { useTheme } from '../theme/UIProvider';\n\nexport type TransitionPreset =\n | 'fade'\n | 'scale'\n | 'pop'\n | 'slide-up'\n | 'slide-down'\n | 'slide-left'\n | 'slide-right';\n\nexport interface TransitionProps {\n children: React.ReactNode;\n /** Entra en true, sale (y desmonta) en false. @default true */\n visible?: boolean;\n /** Uno o varios: 'fade scale' combina opacidad y escala. @default 'fade' */\n preset?: TransitionPreset | TransitionPreset[];\n /** ms del timing. @default tokens.duration.base */\n duration?: number;\n /** ms antes de arrancar. Un índice por delay escalona una lista. @default 0 */\n delay?: number;\n /** Píxeles que recorren los presets de slide. @default 24 */\n distance?: number;\n /** Spring en vez de timing (implícito en 'pop'). */\n spring?: boolean;\n /** Anima también el primer render. En false arranca ya presente. @default true */\n appear?: boolean;\n /** Deja el nodo montado al salir, sólo lo esconde. @default false */\n keepMounted?: boolean;\n /**\n * Progreso 0→1 manejado desde afuera. Con esto el Transition sólo interpola:\n * quien lo pasa decide cuándo y cómo avanza (un PanResponder, por ejemplo).\n */\n value?: Animated.Value;\n onEntered?: () => void;\n onExited?: () => void;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nconst SCALE_FROM: Record<'scale' | 'pop', number> = { scale: 0.92, pop: 0.8 };\n\n/**\n * Sigue el ajuste de \"reducir movimiento\" del sistema: con él activo las\n * animaciones saltan al estado final en vez de desactivarse, así que los\n * callbacks y el desmontaje siguen ocurriendo igual.\n */\nexport function useReduceMotion(): boolean {\n const [reduceMotion, setReduceMotion] = useState(false);\n\n useEffect(() => {\n let alive = true;\n // Sólo se actualiza el estado si el ajuste está activo: el valor inicial ya\n // es `false` y un setState de false a false costaría un render por montaje.\n AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {\n if (alive && enabled) setReduceMotion(true);\n });\n const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduceMotion);\n return () => {\n alive = false;\n sub.remove();\n };\n }, []);\n\n return reduceMotion;\n}\n\n/**\n * Transition\n * Envuelve cualquier árbol y lo anima al entrar y al salir. Todos los presets\n * se reducen a `opacity` y `transform` sobre un único Animated.Value, que es\n * lo que `useNativeDriver` sabe mandar al hilo nativo: la animación corre ahí\n * y el hilo de JS no participa de ningún frame — por eso no hay presets de\n * alto, ancho o color, que obligarían a volver a JS en cada frame.\n *\n * El nodo se mantiene montado hasta que termina la animación de salida, y el\n * comportamiento es idéntico en iOS y Android: no hay ninguna rama por\n * plataforma.\n */\nexport const Transition = memo(\n ({\n children,\n visible = true,\n preset = 'fade',\n duration,\n delay = 0,\n distance = 24,\n spring,\n appear = true,\n keepMounted = false,\n value,\n onEntered,\n onExited,\n style,\n testID,\n }: TransitionProps) => {\n const theme = useTheme();\n const reduceMotion = useReduceMotion();\n const internal = useRef(new Animated.Value(visible && !appear ? 1 : 0)).current;\n const progress = value ?? internal;\n const driven = value === undefined;\n\n const [mounted, setMounted] = useState(visible || keepMounted);\n\n // Los callbacks viven en un ref: pasados inline cambian de identidad en\n // cada render y, como dependencias del efecto, reiniciarían la animación.\n const callbacks = useRef({ onEntered, onExited });\n useEffect(() => {\n callbacks.current = { onEntered, onExited };\n });\n\n const presetKey = Array.isArray(preset) ? preset.join(' ') : preset;\n const presets = useMemo(() => presetKey.split(' ') as TransitionPreset[], [presetKey]);\n\n useEffect(() => {\n if (visible) setMounted(true);\n }, [visible]);\n\n useEffect(() => {\n if (!driven) return;\n const toValue = visible ? 1 : 0;\n const useSpring = spring ?? presets.includes('pop');\n\n const animation = reduceMotion\n ? Animated.timing(progress, { toValue, duration: 0, delay, useNativeDriver: true })\n : useSpring\n ? Animated.spring(progress, { toValue, delay, friction: 9, tension: 90, useNativeDriver: true })\n : Animated.timing(progress, {\n toValue,\n delay,\n duration: duration ?? theme.tokens.duration.base,\n easing: visible ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic),\n useNativeDriver: true,\n });\n\n animation.start(({ finished }) => {\n if (!finished) return;\n if (visible) {\n callbacks.current.onEntered?.();\n return;\n }\n callbacks.current.onExited?.();\n if (!keepMounted) setMounted(false);\n });\n\n return () => animation.stop();\n }, [visible, driven, reduceMotion, spring, delay, duration, keepMounted, presets, progress, theme]);\n\n const animatedStyle = useMemo(() => {\n type Interpolated = Animated.AnimatedInterpolation<number>;\n const transform: Array<{ scale: Interpolated } | { translateX: Interpolated } | { translateY: Interpolated }> = [];\n let opacity: Animated.Value | undefined;\n\n for (const item of presets) {\n if (item === 'fade') {\n opacity = progress;\n continue;\n }\n if (item === 'scale' || item === 'pop') {\n transform.push({\n scale: progress.interpolate({ inputRange: [0, 1], outputRange: [SCALE_FROM[item], 1] }),\n });\n continue;\n }\n const vertical = item === 'slide-up' || item === 'slide-down';\n const from = item === 'slide-up' || item === 'slide-left' ? distance : -distance;\n const offset = progress.interpolate({ inputRange: [0, 1], outputRange: [from, 0] });\n transform.push(vertical ? { translateY: offset } : { translateX: offset });\n }\n\n return { opacity, transform };\n }, [presets, progress, distance]);\n\n if (!mounted) return null;\n\n return (\n <Animated.View\n testID={testID}\n pointerEvents={visible ? undefined : 'none'}\n style={[style, animatedStyle]}\n >\n {children}\n </Animated.View>\n );\n }\n);\nTransition.displayName = 'Transition';\n"
14
+ }
15
+ ],
16
+ "registryDependencies": [
17
+ "core"
18
+ ]
19
+ }
@@ -9,12 +9,13 @@
9
9
  "files": [
10
10
  {
11
11
  "path": "organisms/Wizard.tsx",
12
- "content": "import React, { useCallback, useMemo, useState } from 'react';\nimport { ScrollView, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { Steps, type StepItem } from '../molecules/Steps';\nimport { Button } from '../atoms/Button';\n\nexport interface WizardStep extends StepItem {\n content: React.ReactNode;\n /** false bloquea el avance desde este paso (validación). @default true */\n canGoNext?: boolean;\n}\n\nexport interface WizardProps {\n steps: WizardStep[];\n /** Paso actual (controlado). Si se omite, el Wizard maneja su propio estado. */\n current?: number;\n onStepChange?: (index: number) => void;\n onFinish?: () => void;\n /** @default 'horizontal' */\n orientation?: 'horizontal' | 'vertical';\n completedIndicator?: 'check' | 'number';\n /** Permite volver a un paso ya completado tocándolo en el indicador. @default true */\n allowStepNavigation?: boolean;\n backLabel?: string;\n nextLabel?: string;\n finishLabel?: string;\n /**\n * El contenido del paso scrollea cuando no entra. Ponelo en false sólo si\n * un paso renderiza su propia lista virtualizada (anidar una\n * VirtualizedList dentro de un ScrollView rompe la virtualización).\n * @default true\n */\n scrollableContent?: boolean;\n style?: StyleProp<ViewStyle>;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { flex: 1, gap: theme.tokens.spacing.xl },\n content: { flex: 1 },\n contentInner: { flexGrow: 1, paddingBottom: theme.tokens.spacing.sm },\n footer: { flexDirection: 'row', gap: theme.tokens.spacing.md },\n footerButton: { flex: 1 },\n}));\n\n/**\n * Wizard\n * Flujo multi-paso: indicador (Steps) + contenido del paso actual + barra de\n * navegación. Funciona controlado (`current` + `onStepChange`) o por su\n * cuenta. `canGoNext: false` en un paso deshabilita el avance, que es donde\n * se engancha la validación del formulario de ese paso.\n */\nexport function Wizard({\n steps,\n current: controlledCurrent,\n onStepChange,\n onFinish,\n orientation = 'horizontal',\n completedIndicator = 'check',\n allowStepNavigation = true,\n backLabel = 'Back',\n nextLabel = 'Next',\n finishLabel = 'Finish',\n scrollableContent = true,\n style,\n}: WizardProps) {\n const styles = useStyles();\n const [uncontrolled, setUncontrolled] = useState(0);\n const isControlled = controlledCurrent !== undefined;\n const current = isControlled ? controlledCurrent : uncontrolled;\n\n const goTo = useCallback(\n (index: number) => {\n const clamped = Math.max(0, Math.min(index, steps.length - 1));\n if (!isControlled) setUncontrolled(clamped);\n onStepChange?.(clamped);\n },\n [isControlled, onStepChange, steps.length]\n );\n\n const stepItems = useMemo(\n () => steps.map(({ label, description }) => ({ label, description })),\n [steps]\n );\n\n const activeStep = steps[current];\n const isFirst = current === 0;\n const isLast = current === steps.length - 1;\n const canGoNext = activeStep?.canGoNext !== false;\n\n const handleNext = () => {\n if (isLast) onFinish?.();\n else goTo(current + 1);\n };\n\n return (\n <View style={[styles.container, style]}>\n <Steps\n steps={stepItems}\n current={current}\n orientation={orientation}\n completedIndicator={completedIndicator}\n // Sólo se puede volver atrás tocando el indicador; saltar hacia\n // adelante se saltearía la validación de los pasos intermedios.\n onStepPress={allowStepNavigation ? (index) => index < current && goTo(index) : undefined}\n />\n\n {/* El contenido scrollea en vez de desbordar: con el teclado abierto un\n form largo se comía el footer y los botones quedaban pintados encima\n de los campos. */}\n {scrollableContent ? (\n <ScrollView\n style={styles.content}\n contentContainerStyle={styles.contentInner}\n keyboardShouldPersistTaps=\"handled\"\n automaticallyAdjustKeyboardInsets\n showsVerticalScrollIndicator={false}\n >\n {activeStep?.content}\n </ScrollView>\n ) : (\n <View style={styles.content}>{activeStep?.content}</View>\n )}\n\n <View style={styles.footer}>\n {!isFirst && (\n <Button title={backLabel} variant=\"outline\" onPress={() => goTo(current - 1)} style={styles.footerButton} />\n )}\n <Button\n title={isLast ? finishLabel : nextLabel}\n onPress={handleNext}\n disabled={!canGoNext}\n style={styles.footerButton}\n />\n </View>\n </View>\n );\n}\n"
12
+ "content": "import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { ScrollView, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { Steps, type StepItem } from '../molecules/Steps';\nimport { Button } from '../atoms/Button';\nimport { Transition } from '../atoms/Transition';\n\nexport interface WizardStep extends StepItem {\n content: React.ReactNode;\n /** false bloquea el avance desde este paso (validación). @default true */\n canGoNext?: boolean;\n}\n\nexport interface WizardProps {\n steps: WizardStep[];\n /** Paso actual (controlado). Si se omite, el Wizard maneja su propio estado. */\n current?: number;\n onStepChange?: (index: number) => void;\n onFinish?: () => void;\n /** @default 'horizontal' */\n orientation?: 'horizontal' | 'vertical';\n completedIndicator?: 'check' | 'number';\n /** Permite volver a un paso ya completado tocándolo en el indicador. @default true */\n allowStepNavigation?: boolean;\n backLabel?: string;\n nextLabel?: string;\n finishLabel?: string;\n /**\n * El contenido del paso scrollea cuando no entra. Ponelo en false sólo si\n * un paso renderiza su propia lista virtualizada (anidar una\n * VirtualizedList dentro de un ScrollView rompe la virtualización).\n * @default true\n */\n scrollableContent?: boolean;\n /**\n * El contenido del paso entra deslizándose en el sentido de la navegación.\n * Sólo opacity y transform, así que la animación corre en el hilo nativo.\n * @default true\n */\n animated?: boolean;\n style?: StyleProp<ViewStyle>;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { flex: 1, gap: theme.tokens.spacing.xl },\n content: { flex: 1 },\n contentInner: { flexGrow: 1, paddingBottom: theme.tokens.spacing.sm },\n footer: { flexDirection: 'row', gap: theme.tokens.spacing.md },\n footerButton: { flex: 1 },\n}));\n\n/**\n * Wizard\n * Flujo multi-paso: indicador (Steps) + contenido del paso actual + barra de\n * navegación. Funciona controlado (`current` + `onStepChange`) o por su\n * cuenta. `canGoNext: false` en un paso deshabilita el avance, que es donde\n * se engancha la validación del formulario de ese paso.\n */\nexport function Wizard({\n steps,\n current: controlledCurrent,\n onStepChange,\n onFinish,\n orientation = 'horizontal',\n completedIndicator = 'check',\n allowStepNavigation = true,\n backLabel = 'Back',\n nextLabel = 'Next',\n finishLabel = 'Finish',\n scrollableContent = true,\n animated = true,\n style,\n}: WizardProps) {\n const styles = useStyles();\n const [uncontrolled, setUncontrolled] = useState(0);\n const isControlled = controlledCurrent !== undefined;\n const current = isControlled ? controlledCurrent : uncontrolled;\n\n const goTo = useCallback(\n (index: number) => {\n const clamped = Math.max(0, Math.min(index, steps.length - 1));\n if (!isControlled) setUncontrolled(clamped);\n onStepChange?.(clamped);\n },\n [isControlled, onStepChange, steps.length]\n );\n\n const stepItems = useMemo(\n () => steps.map(({ label, description }) => ({ label, description })),\n [steps]\n );\n\n // El ref todavía guarda el paso anterior durante el render que trae el nuevo.\n const previous = useRef(current);\n const forward = current >= previous.current;\n const entered = useRef(false);\n useEffect(() => {\n previous.current = current;\n entered.current = true;\n }, [current]);\n\n const activeStep = steps[current];\n const isFirst = current === 0;\n const isLast = current === steps.length - 1;\n const canGoNext = activeStep?.canGoNext !== false;\n\n const handleNext = () => {\n if (isLast) onFinish?.();\n else goTo(current + 1);\n };\n\n // El paso saliente no se anima: mantener los dos montados a la vez duplicaría\n // los formularios y su estado.\n const renderContent = () =>\n animated ? (\n <Transition\n key={current}\n testID=\"wizard-step\"\n preset={['fade', forward ? 'slide-left' : 'slide-right']}\n appear={entered.current}\n >\n {activeStep?.content}\n </Transition>\n ) : (\n activeStep?.content\n );\n\n return (\n <View style={[styles.container, style]}>\n <Steps\n steps={stepItems}\n current={current}\n orientation={orientation}\n completedIndicator={completedIndicator}\n // Sólo se puede volver atrás tocando el indicador; saltar hacia\n // adelante se saltearía la validación de los pasos intermedios.\n onStepPress={allowStepNavigation ? (index) => index < current && goTo(index) : undefined}\n />\n\n {/* El contenido scrollea en vez de desbordar: con el teclado abierto un\n form largo se comía el footer y los botones quedaban pintados encima\n de los campos. */}\n {scrollableContent ? (\n <ScrollView\n style={styles.content}\n contentContainerStyle={styles.contentInner}\n keyboardShouldPersistTaps=\"handled\"\n automaticallyAdjustKeyboardInsets\n showsVerticalScrollIndicator={false}\n >\n {renderContent()}\n </ScrollView>\n ) : (\n <View style={styles.content}>{renderContent()}</View>\n )}\n\n <View style={styles.footer}>\n {!isFirst && (\n <Button title={backLabel} variant=\"outline\" onPress={() => goTo(current - 1)} style={styles.footerButton} />\n )}\n <Button\n title={isLast ? finishLabel : nextLabel}\n onPress={handleNext}\n disabled={!canGoNext}\n style={styles.footerButton}\n />\n </View>\n </View>\n );\n}\n"
13
13
  }
14
14
  ],
15
15
  "registryDependencies": [
16
16
  "core",
17
17
  "steps",
18
- "button"
18
+ "button",
19
+ "transition"
19
20
  ]
20
21
  }
@@ -28,7 +28,7 @@ export const Avatar = memo(({ size = 44, backgroundColor, children, style }: Ava
28
28
  width: size,
29
29
  height: size,
30
30
  borderRadius: size / 2,
31
- backgroundColor: backgroundColor || colors.primary + '15',
31
+ backgroundColor: backgroundColor || colors.primarySoft,
32
32
  },
33
33
  style,
34
34
  ]}
@@ -37,10 +37,12 @@ export const Badge = memo(({ label, variant = 'neutral', backgroundColor, textCo
37
37
  const { colors } = theme;
38
38
 
39
39
  const palette: Record<BadgeVariant, { bg: string; fg: string }> = {
40
- success: { bg: colors.success + '20', fg: colors.success },
41
- error: { bg: colors.error + '20', fg: colors.error },
42
- warning: { bg: colors.warning + '20', fg: colors.warning },
43
- info: { bg: colors.secondary + '20', fg: colors.secondary },
40
+ // fg es el rol `*Text`, no el acento pleno: el acento sobre su propio
41
+ // tinte da ~2.2:1 en light — el label quedaba ilegible.
42
+ success: { bg: colors.successSoft, fg: colors.successText },
43
+ error: { bg: colors.errorSoft, fg: colors.errorText },
44
+ warning: { bg: colors.warningSoft, fg: colors.warningText },
45
+ info: { bg: colors.secondarySoft, fg: colors.secondaryText },
44
46
  neutral: { bg: colors.inputBackground, fg: colors.textLight },
45
47
  };
46
48
 
@@ -65,19 +65,21 @@ function resolveColors(variant: ButtonVariant, disabled: boolean, colors: Return
65
65
  return { background: colors.inputBackground, text: colors.textLight, border: 'transparent' };
66
66
  }
67
67
  switch (variant) {
68
+ // Las variantes sin relleno pintan texto sobre el fondo de la pantalla:
69
+ // van con el rol `*Text`. Las rellenas van con `on*` sobre el acento.
68
70
  case 'outline':
69
- return { background: 'transparent', text: colors.primary, border: colors.primary };
71
+ return { background: 'transparent', text: colors.primaryText, border: colors.primary };
70
72
  case 'ghost':
71
- return { background: 'transparent', text: colors.primary, border: 'transparent' };
73
+ return { background: 'transparent', text: colors.primaryText, border: 'transparent' };
72
74
  case 'link':
73
- return { background: 'transparent', text: colors.primary, border: 'transparent' };
75
+ return { background: 'transparent', text: colors.primaryText, border: 'transparent' };
74
76
  case 'secondary':
75
- return { background: colors.secondary, text: colors.white, border: 'transparent' };
77
+ return { background: colors.secondary, text: colors.onSecondary, border: 'transparent' };
76
78
  case 'destructive':
77
- return { background: colors.error, text: colors.white, border: 'transparent' };
79
+ return { background: colors.error, text: colors.onError, border: 'transparent' };
78
80
  case 'primary':
79
81
  default:
80
- return { background: colors.primary, text: colors.white, border: 'transparent' };
82
+ return { background: colors.primary, text: colors.onPrimary, border: 'transparent' };
81
83
  }
82
84
  }
83
85
 
@@ -32,6 +32,9 @@ export const Checkbox = memo(({ value, onValueChange, label, color, disabled, te
32
32
  const styles = useStyles();
33
33
  const allowFontScaling = useAllowFontScaling();
34
34
  const activeColor = color || colors.primary;
35
+ // El check va encima del relleno: con `color` propio no sabemos qué tono
36
+ // contrasta, así que ahí seguimos con blanco (decisión del consumidor).
37
+ const checkColor = color ? colors.white : colors.onPrimary;
35
38
  const progress = useRef(new Animated.Value(value ? 1 : 0)).current;
36
39
  const checkScale = useRef(new Animated.Value(value ? 1 : 0)).current;
37
40
 
@@ -61,7 +64,7 @@ export const Checkbox = memo(({ value, onValueChange, label, color, disabled, te
61
64
  >
62
65
  <Animated.View style={[styles.box, { backgroundColor, borderColor, opacity: disabled ? 0.5 : 1 }]}>
63
66
  <Animated.View style={{ transform: [{ scale: checkScale }] }}>
64
- <Icon name="check" size={16} color={colors.white} />
67
+ <Icon name="check" size={16} color={checkColor} />
65
68
  </Animated.View>
66
69
  </Animated.View>
67
70
  {label && (
package/src/atoms/Fab.tsx CHANGED
@@ -55,7 +55,7 @@ export const Fab = memo(
55
55
  accessibilityRole="button"
56
56
  accessibilityLabel={accessibilityLabel}
57
57
  >
58
- {icon ?? <Icon name={iconName} size={size * 0.5} color={colors.white} />}
58
+ {icon ?? <Icon name={iconName} size={size * 0.5} color={colors.onPrimary} />}
59
59
  </PressableScale>
60
60
  );
61
61
  }
@@ -40,9 +40,9 @@ export interface ImageProps {
40
40
  }
41
41
 
42
42
  const useStyles = createStyles((theme) => ({
43
- container: { overflow: 'hidden', backgroundColor: theme.colors.border + '30' },
43
+ container: { overflow: 'hidden', backgroundColor: theme.colors.skeleton },
44
44
  overlay: { alignItems: 'center', justifyContent: 'center' },
45
- fallback: { flex: 1, backgroundColor: theme.colors.border + '30' },
45
+ fallback: { flex: 1, backgroundColor: theme.colors.skeleton },
46
46
  }));
47
47
 
48
48
  function sourceUri(source: ImageSourcePropType): string | undefined {
@@ -37,7 +37,7 @@ const useStyles = createStyles((theme) => ({
37
37
  fontWeight: theme.tokens.fontWeight.medium,
38
38
  color: theme.colors.text,
39
39
  },
40
- required: { color: theme.colors.error },
40
+ required: { color: theme.colors.errorText },
41
41
  inputWrapper: {
42
42
  flexDirection: 'row',
43
43
  alignItems: 'center',
@@ -55,7 +55,7 @@ const useStyles = createStyles((theme) => ({
55
55
  },
56
56
  leftIcon: { marginLeft: theme.tokens.spacing.lg, marginRight: -theme.tokens.spacing.xs },
57
57
  rightIcon: { padding: theme.tokens.spacing.sm + 2, marginRight: theme.tokens.spacing.xs },
58
- errorText: { marginTop: theme.tokens.spacing.xs, fontSize: theme.tokens.fontSize.sm, color: theme.colors.error },
58
+ errorText: { marginTop: theme.tokens.spacing.xs, fontSize: theme.tokens.fontSize.sm, color: theme.colors.errorText },
59
59
  }));
60
60
 
61
61
  /**
@@ -1,12 +1,22 @@
1
1
  import React, { memo, useRef } from 'react';
2
- import { Animated, Pressable, type PressableProps, type GestureResponderEvent } from 'react-native';
2
+ import {
3
+ Animated,
4
+ Pressable,
5
+ type GestureResponderEvent,
6
+ type PressableProps,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ } from 'react-native';
3
10
 
4
- export interface PressableScaleProps extends PressableProps {
11
+ export interface PressableScaleProps extends Omit<PressableProps, 'style'> {
5
12
  /** Escala objetivo al presionar. @default 0.96 */
6
13
  scaleTo?: number;
14
+ style?: StyleProp<ViewStyle>;
7
15
  children: React.ReactNode;
8
16
  }
9
17
 
18
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
19
+
10
20
  /**
11
21
  * PressableScale
12
22
  * Feedback táctil de "achicarse" al presionar, con Animated.spring de RN
@@ -37,15 +47,15 @@ export const PressableScale = memo(
37
47
  };
38
48
 
39
49
  return (
40
- <Pressable
50
+ <AnimatedPressable
41
51
  {...props}
42
52
  disabled={disabled}
43
53
  onPressIn={handlePressIn}
44
54
  onPressOut={handlePressOut}
45
- style={style}
55
+ style={[style, { transform: [{ scale }] }]}
46
56
  >
47
- <Animated.View style={{ transform: [{ scale }] }}>{children}</Animated.View>
48
- </Pressable>
57
+ {children}
58
+ </AnimatedPressable>
49
59
  );
50
60
  }
51
61
  );
@@ -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';
@@ -22,12 +22,12 @@ const useStyles = createStyles((theme) => ({
22
22
  width: 80,
23
23
  height: 80,
24
24
  borderRadius: 40,
25
- backgroundColor: theme.colors.primary + '20',
25
+ backgroundColor: theme.colors.primarySoft,
26
26
  justifyContent: 'center',
27
27
  alignItems: 'center',
28
28
  marginBottom: theme.tokens.spacing.md,
29
29
  },
30
- initials: { fontSize: theme.tokens.fontSize.xxl, fontWeight: theme.tokens.fontWeight.bold, color: theme.colors.primary },
30
+ initials: { fontSize: theme.tokens.fontSize.xxl, fontWeight: theme.tokens.fontWeight.bold, color: theme.colors.primaryText },
31
31
  title: { fontSize: theme.tokens.fontSize.xl, fontWeight: theme.tokens.fontWeight.bold, textAlign: 'center', color: theme.colors.text, marginBottom: theme.tokens.spacing.xs },
32
32
  subtitle: { fontSize: theme.tokens.fontSize.md, color: theme.colors.textLight },
33
33
  }));