@acvl/mobile-components 0.0.0-stage → 0.0.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 (297) hide show
  1. package/README.md +146 -2
  2. package/assets/fonts/Inter_400Regular.ttf +0 -0
  3. package/assets/fonts/Inter_500Medium.ttf +0 -0
  4. package/assets/fonts/Inter_600SemiBold.ttf +0 -0
  5. package/assets/fonts/Inter_700Bold.ttf +0 -0
  6. package/assets/fonts/Inter_800ExtraBold.ttf +0 -0
  7. package/assets/fonts/Inter_900Black.ttf +0 -0
  8. package/assets/fonts/OFL.txt +93 -0
  9. package/lib/module/componentes/AccionesDelEncabezado.js +212 -0
  10. package/lib/module/componentes/AccionesDelEncabezado.js.map +1 -0
  11. package/lib/module/componentes/BotonDeAvatar.js +55 -0
  12. package/lib/module/componentes/BotonDeAvatar.js.map +1 -0
  13. package/lib/module/componentes/BotonGrande.js +105 -0
  14. package/lib/module/componentes/BotonGrande.js.map +1 -0
  15. package/lib/module/componentes/CabeceraEmpujada.js +101 -0
  16. package/lib/module/componentes/CabeceraEmpujada.js.map +1 -0
  17. package/lib/module/componentes/Chip.js +77 -0
  18. package/lib/module/componentes/Chip.js.map +1 -0
  19. package/lib/module/componentes/ContenidoDePantalla.js +56 -0
  20. package/lib/module/componentes/ContenidoDePantalla.js.map +1 -0
  21. package/lib/module/componentes/EstadoVacio.js +65 -0
  22. package/lib/module/componentes/EstadoVacio.js.map +1 -0
  23. package/lib/module/componentes/Foto.js +106 -0
  24. package/lib/module/componentes/Foto.js.map +1 -0
  25. package/lib/module/componentes/IconoDeRenglon.js +44 -0
  26. package/lib/module/componentes/IconoDeRenglon.js.map +1 -0
  27. package/lib/module/componentes/Logo.js +44 -0
  28. package/lib/module/componentes/Logo.js.map +1 -0
  29. package/lib/module/componentes/Nota.js +51 -0
  30. package/lib/module/componentes/Nota.js.map +1 -0
  31. package/lib/module/componentes/OpcionesExcluyentes.js +69 -0
  32. package/lib/module/componentes/OpcionesExcluyentes.js.map +1 -0
  33. package/lib/module/componentes/PantallaDeError.js +84 -0
  34. package/lib/module/componentes/PantallaDeError.js.map +1 -0
  35. package/lib/module/componentes/Pestanas.js +87 -0
  36. package/lib/module/componentes/Pestanas.js.map +1 -0
  37. package/lib/module/componentes/Rotulo.js +28 -0
  38. package/lib/module/componentes/Rotulo.js.map +1 -0
  39. package/lib/module/componentes/RotuloDeSeccion.js +87 -0
  40. package/lib/module/componentes/RotuloDeSeccion.js.map +1 -0
  41. package/lib/module/componentes/Tarjeta.js +47 -0
  42. package/lib/module/componentes/Tarjeta.js.map +1 -0
  43. package/lib/module/componentes/TarjetaDeDatos.js +79 -0
  44. package/lib/module/componentes/TarjetaDeDatos.js.map +1 -0
  45. package/lib/module/componentes/TiraDeFotos.js +184 -0
  46. package/lib/module/componentes/TiraDeFotos.js.map +1 -0
  47. package/lib/module/componentes/VisorDeFoto.js +171 -0
  48. package/lib/module/componentes/VisorDeFoto.js.map +1 -0
  49. package/lib/module/componentes/logos/bola-blanca.png +0 -0
  50. package/lib/module/componentes/logos/bola.png +0 -0
  51. package/lib/module/componentes/logos/logo-blanco.png +0 -0
  52. package/lib/module/componentes/logos/logo.png +0 -0
  53. package/lib/module/dialogos/HostDeDialogos.js +170 -0
  54. package/lib/module/dialogos/HostDeDialogos.js.map +1 -0
  55. package/lib/module/dialogos/dialogos.js +112 -0
  56. package/lib/module/dialogos/dialogos.js.map +1 -0
  57. package/lib/module/formularios/CampoDeSeleccion.js +47 -0
  58. package/lib/module/formularios/CampoDeSeleccion.js.map +1 -0
  59. package/lib/module/formularios/SelectorDeOpcion.js +406 -0
  60. package/lib/module/formularios/SelectorDeOpcion.js.map +1 -0
  61. package/lib/module/formularios/TextoDeAccesoControlado.js +73 -0
  62. package/lib/module/formularios/TextoDeAccesoControlado.js.map +1 -0
  63. package/lib/module/formularios/campos.js +493 -0
  64. package/lib/module/formularios/campos.js.map +1 -0
  65. package/lib/module/formularios/controlados.js +197 -0
  66. package/lib/module/formularios/controlados.js.map +1 -0
  67. package/lib/module/formularios/errores.js +91 -0
  68. package/lib/module/formularios/errores.js.map +1 -0
  69. package/lib/module/formularios/guardado.js +78 -0
  70. package/lib/module/formularios/guardado.js.map +1 -0
  71. package/lib/module/formularios/useBusqueda.js +22 -0
  72. package/lib/module/formularios/useBusqueda.js.map +1 -0
  73. package/lib/module/fotos/elegir.js +178 -0
  74. package/lib/module/fotos/elegir.js.map +1 -0
  75. package/lib/module/fotos/index.js +8 -0
  76. package/lib/module/fotos/index.js.map +1 -0
  77. package/lib/module/fotos/reducir.js +93 -0
  78. package/lib/module/fotos/reducir.js.map +1 -0
  79. package/lib/module/hoja/HojaInferior.js +108 -0
  80. package/lib/module/hoja/HojaInferior.js.map +1 -0
  81. package/lib/module/hoja/index.js +7 -0
  82. package/lib/module/hoja/index.js.map +1 -0
  83. package/lib/module/hoja/useMasDeLaFicha.js +48 -0
  84. package/lib/module/hoja/useMasDeLaFicha.js.map +1 -0
  85. package/lib/module/index.js +66 -0
  86. package/lib/module/index.js.map +1 -0
  87. package/lib/module/listas/HojaDeFiltros.js +260 -0
  88. package/lib/module/listas/HojaDeFiltros.js.map +1 -0
  89. package/lib/module/listas/ListaPaginada.js +103 -0
  90. package/lib/module/listas/ListaPaginada.js.map +1 -0
  91. package/lib/module/listas/TarjetaDeslizable.js +118 -0
  92. package/lib/module/listas/TarjetaDeslizable.js.map +1 -0
  93. package/lib/module/listas/paginado.js +11 -0
  94. package/lib/module/listas/paginado.js.map +1 -0
  95. package/lib/module/listas/piezas.js +603 -0
  96. package/lib/module/listas/piezas.js.map +1 -0
  97. package/lib/module/navegacion/BarraDePestanas.js +162 -0
  98. package/lib/module/navegacion/BarraDePestanas.js.map +1 -0
  99. package/lib/module/navegacion/BotonDeBarra.js +58 -0
  100. package/lib/module/navegacion/BotonDeBarra.js.map +1 -0
  101. package/lib/module/navegacion/encabezado.js +103 -0
  102. package/lib/module/navegacion/encabezado.js.map +1 -0
  103. package/lib/module/pantallas/PantallaDeAcceso.js +195 -0
  104. package/lib/module/pantallas/PantallaDeAcceso.js.map +1 -0
  105. package/lib/module/pantallas/PantallaDeEdicion.js +121 -0
  106. package/lib/module/pantallas/PantallaDeEdicion.js.map +1 -0
  107. package/lib/module/pantallas/PantallaDeFicha.js +94 -0
  108. package/lib/module/pantallas/PantallaDeFicha.js.map +1 -0
  109. package/lib/module/pantallas/PantallaDePerfil.js +137 -0
  110. package/lib/module/pantallas/PantallaDePerfil.js.map +1 -0
  111. package/lib/module/tema/ProveedorDeTema.js +122 -0
  112. package/lib/module/tema/ProveedorDeTema.js.map +1 -0
  113. package/lib/module/tema/estilos.js +54 -0
  114. package/lib/module/tema/estilos.js.map +1 -0
  115. package/lib/module/tema/fuentes.js +28 -0
  116. package/lib/module/tema/fuentes.js.map +1 -0
  117. package/lib/module/tema/index.js +8 -0
  118. package/lib/module/tema/index.js.map +1 -0
  119. package/lib/module/tema/paleta.js +107 -0
  120. package/lib/module/tema/paleta.js.map +1 -0
  121. package/lib/module/tema/tipografia.js +32 -0
  122. package/lib/module/tema/tipografia.js.map +1 -0
  123. package/lib/module/texto.js +48 -0
  124. package/lib/module/texto.js.map +1 -0
  125. package/lib/typescript/src/componentes/AccionesDelEncabezado.d.ts +34 -0
  126. package/lib/typescript/src/componentes/AccionesDelEncabezado.d.ts.map +1 -0
  127. package/lib/typescript/src/componentes/BotonDeAvatar.d.ts +17 -0
  128. package/lib/typescript/src/componentes/BotonDeAvatar.d.ts.map +1 -0
  129. package/lib/typescript/src/componentes/BotonGrande.d.ts +26 -0
  130. package/lib/typescript/src/componentes/BotonGrande.d.ts.map +1 -0
  131. package/lib/typescript/src/componentes/CabeceraEmpujada.d.ts +18 -0
  132. package/lib/typescript/src/componentes/CabeceraEmpujada.d.ts.map +1 -0
  133. package/lib/typescript/src/componentes/Chip.d.ts +14 -0
  134. package/lib/typescript/src/componentes/Chip.d.ts.map +1 -0
  135. package/lib/typescript/src/componentes/ContenidoDePantalla.d.ts +17 -0
  136. package/lib/typescript/src/componentes/ContenidoDePantalla.d.ts.map +1 -0
  137. package/lib/typescript/src/componentes/EstadoVacio.d.ts +17 -0
  138. package/lib/typescript/src/componentes/EstadoVacio.d.ts.map +1 -0
  139. package/lib/typescript/src/componentes/Foto.d.ts +41 -0
  140. package/lib/typescript/src/componentes/Foto.d.ts.map +1 -0
  141. package/lib/typescript/src/componentes/IconoDeRenglon.d.ts +25 -0
  142. package/lib/typescript/src/componentes/IconoDeRenglon.d.ts.map +1 -0
  143. package/lib/typescript/src/componentes/Logo.d.ts +13 -0
  144. package/lib/typescript/src/componentes/Logo.d.ts.map +1 -0
  145. package/lib/typescript/src/componentes/Nota.d.ts +7 -0
  146. package/lib/typescript/src/componentes/Nota.d.ts.map +1 -0
  147. package/lib/typescript/src/componentes/OpcionesExcluyentes.d.ts +14 -0
  148. package/lib/typescript/src/componentes/OpcionesExcluyentes.d.ts.map +1 -0
  149. package/lib/typescript/src/componentes/PantallaDeError.d.ts +23 -0
  150. package/lib/typescript/src/componentes/PantallaDeError.d.ts.map +1 -0
  151. package/lib/typescript/src/componentes/Pestanas.d.ts +28 -0
  152. package/lib/typescript/src/componentes/Pestanas.d.ts.map +1 -0
  153. package/lib/typescript/src/componentes/Rotulo.d.ts +6 -0
  154. package/lib/typescript/src/componentes/Rotulo.d.ts.map +1 -0
  155. package/lib/typescript/src/componentes/RotuloDeSeccion.d.ts +20 -0
  156. package/lib/typescript/src/componentes/RotuloDeSeccion.d.ts.map +1 -0
  157. package/lib/typescript/src/componentes/Tarjeta.d.ts +15 -0
  158. package/lib/typescript/src/componentes/Tarjeta.d.ts.map +1 -0
  159. package/lib/typescript/src/componentes/TarjetaDeDatos.d.ts +13 -0
  160. package/lib/typescript/src/componentes/TarjetaDeDatos.d.ts.map +1 -0
  161. package/lib/typescript/src/componentes/TiraDeFotos.d.ts +36 -0
  162. package/lib/typescript/src/componentes/TiraDeFotos.d.ts.map +1 -0
  163. package/lib/typescript/src/componentes/VisorDeFoto.d.ts +23 -0
  164. package/lib/typescript/src/componentes/VisorDeFoto.d.ts.map +1 -0
  165. package/lib/typescript/src/dialogos/HostDeDialogos.d.ts +14 -0
  166. package/lib/typescript/src/dialogos/HostDeDialogos.d.ts.map +1 -0
  167. package/lib/typescript/src/dialogos/dialogos.d.ts +65 -0
  168. package/lib/typescript/src/dialogos/dialogos.d.ts.map +1 -0
  169. package/lib/typescript/src/formularios/CampoDeSeleccion.d.ts +22 -0
  170. package/lib/typescript/src/formularios/CampoDeSeleccion.d.ts.map +1 -0
  171. package/lib/typescript/src/formularios/SelectorDeOpcion.d.ts +59 -0
  172. package/lib/typescript/src/formularios/SelectorDeOpcion.d.ts.map +1 -0
  173. package/lib/typescript/src/formularios/TextoDeAccesoControlado.d.ts +18 -0
  174. package/lib/typescript/src/formularios/TextoDeAccesoControlado.d.ts.map +1 -0
  175. package/lib/typescript/src/formularios/campos.d.ts +107 -0
  176. package/lib/typescript/src/formularios/campos.d.ts.map +1 -0
  177. package/lib/typescript/src/formularios/controlados.d.ts +29 -0
  178. package/lib/typescript/src/formularios/controlados.d.ts.map +1 -0
  179. package/lib/typescript/src/formularios/errores.d.ts +36 -0
  180. package/lib/typescript/src/formularios/errores.d.ts.map +1 -0
  181. package/lib/typescript/src/formularios/guardado.d.ts +36 -0
  182. package/lib/typescript/src/formularios/guardado.d.ts.map +1 -0
  183. package/lib/typescript/src/formularios/useBusqueda.d.ts +7 -0
  184. package/lib/typescript/src/formularios/useBusqueda.d.ts.map +1 -0
  185. package/lib/typescript/src/fotos/elegir.d.ts +58 -0
  186. package/lib/typescript/src/fotos/elegir.d.ts.map +1 -0
  187. package/lib/typescript/src/fotos/index.d.ts +3 -0
  188. package/lib/typescript/src/fotos/index.d.ts.map +1 -0
  189. package/lib/typescript/src/fotos/reducir.d.ts +32 -0
  190. package/lib/typescript/src/fotos/reducir.d.ts.map +1 -0
  191. package/lib/typescript/src/hoja/HojaInferior.d.ts +21 -0
  192. package/lib/typescript/src/hoja/HojaInferior.d.ts.map +1 -0
  193. package/lib/typescript/src/hoja/index.d.ts +3 -0
  194. package/lib/typescript/src/hoja/index.d.ts.map +1 -0
  195. package/lib/typescript/src/hoja/useMasDeLaFicha.d.ts +27 -0
  196. package/lib/typescript/src/hoja/useMasDeLaFicha.d.ts.map +1 -0
  197. package/lib/typescript/src/index.d.ts +45 -0
  198. package/lib/typescript/src/index.d.ts.map +1 -0
  199. package/lib/typescript/src/listas/HojaDeFiltros.d.ts +60 -0
  200. package/lib/typescript/src/listas/HojaDeFiltros.d.ts.map +1 -0
  201. package/lib/typescript/src/listas/ListaPaginada.d.ts +39 -0
  202. package/lib/typescript/src/listas/ListaPaginada.d.ts.map +1 -0
  203. package/lib/typescript/src/listas/TarjetaDeslizable.d.ts +33 -0
  204. package/lib/typescript/src/listas/TarjetaDeslizable.d.ts.map +1 -0
  205. package/lib/typescript/src/listas/paginado.d.ts +16 -0
  206. package/lib/typescript/src/listas/paginado.d.ts.map +1 -0
  207. package/lib/typescript/src/listas/piezas.d.ts +135 -0
  208. package/lib/typescript/src/listas/piezas.d.ts.map +1 -0
  209. package/lib/typescript/src/navegacion/BarraDePestanas.d.ts +57 -0
  210. package/lib/typescript/src/navegacion/BarraDePestanas.d.ts.map +1 -0
  211. package/lib/typescript/src/navegacion/BotonDeBarra.d.ts +15 -0
  212. package/lib/typescript/src/navegacion/BotonDeBarra.d.ts.map +1 -0
  213. package/lib/typescript/src/navegacion/encabezado.d.ts +44 -0
  214. package/lib/typescript/src/navegacion/encabezado.d.ts.map +1 -0
  215. package/lib/typescript/src/pantallas/PantallaDeAcceso.d.ts +32 -0
  216. package/lib/typescript/src/pantallas/PantallaDeAcceso.d.ts.map +1 -0
  217. package/lib/typescript/src/pantallas/PantallaDeEdicion.d.ts +38 -0
  218. package/lib/typescript/src/pantallas/PantallaDeEdicion.d.ts.map +1 -0
  219. package/lib/typescript/src/pantallas/PantallaDeFicha.d.ts +35 -0
  220. package/lib/typescript/src/pantallas/PantallaDeFicha.d.ts.map +1 -0
  221. package/lib/typescript/src/pantallas/PantallaDePerfil.d.ts +19 -0
  222. package/lib/typescript/src/pantallas/PantallaDePerfil.d.ts.map +1 -0
  223. package/lib/typescript/src/tema/ProveedorDeTema.d.ts +32 -0
  224. package/lib/typescript/src/tema/ProveedorDeTema.d.ts.map +1 -0
  225. package/lib/typescript/src/tema/estilos.d.ts +31 -0
  226. package/lib/typescript/src/tema/estilos.d.ts.map +1 -0
  227. package/lib/typescript/src/tema/fuentes.d.ts +20 -0
  228. package/lib/typescript/src/tema/fuentes.d.ts.map +1 -0
  229. package/lib/typescript/src/tema/index.d.ts +6 -0
  230. package/lib/typescript/src/tema/index.d.ts.map +1 -0
  231. package/lib/typescript/src/tema/paleta.d.ts +86 -0
  232. package/lib/typescript/src/tema/paleta.d.ts.map +1 -0
  233. package/lib/typescript/src/tema/tipografia.d.ts +29 -0
  234. package/lib/typescript/src/tema/tipografia.d.ts.map +1 -0
  235. package/lib/typescript/src/texto.d.ts +16 -0
  236. package/lib/typescript/src/texto.d.ts.map +1 -0
  237. package/package.json +135 -5
  238. package/src/componentes/AccionesDelEncabezado.tsx +247 -0
  239. package/src/componentes/BotonDeAvatar.tsx +59 -0
  240. package/src/componentes/BotonGrande.tsx +100 -0
  241. package/src/componentes/CabeceraEmpujada.tsx +69 -0
  242. package/src/componentes/Chip.tsx +60 -0
  243. package/src/componentes/ContenidoDePantalla.tsx +53 -0
  244. package/src/componentes/EstadoVacio.tsx +45 -0
  245. package/src/componentes/Foto.tsx +92 -0
  246. package/src/componentes/IconoDeRenglon.tsx +42 -0
  247. package/src/componentes/Logo.tsx +34 -0
  248. package/src/componentes/Nota.tsx +37 -0
  249. package/src/componentes/OpcionesExcluyentes.tsx +62 -0
  250. package/src/componentes/PantallaDeError.tsx +76 -0
  251. package/src/componentes/Pestanas.tsx +86 -0
  252. package/src/componentes/Rotulo.tsx +21 -0
  253. package/src/componentes/RotuloDeSeccion.tsx +71 -0
  254. package/src/componentes/Tarjeta.tsx +44 -0
  255. package/src/componentes/TarjetaDeDatos.tsx +62 -0
  256. package/src/componentes/TiraDeFotos.tsx +189 -0
  257. package/src/componentes/VisorDeFoto.tsx +181 -0
  258. package/src/componentes/logos/bola-blanca.png +0 -0
  259. package/src/componentes/logos/bola.png +0 -0
  260. package/src/componentes/logos/logo-blanco.png +0 -0
  261. package/src/componentes/logos/logo.png +0 -0
  262. package/src/dialogos/HostDeDialogos.tsx +172 -0
  263. package/src/dialogos/dialogos.ts +148 -0
  264. package/src/formularios/CampoDeSeleccion.tsx +46 -0
  265. package/src/formularios/SelectorDeOpcion.tsx +407 -0
  266. package/src/formularios/TextoDeAccesoControlado.tsx +72 -0
  267. package/src/formularios/campos.tsx +483 -0
  268. package/src/formularios/controlados.tsx +198 -0
  269. package/src/formularios/errores.ts +105 -0
  270. package/src/formularios/guardado.ts +76 -0
  271. package/src/formularios/useBusqueda.ts +19 -0
  272. package/src/fotos/elegir.ts +194 -0
  273. package/src/fotos/index.ts +15 -0
  274. package/src/fotos/reducir.ts +87 -0
  275. package/src/hoja/HojaInferior.tsx +102 -0
  276. package/src/hoja/index.ts +4 -0
  277. package/src/hoja/useMasDeLaFicha.tsx +63 -0
  278. package/src/index.ts +143 -0
  279. package/src/listas/HojaDeFiltros.tsx +261 -0
  280. package/src/listas/ListaPaginada.tsx +117 -0
  281. package/src/listas/TarjetaDeslizable.tsx +126 -0
  282. package/src/listas/paginado.ts +15 -0
  283. package/src/listas/piezas.tsx +594 -0
  284. package/src/navegacion/BarraDePestanas.tsx +198 -0
  285. package/src/navegacion/BotonDeBarra.tsx +61 -0
  286. package/src/navegacion/encabezado.tsx +88 -0
  287. package/src/pantallas/PantallaDeAcceso.tsx +184 -0
  288. package/src/pantallas/PantallaDeEdicion.tsx +116 -0
  289. package/src/pantallas/PantallaDeFicha.tsx +115 -0
  290. package/src/pantallas/PantallaDePerfil.tsx +126 -0
  291. package/src/tema/ProveedorDeTema.tsx +150 -0
  292. package/src/tema/estilos.ts +51 -0
  293. package/src/tema/fuentes.ts +20 -0
  294. package/src/tema/index.ts +10 -0
  295. package/src/tema/paleta.ts +147 -0
  296. package/src/tema/tipografia.ts +29 -0
  297. package/src/texto.tsx +44 -0
@@ -0,0 +1,198 @@
1
+ import * as Haptics from "expo-haptics";
2
+ import { useEffect, useState } from "react";
3
+ import { type LayoutChangeEvent, useWindowDimensions, View } from "react-native";
4
+ import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated";
5
+ import { crearEstilos, usePaleta } from "../tema";
6
+ import BotonDeBarra, { type IconoDePestana } from "./BotonDeBarra";
7
+
8
+ /**
9
+ * Lo que una lista deja libre abajo para que su último renglón no quede debajo de la barra, que
10
+ * flota encima del contenido: lo que se separa del borde, lo que mide y un margen.
11
+ */
12
+ export const HUECO_DE_LA_BARRA = 140;
13
+
14
+ // La barra se ajusta al número de pestañas: con una sola no queda un pastillón vacío
15
+ // alrededor del icono, y con cuatro no se sale de la pantalla. Cada pestaña mide lo que
16
+ // pueda hasta `TAB_WIDTH_MAX`, repartiendo el ancho disponible.
17
+ const TAB_WIDTH_MAX = 115;
18
+ const SCREEN_MARGIN = 24;
19
+ const INDICATOR_INSET = 25;
20
+
21
+ interface Ruta {
22
+ key: string;
23
+ name: string;
24
+ params?: object;
25
+ }
26
+
27
+ /**
28
+ * Lo que la barra lee de las props del `tabBar` de `Tabs` (`BottomTabBarProps`). Escrito a mano y
29
+ * no importado: cada versión de expo-router lo exporta desde otro lado.
30
+ */
31
+ interface PropsDelTabBar {
32
+ state: { index: number; routes: Ruta[] };
33
+ navigation: {
34
+ emit: (evento: {
35
+ type: "tabPress" | "tabLongPress";
36
+ target: string;
37
+ canPreventDefault?: boolean;
38
+ }) => object;
39
+ navigate: (...args: any[]) => void;
40
+ };
41
+ descriptors?: Record<string, { options: { title?: string } }>;
42
+ }
43
+
44
+ interface Props extends PropsDelTabBar {
45
+ /** El icono de cada pestaña, por nombre de ruta. Una ruta sin icono deja su hueco vacío. */
46
+ iconos: Record<string, IconoDePestana>;
47
+ /**
48
+ * Nombres de ruta que sí van en la barra. Las pestañas que el usuario no alcanza se
49
+ * montan igual pero con `href: null`, y expo-router las deja en `state.routes` (sólo
50
+ * les pone `tabBarItemStyle.display = "none"`), así que hay que filtrarlas aquí.
51
+ */
52
+ visibles?: string[];
53
+ }
54
+
55
+ /**
56
+ * La barra de pestañas de abajo: una pastilla flotante con un icono por pestaña y la elegida
57
+ * resaltada en rojo. Va en el `tabBar` de `Tabs`:
58
+ *
59
+ * ```tsx
60
+ * <Tabs tabBar={(props) => <BarraDePestanas {...props} iconos={ICONOS} />}>
61
+ * ```
62
+ *
63
+ * Flota encima del contenido: las listas dejan `HUECO_DE_LA_BARRA` abajo.
64
+ */
65
+ const BarraDePestanas = ({ state, navigation, descriptors, iconos, visibles }: Props) => {
66
+ const c = usePaleta();
67
+ const styles = useStyles();
68
+ // Sólo el alto se mide; el ancho lo fija el número de pestañas.
69
+ const [height, setHeight] = useState(0);
70
+ const { width: screenWidth } = useWindowDimensions();
71
+
72
+ const routes = visibles
73
+ ? state.routes.filter((route) => visibles.includes(route.name))
74
+ : state.routes;
75
+
76
+ const tabWidth = Math.min(TAB_WIDTH_MAX, (screenWidth - SCREEN_MARGIN * 2) / routes.length);
77
+ const indicatorWidth = Math.max(0, tabWidth - INDICATOR_INSET);
78
+ const indicatorX = (index: number) => tabWidth * index + (tabWidth - indicatorWidth) / 2;
79
+
80
+ // `state.index` cuenta sobre todas las rutas, incluidas las ocultas. Si la activa no
81
+ // está en la barra —sólo pasa navegando a mano a una ruta sin permiso— la pastilla se
82
+ // queda en la primera.
83
+ const activeIndex = Math.max(
84
+ 0,
85
+ routes.findIndex((route) => route.key === state.routes[state.index]?.key),
86
+ );
87
+
88
+ const onTabbarLayout = (e: LayoutChangeEvent) => setHeight(e.nativeEvent.layout.height);
89
+
90
+ const tabPositionX = useSharedValue(indicatorX(activeIndex));
91
+
92
+ // La pastilla sigue a la pestaña activa (no al toque): así queda en su sitio cuando
93
+ // el grupo arranca en una ruta distinta de la primera, o cuando se navega sin tocar
94
+ // la barra.
95
+ useEffect(() => {
96
+ tabPositionX.value = withSpring(indicatorX(activeIndex), { duration: 350 });
97
+ // eslint-disable-next-line react-hooks/exhaustive-deps
98
+ }, [activeIndex, tabWidth, indicatorWidth, tabPositionX]);
99
+
100
+ const animatedStyle = useAnimatedStyle(() => {
101
+ return {
102
+ transform: [{ translateX: tabPositionX.value }],
103
+ };
104
+ });
105
+
106
+ return (
107
+ <View style={styles.wrapper}>
108
+ <View
109
+ onLayout={onTabbarLayout}
110
+ style={[styles.tabbar, { width: tabWidth * routes.length }]}
111
+ >
112
+ <Animated.View
113
+ style={[
114
+ animatedStyle,
115
+ {
116
+ position: "absolute",
117
+ borderRadius: 30,
118
+ backgroundColor: c.primary,
119
+ // Nada que pintar hasta la primera medición del alto.
120
+ height: Math.max(0, height - 15),
121
+ width: indicatorWidth,
122
+ },
123
+ ]}
124
+ />
125
+ {routes.map((route, index) => {
126
+ const isFocused = index === activeIndex;
127
+
128
+ const onPress = () => {
129
+ const event = navigation.emit({
130
+ type: "tabPress",
131
+ target: route.key,
132
+ canPreventDefault: true,
133
+ });
134
+
135
+ // Sólo el de `tabPress` (`canPreventDefault`) trae `defaultPrevented`.
136
+ const prevenido = "defaultPrevented" in event && event.defaultPrevented;
137
+ if (!isFocused && !prevenido) {
138
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
139
+ navigation.navigate(route.name, route.params);
140
+ }
141
+ };
142
+
143
+ const onLongPress = () => {
144
+ navigation.emit({
145
+ type: "tabLongPress",
146
+ target: route.key,
147
+ });
148
+ };
149
+
150
+ return (
151
+ <BotonDeBarra
152
+ key={route.key}
153
+ onPress={onPress}
154
+ onLongPress={onLongPress}
155
+ isFocused={isFocused}
156
+ icono={iconos[route.name]}
157
+ etiqueta={descriptors?.[route.key]?.options.title ?? route.name}
158
+ color={isFocused ? c.sobrePrimario : c.primary}
159
+ />
160
+ );
161
+ })}
162
+ </View>
163
+ </View>
164
+ );
165
+ };
166
+
167
+ const useStyles = crearEstilos((c) => ({
168
+ wrapper: {
169
+ position: "absolute",
170
+ bottom: 30,
171
+ left: 0,
172
+ right: 0,
173
+ alignItems: "center",
174
+ },
175
+ tabbar: {
176
+ flexDirection: "row",
177
+ justifyContent: "space-between",
178
+ alignItems: "center",
179
+ backgroundColor: c.superficie,
180
+ paddingVertical: 22,
181
+ borderRadius: 35,
182
+
183
+ // android
184
+ elevation: 3,
185
+
186
+ // ios
187
+ shadowColor: c.sombra,
188
+ shadowOffset: { width: 0, height: 10 },
189
+ shadowRadius: 10,
190
+ shadowOpacity: 0.1,
191
+
192
+ // En oscuro la sombra no se ve: el borde separa la barra del mapa o la lista de abajo.
193
+ borderWidth: c.esOscuro ? 1 : 0,
194
+ borderColor: c.secondaryLighter,
195
+ },
196
+ }));
197
+
198
+ export default BarraDePestanas;
@@ -0,0 +1,61 @@
1
+ import MaterialIcons from "@expo/vector-icons/MaterialIcons";
2
+ import React, { useEffect } from "react";
3
+ import { Pressable, StyleSheet } from "react-native";
4
+ import Animated, {
5
+ interpolate,
6
+ useAnimatedStyle,
7
+ useSharedValue,
8
+ withSpring,
9
+ } from "react-native-reanimated";
10
+
11
+ export type IconoDePestana = React.ComponentProps<typeof MaterialIcons>["name"];
12
+
13
+ interface Props {
14
+ onPress: () => void;
15
+ onLongPress: () => void;
16
+ isFocused: boolean;
17
+ icono: IconoDePestana | undefined;
18
+ etiqueta: string;
19
+ color: string;
20
+ }
21
+
22
+ /** Una pestaña de `BarraDePestanas`: su icono, que crece un poco al elegirla. */
23
+ const BotonDeBarra = ({ onPress, onLongPress, isFocused, icono, etiqueta, color }: Props) => {
24
+ const scale = useSharedValue(0);
25
+
26
+ useEffect(() => {
27
+ scale.value = withSpring(isFocused ? 1 : 0, { duration: 350 });
28
+ }, [scale, isFocused]);
29
+
30
+ const animatedIconStyle = useAnimatedStyle(() => {
31
+ const scaleValue = interpolate(scale.value, [0, 1], [1, 1.2]);
32
+ return {
33
+ transform: [{ scale: scaleValue }],
34
+ };
35
+ });
36
+
37
+ return (
38
+ <Pressable
39
+ onPress={onPress}
40
+ onLongPress={onLongPress}
41
+ accessibilityRole="tab"
42
+ accessibilityLabel={etiqueta}
43
+ accessibilityState={{ selected: isFocused }}
44
+ style={styles.tabbarItem}
45
+ >
46
+ <Animated.View style={animatedIconStyle}>
47
+ {icono ? <MaterialIcons name={icono} size={24} color={color} /> : null}
48
+ </Animated.View>
49
+ </Pressable>
50
+ );
51
+ };
52
+
53
+ const styles = StyleSheet.create({
54
+ tabbarItem: {
55
+ flex: 1,
56
+ justifyContent: "center",
57
+ alignItems: "center",
58
+ },
59
+ });
60
+
61
+ export default BotonDeBarra;
@@ -0,0 +1,88 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
4
+ import { Logo } from "../componentes/Logo";
5
+ import { Text } from "../texto";
6
+ import { crearEstilos, renglon, tipografia, usePaleta } from "../tema";
7
+
8
+ /**
9
+ * Alto del header por debajo de la barra de estado. Iguala el de `CabeceraEmpujada` (10 de
10
+ * padding + la fila de 45 del botón de atrás + 13), y se suma al inset porque `headerStyle.height`
11
+ * de React Navigation incluye la franja de la barra de estado: un alto fijo salía bien en iOS
12
+ * —inset grande— y desproporcionado en Android.
13
+ */
14
+ export const ALTO_DEL_ENCABEZADO = 76;
15
+
16
+ /**
17
+ * El título del header de las pestañas: un rótulo arriba (quién entró, la yarda, el nombre de la
18
+ * app) y el de la pestaña en rojo.
19
+ */
20
+ export function TituloDelEncabezado({ eyebrow, children }: { eyebrow: string; children: string }) {
21
+ const styles = useStyles();
22
+ return (
23
+ <View>
24
+ {/* Sin recortar: parte en dos renglones si hace falta, como todo lo que se lee de
25
+ pie. */}
26
+ <Text style={styles.eyebrow}>{eyebrow.toUpperCase()}</Text>
27
+ {/* Con el avatar a la derecha el título queda justo en pantallas angostas: que encoja
28
+ antes que partirse en dos renglones. */}
29
+ <Text numberOfLines={1} adjustsFontSizeToFit style={styles.titulo}>
30
+ {children}
31
+ </Text>
32
+ </View>
33
+ );
34
+ }
35
+
36
+ /**
37
+ * El header de las pestañas de una app, igual en todas: la bola a la izquierda, el rótulo y el
38
+ * título, y a la derecha lo que la app ponga (sus pendientes, el avatar). Va en el `screenOptions`
39
+ * de `Tabs`:
40
+ *
41
+ * ```tsx
42
+ * const encabezado = useOpcionesDelEncabezado({ eyebrow: "Operador", derecha: <BotonDeAvatar … /> });
43
+ * <Tabs screenOptions={encabezado} tabBar={…}>
44
+ * ```
45
+ */
46
+ export function useOpcionesDelEncabezado({
47
+ eyebrow,
48
+ derecha,
49
+ }: {
50
+ eyebrow: string;
51
+ derecha?: React.ReactNode;
52
+ }) {
53
+ const c = usePaleta();
54
+ const styles = useStyles();
55
+ const insets = useSafeAreaInsets();
56
+ return {
57
+ headerShadowVisible: false,
58
+ headerStyle: { height: insets.top + ALTO_DEL_ENCABEZADO, backgroundColor: c.superficie },
59
+ headerTitleAlign: "left" as const,
60
+ headerTitle: (props: { children: string }) => (
61
+ <TituloDelEncabezado eyebrow={eyebrow}>{props.children}</TituloDelEncabezado>
62
+ ),
63
+ headerLeft: () => (
64
+ <View style={styles.izquierda}>
65
+ <Logo />
66
+ </View>
67
+ ),
68
+ headerRight: derecha ? () => <View style={styles.derecha}>{derecha}</View> : undefined,
69
+ tabBarHideOnKeyboard: true,
70
+ };
71
+ }
72
+
73
+ const useStyles = crearEstilos((c) => ({
74
+ // Con la letra mínima (`etiqueta`): a 11 el nombre no se leía.
75
+ eyebrow: {
76
+ ...renglon(tipografia.etiqueta),
77
+ fontWeight: "700",
78
+ letterSpacing: 1.4,
79
+ color: c.eyebrow,
80
+ },
81
+ titulo: {
82
+ fontSize: 30,
83
+ fontWeight: "800",
84
+ color: c.primary,
85
+ },
86
+ izquierda: { marginLeft: 20 },
87
+ derecha: { flexDirection: "row", alignItems: "center", gap: 6, marginRight: 20 },
88
+ }));
@@ -0,0 +1,184 @@
1
+ import * as Haptics from "expo-haptics";
2
+ import { useState } from "react";
3
+ import { useForm } from "react-hook-form";
4
+ import { useWindowDimensions, View } from "react-native";
5
+ import { KeyboardAvoidingView } from "react-native-keyboard-controller";
6
+ import { Button, Card, Chip, Snackbar } from "react-native-paper";
7
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
8
+ import { LogoCompleto } from "../componentes/Logo";
9
+ import { setErrors } from "../formularios/errores";
10
+ import TextoDeAccesoControlado from "../formularios/TextoDeAccesoControlado";
11
+ import { crearEstilos, usePaleta } from "../tema";
12
+ import { Text } from "../texto";
13
+
14
+ // Mismo corte que ContenidoDePantalla: de aquí para abajo es teléfono.
15
+ const ANCHO_DE_TABLETA = 700;
16
+
17
+ /** Lo que se escribe, con los nombres del servidor: así `setErrors` lo cuelga de su campo. */
18
+ export interface DatosDeAcceso {
19
+ username: string;
20
+ password: string;
21
+ }
22
+
23
+ /**
24
+ * Lo que pasó al intentar entrar: `null` si entró (la app ya guardó la sesión y su layout la
25
+ * lleva adentro), o lo que se le dice a quien escribe.
26
+ */
27
+ export type ResultadoDeAcceso = null | {
28
+ mensaje: string;
29
+ /** `aviso` para lo que no es un error suyo (cambiar la contraseña); por omisión, `error`. */
30
+ tono?: "aviso" | "error";
31
+ /** El rechazo del servidor, si trae motivos por campo (un 400): se cuelgan de cada uno. */
32
+ errorDelServidor?: unknown;
33
+ };
34
+
35
+ /**
36
+ * La pantalla de entrada de las apps: el logo con el nombre de la app, usuario, contraseña e
37
+ * «Iniciar Sesión», y la versión abajo. En una tableta va en una tarjeta centrada; en un teléfono,
38
+ * directo sobre el fondo.
39
+ *
40
+ * Lo de cada app es `onEntrar`: llama a su login y dice qué contestar a cada respuesta (una cuenta
41
+ * deshabilitada, una que no tiene acceso a esta app). El aviso sale **arriba**, porque el teclado
42
+ * tapa uno anclado abajo, y dura lo que tarda en leerse.
43
+ */
44
+ export default function PantallaDeAcceso({
45
+ nombre,
46
+ version,
47
+ onEntrar,
48
+ }: {
49
+ /** Lo que va bajo el logo: «TMS». */
50
+ nombre: string;
51
+ version?: string | null;
52
+ onEntrar: (datos: DatosDeAcceso) => Promise<ResultadoDeAcceso>;
53
+ }) {
54
+ const c = usePaleta();
55
+ const styles = useStyles();
56
+ const insets = useSafeAreaInsets();
57
+ const { width } = useWindowDimensions();
58
+ const form = useForm<DatosDeAcceso>({ defaultValues: { username: "", password: "" } });
59
+ const [entrando, setEntrando] = useState(false);
60
+ const [aviso, setAviso] = useState<string | null>(null);
61
+
62
+ const entrar = form.handleSubmit(async (datos) => {
63
+ setEntrando(true);
64
+ try {
65
+ const resultado = await onEntrar(datos);
66
+ if (!resultado) {
67
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
68
+ return;
69
+ }
70
+ Haptics.notificationAsync(
71
+ resultado.tono === "aviso"
72
+ ? Haptics.NotificationFeedbackType.Warning
73
+ : Haptics.NotificationFeedbackType.Error,
74
+ );
75
+ if (resultado.errorDelServidor) setErrors(form, resultado.errorDelServidor);
76
+ setAviso(resultado.mensaje);
77
+ } catch {
78
+ // `onEntrar` contesta lo que conoce; lo que se le escape no deja el botón cargando.
79
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
80
+ setAviso("No se pudo iniciar sesión. Intente de nuevo.");
81
+ } finally {
82
+ setEntrando(false);
83
+ }
84
+ });
85
+
86
+ const contenido = (
87
+ <>
88
+ <View style={styles.marca}>
89
+ <LogoCompleto />
90
+ {/* La itálica es sintética (ni Inter ni San Francisco traen corte cursivo): la
91
+ inclinación saca la última letra del ancho que se midió y se corta contra el
92
+ borde. El `paddingHorizontal` le devuelve ese aire. */}
93
+ <Text style={styles.nombre}>{nombre}</Text>
94
+ </View>
95
+
96
+ <View style={styles.campos}>
97
+ <TextoDeAccesoControlado
98
+ name="username"
99
+ control={form.control}
100
+ etiqueta="Usuario"
101
+ entrada={{ autoCapitalize: "none" }}
102
+ />
103
+ <TextoDeAccesoControlado
104
+ name="password"
105
+ control={form.control}
106
+ etiqueta="Contraseña"
107
+ contrasena
108
+ entrada={{ autoCapitalize: "none" }}
109
+ />
110
+ </View>
111
+
112
+ <Button
113
+ mode={"contained"}
114
+ loading={entrando}
115
+ disabled={entrando}
116
+ onPress={entrar}
117
+ style={styles.boton}
118
+ >
119
+ Iniciar Sesión
120
+ </Button>
121
+ </>
122
+ );
123
+
124
+ return (
125
+ <View style={styles.root}>
126
+ {/* `behavior="padding"` funciona en ambas plataformas con edge-to-edge gracias a
127
+ react-native-keyboard-controller (el de RN lo ignora en Android). */}
128
+ <KeyboardAvoidingView style={styles.centro} behavior="padding">
129
+ {/* El tope de ancho vive en este View y no en la Card: el Surface de Paper reparte
130
+ el estilo en dos capas anidadas en iOS y `maxWidth` cae en la de adentro, que
131
+ queda pegada a la izquierda de la de afuera. La Card sólo se estira. */}
132
+ <View style={styles.columna}>
133
+ {width > ANCHO_DE_TABLETA ? (
134
+ <Card style={styles.card}>
135
+ <Card.Content>{contenido}</Card.Content>
136
+ </Card>
137
+ ) : (
138
+ contenido
139
+ )}
140
+ </View>
141
+ </KeyboardAvoidingView>
142
+
143
+ {version ? (
144
+ <View style={styles.version}>
145
+ <Chip
146
+ compact
147
+ textStyle={{ color: c.secondary, fontSize: 11 }}
148
+ style={{ backgroundColor: c.superficieTenue }}
149
+ >
150
+ v {version}
151
+ </Chip>
152
+ </View>
153
+ ) : null}
154
+
155
+ <Snackbar
156
+ visible={aviso !== null}
157
+ onDismiss={() => setAviso(null)}
158
+ // Un aviso largo se queda lo que tarda en leerse.
159
+ duration={Math.max(2400, (aviso?.length ?? 0) * 60)}
160
+ wrapperStyle={{ top: insets.top + 12, bottom: undefined }}
161
+ >
162
+ {aviso}
163
+ </Snackbar>
164
+ </View>
165
+ );
166
+ }
167
+
168
+ const useStyles = crearEstilos((c) => ({
169
+ root: { flex: 1, backgroundColor: c.fondo },
170
+ centro: { flex: 1, justifyContent: "center", padding: 24, backgroundColor: c.fondo },
171
+ columna: { width: "100%", maxWidth: 400, alignSelf: "center" },
172
+ card: { backgroundColor: c.superficie },
173
+ marca: { alignItems: "center", marginBottom: 20 },
174
+ nombre: {
175
+ fontSize: 15,
176
+ color: c.primary,
177
+ fontWeight: "bold",
178
+ fontStyle: "italic",
179
+ paddingHorizontal: 3,
180
+ },
181
+ campos: { gap: 15, paddingBottom: 20 },
182
+ boton: { marginTop: 10, borderRadius: 15 },
183
+ version: { position: "absolute", bottom: 50, left: 0, right: 0, alignItems: "center" },
184
+ }));
@@ -0,0 +1,116 @@
1
+ import React from "react";
2
+ import { ActivityIndicator, ScrollView, View } from "react-native";
3
+ import { KeyboardAvoidingView } from "react-native-keyboard-controller";
4
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
5
+ import BotonGrande from "../componentes/BotonGrande";
6
+ import CabeceraEmpujada from "../componentes/CabeceraEmpujada";
7
+ import EstadoVacio from "../componentes/EstadoVacio";
8
+ import Nota from "../componentes/Nota";
9
+ import { mensajeDeError } from "../formularios/errores";
10
+ import { crearEstilos, usePaleta } from "../tema";
11
+
12
+ /**
13
+ * El armazón de una pantalla que edita algo (un viaje, un tracto, una caja, un operador). Dos
14
+ * partes, porque el formulario nace cuando ya llegó lo que edita (sus valores iniciales salen de
15
+ * ahí):
16
+ *
17
+ * - `EsperaDeEdicion`: lo que se ve antes: sin permiso, cargando o si falló, con «Reintentar».
18
+ * - `FormularioDeEdicion`: la cabecera, los campos en un scroll que esquiva el teclado, el error
19
+ * del último intento y «Guardar» abajo, fijo.
20
+ */
21
+
22
+ /** Lo que la espera lee de un `use…Query` de RTK Query. */
23
+ interface ConsultaDeEdicion {
24
+ isError: boolean;
25
+ error?: unknown;
26
+ refetch: () => unknown;
27
+ }
28
+
29
+ export function EsperaDeEdicion({
30
+ eyebrow,
31
+ puede,
32
+ sinPermiso,
33
+ consulta,
34
+ queEs,
35
+ }: {
36
+ eyebrow: string;
37
+ puede: boolean;
38
+ /** Qué permiso falta, dicho para quien lo lee. */
39
+ sinPermiso: string;
40
+ consulta: ConsultaDeEdicion;
41
+ /** «el viaje», «el tracto»: para «No se pudo cargar el tracto». */
42
+ queEs: string;
43
+ }) {
44
+ const c = usePaleta();
45
+ const styles = useStyles();
46
+ return (
47
+ <View style={styles.root}>
48
+ <CabeceraEmpujada eyebrow={eyebrow} titulo="…" />
49
+ {!puede ? (
50
+ <EstadoVacio icono="lock-outline" titulo="No tienes permiso" texto={sinPermiso} />
51
+ ) : consulta.isError ? (
52
+ <EstadoVacio
53
+ icono="cloud-alert"
54
+ titulo={`No se pudo cargar ${queEs}`}
55
+ texto={mensajeDeError(consulta.error, "Revisa la señal e intenta de nuevo.")}
56
+ >
57
+ <BotonGrande icono="refresh" modo="contorno" onPress={consulta.refetch}>
58
+ Reintentar
59
+ </BotonGrande>
60
+ </EstadoVacio>
61
+ ) : (
62
+ <ActivityIndicator color={c.primary} style={styles.cargando} />
63
+ )}
64
+ </View>
65
+ );
66
+ }
67
+
68
+ export function FormularioDeEdicion({
69
+ eyebrow,
70
+ titulo,
71
+ error,
72
+ guardando,
73
+ onGuardar,
74
+ textoDeGuardar = "Guardar",
75
+ children,
76
+ }: {
77
+ eyebrow: string;
78
+ titulo: string;
79
+ /** Lo que dijo el servidor la última vez que no guardó. */
80
+ error: string | null;
81
+ guardando: boolean;
82
+ onGuardar: () => void;
83
+ /** Lo que dice el botón: «Guardar», o lo que haga («Registrar la renovación»). */
84
+ textoDeGuardar?: string;
85
+ children: React.ReactNode;
86
+ }) {
87
+ const styles = useStyles();
88
+ const insets = useSafeAreaInsets();
89
+ return (
90
+ <View style={styles.root}>
91
+ <CabeceraEmpujada eyebrow={eyebrow} titulo={titulo} />
92
+ <KeyboardAvoidingView behavior="padding" style={styles.flex}>
93
+ <ScrollView
94
+ contentContainerStyle={styles.contenido}
95
+ keyboardShouldPersistTaps="handled"
96
+ >
97
+ {children}
98
+ {error ? <Nota tono="aviso" texto={error} /> : null}
99
+ </ScrollView>
100
+ <View style={[styles.pie, { paddingBottom: insets.bottom + 16 }]}>
101
+ <BotonGrande icono="check" onPress={onGuardar} cargando={guardando}>
102
+ {guardando ? "Guardando…" : textoDeGuardar}
103
+ </BotonGrande>
104
+ </View>
105
+ </KeyboardAvoidingView>
106
+ </View>
107
+ );
108
+ }
109
+
110
+ const useStyles = crearEstilos((c) => ({
111
+ root: { flex: 1, backgroundColor: c.fondo },
112
+ flex: { flex: 1 },
113
+ cargando: { marginTop: 24 },
114
+ contenido: { padding: 20, gap: 12 },
115
+ pie: { paddingHorizontal: 20, paddingTop: 12, backgroundColor: c.superficie },
116
+ }));