@acvl/mobile-components 0.0.0-stage → 0.0.2

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 (322) hide show
  1. package/README.md +147 -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/camara/CampoDeFotos.js +124 -0
  10. package/lib/module/camara/CampoDeFotos.js.map +1 -0
  11. package/lib/module/camara/elegir.js +104 -0
  12. package/lib/module/camara/elegir.js.map +1 -0
  13. package/lib/module/camara/index.js +10 -0
  14. package/lib/module/camara/index.js.map +1 -0
  15. package/lib/module/camara/reducir.js +93 -0
  16. package/lib/module/camara/reducir.js.map +1 -0
  17. package/lib/module/componentes/AccionesDelEncabezado.js +212 -0
  18. package/lib/module/componentes/AccionesDelEncabezado.js.map +1 -0
  19. package/lib/module/componentes/BotonDeAvatar.js +55 -0
  20. package/lib/module/componentes/BotonDeAvatar.js.map +1 -0
  21. package/lib/module/componentes/BotonGrande.js +105 -0
  22. package/lib/module/componentes/BotonGrande.js.map +1 -0
  23. package/lib/module/componentes/CabeceraEmpujada.js +101 -0
  24. package/lib/module/componentes/CabeceraEmpujada.js.map +1 -0
  25. package/lib/module/componentes/Chip.js +77 -0
  26. package/lib/module/componentes/Chip.js.map +1 -0
  27. package/lib/module/componentes/ContenidoDePantalla.js +56 -0
  28. package/lib/module/componentes/ContenidoDePantalla.js.map +1 -0
  29. package/lib/module/componentes/EstadoVacio.js +65 -0
  30. package/lib/module/componentes/EstadoVacio.js.map +1 -0
  31. package/lib/module/componentes/Foto.js +106 -0
  32. package/lib/module/componentes/Foto.js.map +1 -0
  33. package/lib/module/componentes/IconoDeRenglon.js +44 -0
  34. package/lib/module/componentes/IconoDeRenglon.js.map +1 -0
  35. package/lib/module/componentes/Logo.js +44 -0
  36. package/lib/module/componentes/Logo.js.map +1 -0
  37. package/lib/module/componentes/Nota.js +51 -0
  38. package/lib/module/componentes/Nota.js.map +1 -0
  39. package/lib/module/componentes/OpcionesExcluyentes.js +69 -0
  40. package/lib/module/componentes/OpcionesExcluyentes.js.map +1 -0
  41. package/lib/module/componentes/PantallaDeError.js +84 -0
  42. package/lib/module/componentes/PantallaDeError.js.map +1 -0
  43. package/lib/module/componentes/Pestanas.js +87 -0
  44. package/lib/module/componentes/Pestanas.js.map +1 -0
  45. package/lib/module/componentes/Rotulo.js +28 -0
  46. package/lib/module/componentes/Rotulo.js.map +1 -0
  47. package/lib/module/componentes/RotuloDeSeccion.js +87 -0
  48. package/lib/module/componentes/RotuloDeSeccion.js.map +1 -0
  49. package/lib/module/componentes/Tarjeta.js +63 -0
  50. package/lib/module/componentes/Tarjeta.js.map +1 -0
  51. package/lib/module/componentes/TarjetaDeDatos.js +79 -0
  52. package/lib/module/componentes/TarjetaDeDatos.js.map +1 -0
  53. package/lib/module/componentes/TiraDeFotos.js +184 -0
  54. package/lib/module/componentes/TiraDeFotos.js.map +1 -0
  55. package/lib/module/componentes/VisorDeFoto.js +171 -0
  56. package/lib/module/componentes/VisorDeFoto.js.map +1 -0
  57. package/lib/module/componentes/logos/bola-blanca.png +0 -0
  58. package/lib/module/componentes/logos/bola.png +0 -0
  59. package/lib/module/componentes/logos/logo-blanco.png +0 -0
  60. package/lib/module/componentes/logos/logo.png +0 -0
  61. package/lib/module/dialogos/HostDeDialogos.js +170 -0
  62. package/lib/module/dialogos/HostDeDialogos.js.map +1 -0
  63. package/lib/module/dialogos/dialogos.js +112 -0
  64. package/lib/module/dialogos/dialogos.js.map +1 -0
  65. package/lib/module/formularios/CampoDeSeleccion.js +72 -0
  66. package/lib/module/formularios/CampoDeSeleccion.js.map +1 -0
  67. package/lib/module/formularios/SelectorDeOpcion.js +406 -0
  68. package/lib/module/formularios/SelectorDeOpcion.js.map +1 -0
  69. package/lib/module/formularios/TextoDeAccesoControlado.js +73 -0
  70. package/lib/module/formularios/TextoDeAccesoControlado.js.map +1 -0
  71. package/lib/module/formularios/campos.js +501 -0
  72. package/lib/module/formularios/campos.js.map +1 -0
  73. package/lib/module/formularios/controlados.js +199 -0
  74. package/lib/module/formularios/controlados.js.map +1 -0
  75. package/lib/module/formularios/errores.js +91 -0
  76. package/lib/module/formularios/errores.js.map +1 -0
  77. package/lib/module/formularios/guardado.js +78 -0
  78. package/lib/module/formularios/guardado.js.map +1 -0
  79. package/lib/module/formularios/useBusqueda.js +22 -0
  80. package/lib/module/formularios/useBusqueda.js.map +1 -0
  81. package/lib/module/fotos/elegir.js +105 -0
  82. package/lib/module/fotos/elegir.js.map +1 -0
  83. package/lib/module/fotos/index.js +9 -0
  84. package/lib/module/fotos/index.js.map +1 -0
  85. package/lib/module/hoja/HojaDeLista.js +211 -0
  86. package/lib/module/hoja/HojaDeLista.js.map +1 -0
  87. package/lib/module/hoja/HojaInferior.js +112 -0
  88. package/lib/module/hoja/HojaInferior.js.map +1 -0
  89. package/lib/module/hoja/index.js +8 -0
  90. package/lib/module/hoja/index.js.map +1 -0
  91. package/lib/module/hoja/useMasDeLaFicha.js +48 -0
  92. package/lib/module/hoja/useMasDeLaFicha.js.map +1 -0
  93. package/lib/module/index.js +67 -0
  94. package/lib/module/index.js.map +1 -0
  95. package/lib/module/listas/HojaDeFiltros.js +270 -0
  96. package/lib/module/listas/HojaDeFiltros.js.map +1 -0
  97. package/lib/module/listas/ListaPaginada.js +103 -0
  98. package/lib/module/listas/ListaPaginada.js.map +1 -0
  99. package/lib/module/listas/TarjetaDeslizable.js +118 -0
  100. package/lib/module/listas/TarjetaDeslizable.js.map +1 -0
  101. package/lib/module/listas/paginado.js +11 -0
  102. package/lib/module/listas/paginado.js.map +1 -0
  103. package/lib/module/listas/piezas.js +666 -0
  104. package/lib/module/listas/piezas.js.map +1 -0
  105. package/lib/module/navegacion/BarraDePestanas.js +162 -0
  106. package/lib/module/navegacion/BarraDePestanas.js.map +1 -0
  107. package/lib/module/navegacion/BotonDeBarra.js +58 -0
  108. package/lib/module/navegacion/BotonDeBarra.js.map +1 -0
  109. package/lib/module/navegacion/encabezado.js +103 -0
  110. package/lib/module/navegacion/encabezado.js.map +1 -0
  111. package/lib/module/pantallas/PantallaDeAcceso.js +195 -0
  112. package/lib/module/pantallas/PantallaDeAcceso.js.map +1 -0
  113. package/lib/module/pantallas/PantallaDeEdicion.js +121 -0
  114. package/lib/module/pantallas/PantallaDeEdicion.js.map +1 -0
  115. package/lib/module/pantallas/PantallaDeFicha.js +94 -0
  116. package/lib/module/pantallas/PantallaDeFicha.js.map +1 -0
  117. package/lib/module/pantallas/PantallaDeHistorial.js +328 -0
  118. package/lib/module/pantallas/PantallaDeHistorial.js.map +1 -0
  119. package/lib/module/pantallas/PantallaDePerfil.js +137 -0
  120. package/lib/module/pantallas/PantallaDePerfil.js.map +1 -0
  121. package/lib/module/tema/ProveedorDeTema.js +122 -0
  122. package/lib/module/tema/ProveedorDeTema.js.map +1 -0
  123. package/lib/module/tema/estilos.js +54 -0
  124. package/lib/module/tema/estilos.js.map +1 -0
  125. package/lib/module/tema/fuentes.js +28 -0
  126. package/lib/module/tema/fuentes.js.map +1 -0
  127. package/lib/module/tema/index.js +8 -0
  128. package/lib/module/tema/index.js.map +1 -0
  129. package/lib/module/tema/paleta.js +107 -0
  130. package/lib/module/tema/paleta.js.map +1 -0
  131. package/lib/module/tema/tipografia.js +32 -0
  132. package/lib/module/tema/tipografia.js.map +1 -0
  133. package/lib/module/texto.js +48 -0
  134. package/lib/module/texto.js.map +1 -0
  135. package/lib/typescript/src/camara/CampoDeFotos.d.ts +30 -0
  136. package/lib/typescript/src/camara/CampoDeFotos.d.ts.map +1 -0
  137. package/lib/typescript/src/camara/elegir.d.ts +47 -0
  138. package/lib/typescript/src/camara/elegir.d.ts.map +1 -0
  139. package/lib/typescript/src/camara/index.d.ts +4 -0
  140. package/lib/typescript/src/camara/index.d.ts.map +1 -0
  141. package/lib/typescript/src/camara/reducir.d.ts +32 -0
  142. package/lib/typescript/src/camara/reducir.d.ts.map +1 -0
  143. package/lib/typescript/src/componentes/AccionesDelEncabezado.d.ts +34 -0
  144. package/lib/typescript/src/componentes/AccionesDelEncabezado.d.ts.map +1 -0
  145. package/lib/typescript/src/componentes/BotonDeAvatar.d.ts +17 -0
  146. package/lib/typescript/src/componentes/BotonDeAvatar.d.ts.map +1 -0
  147. package/lib/typescript/src/componentes/BotonGrande.d.ts +26 -0
  148. package/lib/typescript/src/componentes/BotonGrande.d.ts.map +1 -0
  149. package/lib/typescript/src/componentes/CabeceraEmpujada.d.ts +18 -0
  150. package/lib/typescript/src/componentes/CabeceraEmpujada.d.ts.map +1 -0
  151. package/lib/typescript/src/componentes/Chip.d.ts +14 -0
  152. package/lib/typescript/src/componentes/Chip.d.ts.map +1 -0
  153. package/lib/typescript/src/componentes/ContenidoDePantalla.d.ts +17 -0
  154. package/lib/typescript/src/componentes/ContenidoDePantalla.d.ts.map +1 -0
  155. package/lib/typescript/src/componentes/EstadoVacio.d.ts +17 -0
  156. package/lib/typescript/src/componentes/EstadoVacio.d.ts.map +1 -0
  157. package/lib/typescript/src/componentes/Foto.d.ts +41 -0
  158. package/lib/typescript/src/componentes/Foto.d.ts.map +1 -0
  159. package/lib/typescript/src/componentes/IconoDeRenglon.d.ts +25 -0
  160. package/lib/typescript/src/componentes/IconoDeRenglon.d.ts.map +1 -0
  161. package/lib/typescript/src/componentes/Logo.d.ts +13 -0
  162. package/lib/typescript/src/componentes/Logo.d.ts.map +1 -0
  163. package/lib/typescript/src/componentes/Nota.d.ts +7 -0
  164. package/lib/typescript/src/componentes/Nota.d.ts.map +1 -0
  165. package/lib/typescript/src/componentes/OpcionesExcluyentes.d.ts +14 -0
  166. package/lib/typescript/src/componentes/OpcionesExcluyentes.d.ts.map +1 -0
  167. package/lib/typescript/src/componentes/PantallaDeError.d.ts +23 -0
  168. package/lib/typescript/src/componentes/PantallaDeError.d.ts.map +1 -0
  169. package/lib/typescript/src/componentes/Pestanas.d.ts +28 -0
  170. package/lib/typescript/src/componentes/Pestanas.d.ts.map +1 -0
  171. package/lib/typescript/src/componentes/Rotulo.d.ts +6 -0
  172. package/lib/typescript/src/componentes/Rotulo.d.ts.map +1 -0
  173. package/lib/typescript/src/componentes/RotuloDeSeccion.d.ts +20 -0
  174. package/lib/typescript/src/componentes/RotuloDeSeccion.d.ts.map +1 -0
  175. package/lib/typescript/src/componentes/Tarjeta.d.ts +25 -0
  176. package/lib/typescript/src/componentes/Tarjeta.d.ts.map +1 -0
  177. package/lib/typescript/src/componentes/TarjetaDeDatos.d.ts +13 -0
  178. package/lib/typescript/src/componentes/TarjetaDeDatos.d.ts.map +1 -0
  179. package/lib/typescript/src/componentes/TiraDeFotos.d.ts +36 -0
  180. package/lib/typescript/src/componentes/TiraDeFotos.d.ts.map +1 -0
  181. package/lib/typescript/src/componentes/VisorDeFoto.d.ts +23 -0
  182. package/lib/typescript/src/componentes/VisorDeFoto.d.ts.map +1 -0
  183. package/lib/typescript/src/dialogos/HostDeDialogos.d.ts +14 -0
  184. package/lib/typescript/src/dialogos/HostDeDialogos.d.ts.map +1 -0
  185. package/lib/typescript/src/dialogos/dialogos.d.ts +65 -0
  186. package/lib/typescript/src/dialogos/dialogos.d.ts.map +1 -0
  187. package/lib/typescript/src/formularios/CampoDeSeleccion.d.ts +24 -0
  188. package/lib/typescript/src/formularios/CampoDeSeleccion.d.ts.map +1 -0
  189. package/lib/typescript/src/formularios/SelectorDeOpcion.d.ts +59 -0
  190. package/lib/typescript/src/formularios/SelectorDeOpcion.d.ts.map +1 -0
  191. package/lib/typescript/src/formularios/TextoDeAccesoControlado.d.ts +18 -0
  192. package/lib/typescript/src/formularios/TextoDeAccesoControlado.d.ts.map +1 -0
  193. package/lib/typescript/src/formularios/campos.d.ts +113 -0
  194. package/lib/typescript/src/formularios/campos.d.ts.map +1 -0
  195. package/lib/typescript/src/formularios/controlados.d.ts +29 -0
  196. package/lib/typescript/src/formularios/controlados.d.ts.map +1 -0
  197. package/lib/typescript/src/formularios/errores.d.ts +36 -0
  198. package/lib/typescript/src/formularios/errores.d.ts.map +1 -0
  199. package/lib/typescript/src/formularios/guardado.d.ts +36 -0
  200. package/lib/typescript/src/formularios/guardado.d.ts.map +1 -0
  201. package/lib/typescript/src/formularios/useBusqueda.d.ts +7 -0
  202. package/lib/typescript/src/formularios/useBusqueda.d.ts.map +1 -0
  203. package/lib/typescript/src/fotos/elegir.d.ts +28 -0
  204. package/lib/typescript/src/fotos/elegir.d.ts.map +1 -0
  205. package/lib/typescript/src/fotos/index.d.ts +3 -0
  206. package/lib/typescript/src/fotos/index.d.ts.map +1 -0
  207. package/lib/typescript/src/hoja/HojaDeLista.d.ts +50 -0
  208. package/lib/typescript/src/hoja/HojaDeLista.d.ts.map +1 -0
  209. package/lib/typescript/src/hoja/HojaInferior.d.ts +21 -0
  210. package/lib/typescript/src/hoja/HojaInferior.d.ts.map +1 -0
  211. package/lib/typescript/src/hoja/index.d.ts +4 -0
  212. package/lib/typescript/src/hoja/index.d.ts.map +1 -0
  213. package/lib/typescript/src/hoja/useMasDeLaFicha.d.ts +27 -0
  214. package/lib/typescript/src/hoja/useMasDeLaFicha.d.ts.map +1 -0
  215. package/lib/typescript/src/index.d.ts +46 -0
  216. package/lib/typescript/src/index.d.ts.map +1 -0
  217. package/lib/typescript/src/listas/HojaDeFiltros.d.ts +76 -0
  218. package/lib/typescript/src/listas/HojaDeFiltros.d.ts.map +1 -0
  219. package/lib/typescript/src/listas/ListaPaginada.d.ts +39 -0
  220. package/lib/typescript/src/listas/ListaPaginada.d.ts.map +1 -0
  221. package/lib/typescript/src/listas/TarjetaDeslizable.d.ts +33 -0
  222. package/lib/typescript/src/listas/TarjetaDeslizable.d.ts.map +1 -0
  223. package/lib/typescript/src/listas/paginado.d.ts +16 -0
  224. package/lib/typescript/src/listas/paginado.d.ts.map +1 -0
  225. package/lib/typescript/src/listas/piezas.d.ts +157 -0
  226. package/lib/typescript/src/listas/piezas.d.ts.map +1 -0
  227. package/lib/typescript/src/navegacion/BarraDePestanas.d.ts +57 -0
  228. package/lib/typescript/src/navegacion/BarraDePestanas.d.ts.map +1 -0
  229. package/lib/typescript/src/navegacion/BotonDeBarra.d.ts +15 -0
  230. package/lib/typescript/src/navegacion/BotonDeBarra.d.ts.map +1 -0
  231. package/lib/typescript/src/navegacion/encabezado.d.ts +44 -0
  232. package/lib/typescript/src/navegacion/encabezado.d.ts.map +1 -0
  233. package/lib/typescript/src/pantallas/PantallaDeAcceso.d.ts +32 -0
  234. package/lib/typescript/src/pantallas/PantallaDeAcceso.d.ts.map +1 -0
  235. package/lib/typescript/src/pantallas/PantallaDeEdicion.d.ts +38 -0
  236. package/lib/typescript/src/pantallas/PantallaDeEdicion.d.ts.map +1 -0
  237. package/lib/typescript/src/pantallas/PantallaDeFicha.d.ts +35 -0
  238. package/lib/typescript/src/pantallas/PantallaDeFicha.d.ts.map +1 -0
  239. package/lib/typescript/src/pantallas/PantallaDeHistorial.d.ts +65 -0
  240. package/lib/typescript/src/pantallas/PantallaDeHistorial.d.ts.map +1 -0
  241. package/lib/typescript/src/pantallas/PantallaDePerfil.d.ts +19 -0
  242. package/lib/typescript/src/pantallas/PantallaDePerfil.d.ts.map +1 -0
  243. package/lib/typescript/src/tema/ProveedorDeTema.d.ts +32 -0
  244. package/lib/typescript/src/tema/ProveedorDeTema.d.ts.map +1 -0
  245. package/lib/typescript/src/tema/estilos.d.ts +31 -0
  246. package/lib/typescript/src/tema/estilos.d.ts.map +1 -0
  247. package/lib/typescript/src/tema/fuentes.d.ts +20 -0
  248. package/lib/typescript/src/tema/fuentes.d.ts.map +1 -0
  249. package/lib/typescript/src/tema/index.d.ts +6 -0
  250. package/lib/typescript/src/tema/index.d.ts.map +1 -0
  251. package/lib/typescript/src/tema/paleta.d.ts +86 -0
  252. package/lib/typescript/src/tema/paleta.d.ts.map +1 -0
  253. package/lib/typescript/src/tema/tipografia.d.ts +29 -0
  254. package/lib/typescript/src/tema/tipografia.d.ts.map +1 -0
  255. package/lib/typescript/src/texto.d.ts +16 -0
  256. package/lib/typescript/src/texto.d.ts.map +1 -0
  257. package/package.json +140 -5
  258. package/src/camara/CampoDeFotos.tsx +132 -0
  259. package/src/camara/elegir.ts +113 -0
  260. package/src/camara/index.ts +15 -0
  261. package/src/camara/reducir.ts +87 -0
  262. package/src/componentes/AccionesDelEncabezado.tsx +247 -0
  263. package/src/componentes/BotonDeAvatar.tsx +59 -0
  264. package/src/componentes/BotonGrande.tsx +100 -0
  265. package/src/componentes/CabeceraEmpujada.tsx +69 -0
  266. package/src/componentes/Chip.tsx +60 -0
  267. package/src/componentes/ContenidoDePantalla.tsx +53 -0
  268. package/src/componentes/EstadoVacio.tsx +45 -0
  269. package/src/componentes/Foto.tsx +92 -0
  270. package/src/componentes/IconoDeRenglon.tsx +42 -0
  271. package/src/componentes/Logo.tsx +34 -0
  272. package/src/componentes/Nota.tsx +37 -0
  273. package/src/componentes/OpcionesExcluyentes.tsx +62 -0
  274. package/src/componentes/PantallaDeError.tsx +76 -0
  275. package/src/componentes/Pestanas.tsx +86 -0
  276. package/src/componentes/Rotulo.tsx +21 -0
  277. package/src/componentes/RotuloDeSeccion.tsx +71 -0
  278. package/src/componentes/Tarjeta.tsx +58 -0
  279. package/src/componentes/TarjetaDeDatos.tsx +62 -0
  280. package/src/componentes/TiraDeFotos.tsx +189 -0
  281. package/src/componentes/VisorDeFoto.tsx +181 -0
  282. package/src/componentes/logos/bola-blanca.png +0 -0
  283. package/src/componentes/logos/bola.png +0 -0
  284. package/src/componentes/logos/logo-blanco.png +0 -0
  285. package/src/componentes/logos/logo.png +0 -0
  286. package/src/dialogos/HostDeDialogos.tsx +172 -0
  287. package/src/dialogos/dialogos.ts +148 -0
  288. package/src/formularios/CampoDeSeleccion.tsx +60 -0
  289. package/src/formularios/SelectorDeOpcion.tsx +407 -0
  290. package/src/formularios/TextoDeAccesoControlado.tsx +72 -0
  291. package/src/formularios/campos.tsx +497 -0
  292. package/src/formularios/controlados.tsx +200 -0
  293. package/src/formularios/errores.ts +105 -0
  294. package/src/formularios/guardado.ts +76 -0
  295. package/src/formularios/useBusqueda.ts +19 -0
  296. package/src/fotos/elegir.ts +116 -0
  297. package/src/fotos/index.ts +6 -0
  298. package/src/hoja/HojaDeLista.tsx +214 -0
  299. package/src/hoja/HojaInferior.tsx +103 -0
  300. package/src/hoja/index.ts +5 -0
  301. package/src/hoja/useMasDeLaFicha.tsx +63 -0
  302. package/src/index.ts +151 -0
  303. package/src/listas/HojaDeFiltros.tsx +286 -0
  304. package/src/listas/ListaPaginada.tsx +117 -0
  305. package/src/listas/TarjetaDeslizable.tsx +126 -0
  306. package/src/listas/paginado.ts +15 -0
  307. package/src/listas/piezas.tsx +670 -0
  308. package/src/navegacion/BarraDePestanas.tsx +198 -0
  309. package/src/navegacion/BotonDeBarra.tsx +61 -0
  310. package/src/navegacion/encabezado.tsx +88 -0
  311. package/src/pantallas/PantallaDeAcceso.tsx +184 -0
  312. package/src/pantallas/PantallaDeEdicion.tsx +116 -0
  313. package/src/pantallas/PantallaDeFicha.tsx +115 -0
  314. package/src/pantallas/PantallaDeHistorial.tsx +293 -0
  315. package/src/pantallas/PantallaDePerfil.tsx +126 -0
  316. package/src/tema/ProveedorDeTema.tsx +150 -0
  317. package/src/tema/estilos.ts +51 -0
  318. package/src/tema/fuentes.ts +20 -0
  319. package/src/tema/index.ts +10 -0
  320. package/src/tema/paleta.ts +147 -0
  321. package/src/tema/tipografia.ts +29 -0
  322. package/src/texto.tsx +44 -0
@@ -0,0 +1,105 @@
1
+ /** Si un texto es una página HTML y no un mensaje. */
2
+ const pareceHtml = (texto: string) => /^\s*</.test(texto) || /<(html|body|head)[\s>]/i.test(texto);
3
+
4
+ /**
5
+ * El mensaje que el backend mandó, listo para enseñárselo al usuario.
6
+ *
7
+ * Los errores de los backends de ACV salen de `drf_simple_api_errors`, que responde
8
+ * `{ title, detail?: string[], invalid_params?: [{ name, reason: string[] }] }`. Llegan en dos
9
+ * formas y se leen las dos:
10
+ *
11
+ * - `detail`: los errores de regla, que no cuelgan de un campo (el orden de los pasos, «falta lo
12
+ * que la acción pide»). Si vienen varios se juntan todos, uno por renglón: enseñar sólo el
13
+ * primero obligaba a corregir y reintentar para descubrir el siguiente.
14
+ * - `invalid_params`: los de un campo (`clave`, `lng`, `fecha_real`), cada uno con su motivo.
15
+ *
16
+ * `title` es la etiqueta del tipo de error («Validation error.»), que sola no le dice nada a
17
+ * nadie: sólo se usa si no hay nada más.
18
+ *
19
+ * Sirve para el feedback de una acción —un aviso, Pendientes—, no para pintar errores en los
20
+ * campos de un formulario: eso lo hace `setErrors` con `invalid_params`.
21
+ */
22
+ export const mensajeDeError = (error: any, fallback: string): string => {
23
+ const data = error?.data;
24
+
25
+ if (typeof data === "string" && data) {
26
+ // Una página de error de un proxy (un 413 o un 502 de nginx) llega como `PARSING_ERROR`
27
+ // con el HTML en `data`: enseñarla tal cual llenaba Pendientes de etiquetas. Va el
28
+ // mensaje genérico con el código.
29
+ if (pareceHtml(data)) {
30
+ const codigo = error?.originalStatus ?? error?.status;
31
+ return typeof codigo === "number" ? `${fallback} (error ${codigo})` : fallback;
32
+ }
33
+ return data;
34
+ }
35
+
36
+ if (data && typeof data === "object") {
37
+ const renglones: string[] = [];
38
+
39
+ if (Array.isArray(data.detail)) renglones.push(...data.detail.map(String));
40
+ else if (typeof data.detail === "string" && data.detail) renglones.push(data.detail);
41
+
42
+ for (const param of Array.isArray(data.invalid_params) ? data.invalid_params : []) {
43
+ const razon = Array.isArray(param?.reason) ? param.reason.join(" ") : param?.reason;
44
+ if (razon) renglones.push(String(razon));
45
+ }
46
+
47
+ const limpios = renglones.map((r) => r.trim()).filter(Boolean);
48
+ if (limpios.length) return limpios.join("\n");
49
+
50
+ if (typeof data.title === "string" && data.title) return data.title;
51
+ }
52
+
53
+ // Un `Error` de la propia app (p. ej. el dispositivo no sabe compartir archivos) no trae
54
+ // `data`: su mensaje ya está escrito para el usuario.
55
+ if (typeof error?.message === "string" && error.message) return error.message;
56
+
57
+ return fallback;
58
+ };
59
+
60
+ /**
61
+ * Vuelca los motivos de un 400 sobre los campos del formulario.
62
+ *
63
+ * El backend responde en RFC 7807 (`invalid_params: [{ name, reason }]`). Un campo suelto trae
64
+ * su motivo como lista de textos y se cuelga de su nombre tal cual. Una **lista editable**
65
+ * (`useFieldArray`) trae en su lugar un objeto por renglón —vacío si ese
66
+ * renglón está bien—, que es como DRF reporta un serializador anidado `many=True`; ahí el motivo
67
+ * va a `lista.<índice>.<campo>`, que es justo como se llaman los inputs de la fila, para que se
68
+ * pinte sobre el campo que falla.
69
+ *
70
+ * Sin ese segundo caso el error quedaba a nombre de la lista entera, donde no hay ningún input
71
+ * registrado: quien capturaba sólo veía el aviso genérico de que no se pudo guardar. Es la misma
72
+ * traducción que hace el portal (`setErrors` de @acvl/frontend-components), así que la forma de
73
+ * la respuesta es una sola para los dos clientes.
74
+ */
75
+ export const setErrors = (form: any, error: any) => {
76
+ // Sin respuesta (una falla de red) o un 400 de regla sin campos (`invalid_params: null`): no
77
+ // hay nada que colgar de los campos, y quien llama ya enseña el aviso general.
78
+ const params = error?.data?.invalid_params;
79
+ if (!params || typeof params !== "object") return;
80
+
81
+ Object.values(params).forEach((param: any) => {
82
+ const porRenglon =
83
+ Array.isArray(param.reason) &&
84
+ param.reason.length > 0 &&
85
+ param.reason.every(
86
+ (renglon: any) =>
87
+ typeof renglon === "object" && renglon !== null && !Array.isArray(renglon),
88
+ );
89
+
90
+ if (porRenglon) {
91
+ param.reason.forEach((renglon: any, indice: number) => {
92
+ Object.entries(renglon).forEach(([campo, motivos]: [string, any]) => {
93
+ form.setError(`${param.name}.${indice}.${campo}`, {
94
+ message: Array.isArray(motivos) ? motivos.join(", ") : String(motivos),
95
+ });
96
+ });
97
+ });
98
+ return;
99
+ }
100
+
101
+ form.setError(param.name, {
102
+ message: Array.isArray(param.reason) ? param.reason.join(", ") : param.reason,
103
+ });
104
+ });
105
+ };
@@ -0,0 +1,76 @@
1
+ import { router } from "expo-router";
2
+ import React from "react";
3
+ import type { FieldValues, UseFormReturn } from "react-hook-form";
4
+ import { avisar } from "../dialogos/dialogos";
5
+ import { mensajeDeError, setErrors } from "./errores";
6
+
7
+ /**
8
+ * Lo que comparten los formularios de edición: cómo se compara lo capturado contra lo que vino,
9
+ * cómo se lee un rechazo del servidor y cómo se guarda.
10
+ */
11
+
12
+ /** Un texto anulable como se manda: sin espacios a los lados, y vacío es `null`. */
13
+ export const textoONulo = (texto: string | null | undefined): string | null => {
14
+ const limpio = (texto ?? "").trim();
15
+ return limpio ? limpio : null;
16
+ };
17
+
18
+ /** Una fecha como se manda: ISO **con su hora** (la hora es el dato), o `null`. */
19
+ export const isoONulo = (fecha: Date | null): string | null => (fecha ? fecha.toISOString() : null);
20
+
21
+ /** Una fecha del servidor como la captura el formulario. */
22
+ export const fechaONula = (iso: string | null | undefined): Date | null =>
23
+ iso ? new Date(iso) : null;
24
+
25
+ /** Si dos instantes son el mismo; dos nulos también lo son. */
26
+ export const mismoInstante = (a: Date | null, b: Date | null): boolean =>
27
+ (a?.getTime() ?? null) === (b?.getTime() ?? null);
28
+
29
+ /**
30
+ * Cuelga de cada campo el motivo con el que el servidor lo rechazó (`invalid_params`). Sólo si
31
+ * vienen como lista: un 400 de regla trae `detail` y `invalid_params` nulo, y `setErrors` no lo
32
+ * aguanta. Lo que no es de un campo lo dice quien llama, con `mensajeDeError`.
33
+ */
34
+ export const volcarErrores = <T extends FieldValues>(form: UseFormReturn<T>, error: unknown) => {
35
+ const data = (error as { data?: { invalid_params?: unknown } } | null)?.data;
36
+ if (data && Array.isArray(data.invalid_params)) setErrors(form, error);
37
+ };
38
+
39
+ /**
40
+ * Guardar un formulario de edición, igual en todos: si nada cambió, sólo se regresa; si el
41
+ * servidor lo rechaza, cada motivo se cuelga de su campo (`volcarErrores`), el mensaje queda
42
+ * abajo del formulario (`error`) y se avisa. Al guardar, regresa a la pantalla de antes.
43
+ */
44
+ export function useGuardado<T extends FieldValues>(form: UseFormReturn<T>) {
45
+ const [error, setError] = React.useState<string | null>(null);
46
+ const guardar = async (cambios: object, mandar: () => Promise<unknown>) => {
47
+ if (Object.keys(cambios).length === 0) {
48
+ router.back();
49
+ return;
50
+ }
51
+ setError(null);
52
+ try {
53
+ await mandar();
54
+ router.back();
55
+ } catch (falla) {
56
+ volcarErrores(form, falla);
57
+ const mensaje = mensajeDeError(falla, "Revisa la señal e intenta de nuevo.");
58
+ setError(mensaje);
59
+ await avisar({ titulo: "No se guardó", mensaje, icono: "alert-circle-outline" });
60
+ }
61
+ };
62
+ return { error, guardar };
63
+ }
64
+
65
+ /**
66
+ * De dos cuerpos completos —lo que se mandaría con lo que vino y con lo capturado—, sólo las
67
+ * llaves que cambiaron, como el `formParser` del portal: un `PATCH` con todo pisaría lo que alguien
68
+ * cambió en el portal mientras la pantalla estaba abierta. Compara por valor (`JSON`), así que
69
+ * sirve con listas y objetos (los contactos, una dirección).
70
+ */
71
+ export const soloLoQueCambio = <T extends Record<string, unknown>>(antes: T, despues: T) =>
72
+ Object.fromEntries(
73
+ Object.entries(despues).filter(
74
+ ([llave, valor]) => JSON.stringify(valor) !== JSON.stringify(antes[llave]),
75
+ ),
76
+ ) as Partial<T>;
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+
3
+ /** Lo que tarda en buscar después de la última tecla: no una petición por letra. */
4
+ const PAUSA_MS = 300;
5
+
6
+ /**
7
+ * Un buscador que pide al servidor: `[texto, busqueda, setTexto]`. `texto` es lo escrito, al
8
+ * momento, para el campo; `busqueda` es lo mismo sin espacios a los lados y **300 ms después de la
9
+ * última tecla**, para la consulta.
10
+ */
11
+ export function useBusqueda(): [string, string, (texto: string) => void] {
12
+ const [texto, setTexto] = React.useState("");
13
+ const [busqueda, setBusqueda] = React.useState("");
14
+ React.useEffect(() => {
15
+ const espera = setTimeout(() => setBusqueda(texto.trim()), PAUSA_MS);
16
+ return () => clearTimeout(espera);
17
+ }, [texto]);
18
+ return [texto, busqueda, setTexto];
19
+ }
@@ -0,0 +1,116 @@
1
+ // Elegir un documento para subirlo, o preguntar de dónde (cámara, galería o documento). Contesta lo
2
+ // elegido ya listo para el `FormData`; qué se hace con él es de quien llama. La cámara y la
3
+ // galería solas están en `camara/`, que no pide expo-document-picker.
4
+ import * as DocumentPicker from "expo-document-picker";
5
+ import { File } from "expo-file-system";
6
+ import {
7
+ elegirDeGaleria,
8
+ tomarFoto,
9
+ type ArchivoElegido,
10
+ type PermisoDeCamara,
11
+ } from "../camara/elegir";
12
+ import { reducirImagenAdjunta } from "../camara/reducir";
13
+ import { avisar, mostrarDialogo } from "../dialogos/dialogos";
14
+
15
+ /** El tope de un archivo en el servidor (`ConArchivoSubido.TAMANO_MAXIMO`): 25 MB. */
16
+ export const TAMANO_MAXIMO_BYTES = 25 * 1024 * 1024;
17
+
18
+ /**
19
+ * Lo que el servidor acepta de un adjunto (`core/shared/serializers.py`): PDF, imágenes y hojas de
20
+ * cálculo. Lo demás ni se ofrece en el selector.
21
+ */
22
+ export const TIPOS_DE_ADJUNTO = [
23
+ "application/pdf",
24
+ "image/*",
25
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
26
+ "application/vnd.ms-excel",
27
+ "text/csv",
28
+ ];
29
+
30
+ /**
31
+ * Documentos (un PDF, una foto ya escaneada). Uno más pesado que el tope del servidor se rechaza al
32
+ * subirlo, así que se avisa aquí y se deja fuera. Una imagen adjuntada como archivo también se
33
+ * reduce, igual que una de la cámara.
34
+ */
35
+ export async function elegirDocumentos({
36
+ tipos = TIPOS_DE_ADJUNTO,
37
+ varias = false,
38
+ tamanoMaximo = TAMANO_MAXIMO_BYTES,
39
+ }: { tipos?: string[]; varias?: boolean; tamanoMaximo?: number } = {}): Promise<ArchivoElegido[]> {
40
+ const resultado = await DocumentPicker.getDocumentAsync({
41
+ type: tipos,
42
+ multiple: varias,
43
+ copyToCacheDirectory: true,
44
+ });
45
+ if (resultado.canceled) return [];
46
+ const elegidos: ArchivoElegido[] = [];
47
+ for (const doc of resultado.assets) {
48
+ const pesa = doc.size ?? new File(doc.uri).size;
49
+ if (pesa > tamanoMaximo) {
50
+ await avisar({
51
+ titulo: "Archivo demasiado pesado",
52
+ mensaje:
53
+ `«${doc.name}» pesa ${(pesa / (1024 * 1024)).toFixed(1)} MB y el tope son ` +
54
+ `${tamanoMaximo / (1024 * 1024)} MB. Elige uno más ligero o tómale una foto.`,
55
+ icono: "file-alert-outline",
56
+ });
57
+ continue;
58
+ }
59
+ const tipo = doc.mimeType ?? "application/octet-stream";
60
+ const esFoto = tipo.startsWith("image/");
61
+ const archivo = esFoto
62
+ ? await reducirImagenAdjunta({ uri: doc.uri, nombre: doc.name, tipo })
63
+ : { uri: doc.uri, nombre: doc.name, tipo };
64
+ elegidos.push({ ...archivo, esFoto });
65
+ }
66
+ return elegidos;
67
+ }
68
+
69
+ /**
70
+ * Pregunta de dónde (cámara, galería o un documento) y elige uno. Contesta `null` si se canceló en
71
+ * cualquier paso o el archivo no se puede subir (y ya se dijo por qué).
72
+ */
73
+ export async function elegirAdjunto({
74
+ titulo = "Subir un archivo",
75
+ permiso,
76
+ tipos,
77
+ }: {
78
+ titulo?: string;
79
+ permiso: PermisoDeCamara;
80
+ tipos?: string[];
81
+ }): Promise<ArchivoElegido | null> {
82
+ const origen = await mostrarDialogo<"camara" | "galeria" | "documento" | null>({
83
+ titulo,
84
+ icono: "paperclip",
85
+ botones: [
86
+ {
87
+ texto: "Tomar una foto",
88
+ icono: "camera-outline",
89
+ estilo: "principal",
90
+ valor: "camara",
91
+ },
92
+ {
93
+ texto: "De la galería",
94
+ icono: "image-outline",
95
+ estilo: "secundario",
96
+ valor: "galeria",
97
+ },
98
+ {
99
+ texto: "Un documento",
100
+ icono: "file-document-outline",
101
+ estilo: "secundario",
102
+ valor: "documento",
103
+ },
104
+ { texto: "Cancelar", estilo: "cancelar", valor: null },
105
+ ],
106
+ });
107
+ const elegidos =
108
+ origen === "camara"
109
+ ? await tomarFoto(permiso)
110
+ : origen === "galeria"
111
+ ? await elegirDeGaleria()
112
+ : origen === "documento"
113
+ ? await elegirDocumentos({ tipos })
114
+ : [];
115
+ return elegidos[0] ?? null;
116
+ }
@@ -0,0 +1,6 @@
1
+ // `@acvl/mobile-components/fotos`: tomar y elegir fotos y archivos. Va aparte de la entrada
2
+ // principal porque usa expo-image-picker, expo-image-manipulator, expo-document-picker y
3
+ // expo-file-system, que sólo instala la app que sube archivos. Lo de la cámara sola (sin
4
+ // documentos) está también en `camara`.
5
+ export * from "../camara";
6
+ export { elegirAdjunto, elegirDocumentos, TAMANO_MAXIMO_BYTES, TIPOS_DE_ADJUNTO } from "./elegir";
@@ -0,0 +1,214 @@
1
+ import BottomSheet, {
2
+ BottomSheetBackdrop,
3
+ BottomSheetFlatList,
4
+ BottomSheetTextInput,
5
+ type BottomSheetBackdropProps,
6
+ } from "@gorhom/bottom-sheet";
7
+ import React, { useEffect, useRef, useState } from "react";
8
+ import { ActivityIndicator, BackHandler, View } from "react-native";
9
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
10
+ import BotonGrande from "../componentes/BotonGrande";
11
+ import { EnHoja } from "../componentes/Tarjeta";
12
+ import { Buscador } from "../listas/piezas";
13
+ import { crearEstilos, renglon, tipografia, usePaleta } from "../tema";
14
+ import { Text } from "../texto";
15
+
16
+ /** Lo alto de la hoja: arriba se sigue viendo un poco de la ficha, que dice dónde se está. */
17
+ const ALTO = "85%";
18
+
19
+ /** Lo que la hoja lee de la consulta de su lista, sea de páginas o de scroll infinito. */
20
+ export interface ListaDeLaHoja<T> {
21
+ /** Lo cargado hasta ahora, de todas las páginas. */
22
+ renglones: T[];
23
+ /** Cuántos hay en total, si el servidor lo dice. */
24
+ total?: number | null;
25
+ /** La primera carga: todavía no hay nada que enseñar. */
26
+ cargando: boolean;
27
+ /** Falló y no hay nada que enseñar. */
28
+ fallo: boolean;
29
+ hayMas: boolean;
30
+ cargandoMas: boolean;
31
+ onMas: () => void;
32
+ onReintentar: () => void;
33
+ }
34
+
35
+ /**
36
+ * Una lista que sube desde abajo sobre una ficha: lo que cuelga de ella (las transferencias de un
37
+ * embarque, las facturas de un pedimento), **con su buscador, su scroll infinito y lo que se hace
38
+ * con ella** (agregar, quitar), sin salir de la ficha. Es la hermana de `HojaInferior` para cuando
39
+ * lo de adentro es una lista larga: aquélla mide su contenido y ésta tiene alto fijo, porque al
40
+ * buscar la lista crece y se encoge y la hoja no debe brincar.
41
+ *
42
+ * Los renglones son `RenglonDeHoja` o los de la lista de la app: una `Tarjeta` adentro ya va del
43
+ * color del fondo (`EnHoja`) para que se distinga de la hoja. Arriba de la lista, el buscador (si lo hay), `acciones` (los botones de la
44
+ * lista) y cuántos hay. Se cierra deslizándola, tocando el velo o con atrás.
45
+ *
46
+ * Sólo se monta abierta: quien la usa pide su consulta sólo entonces (`skip`).
47
+ */
48
+ export default function HojaDeLista<T>({
49
+ abierta,
50
+ onCerrar,
51
+ titulo,
52
+ subtitulo,
53
+ buscador,
54
+ acciones,
55
+ lista,
56
+ llave,
57
+ renderItem,
58
+ vacio,
59
+ nombres,
60
+ }: {
61
+ abierta: boolean;
62
+ onCerrar: () => void;
63
+ titulo: string;
64
+ subtitulo?: string | null;
65
+ buscador?: { texto: string; onCambiar: (texto: string) => void; placeholder: string };
66
+ /** Lo que se hace con la lista entera: «Agregar transferencias». */
67
+ acciones?: React.ReactNode;
68
+ lista: ListaDeLaHoja<T>;
69
+ llave: (item: T) => string | number;
70
+ renderItem: (item: T) => React.ReactElement;
71
+ /** Lo que se dice sin renglones: «Este embarque no trae transferencias». */
72
+ vacio: string;
73
+ /** Cómo se cuentan: `["transferencia", "transferencias"]` → «12 transferencias». */
74
+ nombres?: readonly [string, string];
75
+ }) {
76
+ const c = usePaleta();
77
+ const styles = useStyles();
78
+ const hoja = useRef<BottomSheet>(null);
79
+ const insets = useSafeAreaInsets();
80
+
81
+ // **Sólo existe abierta.** Cerrada (`index={-1}`), gorhom la estaciona justo debajo de su
82
+ // contenedor, y en Android con la barra de navegación de borde a borde ese contenedor mide
83
+ // menos que la pantalla: la hoja se asomaba al pie de la ficha desde que se entraba. Se monta
84
+ // al abrir y se desmonta cuando termina de bajar (`onClose`), así cerrada no hay nada que se
85
+ // asome, y la lista y su consulta tampoco viven de más.
86
+ const [montada, setMontada] = useState(abierta);
87
+ useEffect(() => {
88
+ if (abierta) setMontada(true);
89
+ else hoja.current?.close();
90
+ }, [abierta]);
91
+
92
+ useEffect(() => {
93
+ if (!abierta) return;
94
+ const sub = BackHandler.addEventListener("hardwareBackPress", () => {
95
+ onCerrar();
96
+ return true;
97
+ });
98
+ return () => sub.remove();
99
+ }, [abierta, onCerrar]);
100
+
101
+ const { renglones, total } = lista;
102
+
103
+ const cabecera = (
104
+ <View style={styles.cabecera}>
105
+ <View style={styles.titulos}>
106
+ <Text style={styles.titulo}>{titulo}</Text>
107
+ {subtitulo ? <Text style={styles.subtitulo}>{subtitulo}</Text> : null}
108
+ </View>
109
+ {buscador ? (
110
+ <Buscador
111
+ texto={buscador.texto}
112
+ onCambiar={buscador.onCambiar}
113
+ placeholder={buscador.placeholder}
114
+ entrada={BottomSheetTextInput}
115
+ style={styles.buscador}
116
+ />
117
+ ) : null}
118
+ {acciones}
119
+ {nombres && total != null && renglones.length ? (
120
+ <Text style={styles.cuenta}>
121
+ {total === 1 ? `1 ${nombres[0]}` : `${total} ${nombres[1]}`}
122
+ </Text>
123
+ ) : null}
124
+ </View>
125
+ );
126
+
127
+ const sinRenglones = lista.cargando ? (
128
+ <ActivityIndicator color={c.primary} style={styles.espera} />
129
+ ) : lista.fallo ? (
130
+ <View style={styles.vacio}>
131
+ <Text style={styles.linea}>No se pudo cargar. Revisa tu señal.</Text>
132
+ <BotonGrande icono="refresh" modo="contorno" onPress={lista.onReintentar}>
133
+ Reintentar
134
+ </BotonGrande>
135
+ </View>
136
+ ) : (
137
+ <Text style={[styles.linea, styles.vacio]}>{vacio}</Text>
138
+ );
139
+
140
+ if (!montada) return null;
141
+
142
+ return (
143
+ <BottomSheet
144
+ ref={hoja}
145
+ index={0}
146
+ animateOnMount
147
+ snapPoints={[ALTO]}
148
+ enableDynamicSizing={false}
149
+ enablePanDownToClose
150
+ onClose={() => {
151
+ setMontada(false);
152
+ onCerrar();
153
+ }}
154
+ // Con el teclado, la hoja sube a lo que quede de pantalla en vez de quedar tapada.
155
+ keyboardBehavior="extend"
156
+ keyboardBlurBehavior="restore"
157
+ android_keyboardInputMode="adjustResize"
158
+ backdropComponent={(props: BottomSheetBackdropProps) => (
159
+ <BottomSheetBackdrop
160
+ {...props}
161
+ appearsOnIndex={0}
162
+ disappearsOnIndex={-1}
163
+ pressBehavior="close"
164
+ />
165
+ )}
166
+ backgroundStyle={styles.fondo}
167
+ handleIndicatorStyle={styles.agarradera}
168
+ >
169
+ <EnHoja.Provider value>
170
+ <BottomSheetFlatList
171
+ data={renglones}
172
+ keyExtractor={(item: T) => String(llave(item))}
173
+ renderItem={({ item }: { item: T }) => renderItem(item)}
174
+ ListHeaderComponent={cabecera}
175
+ ListEmptyComponent={sinRenglones}
176
+ ListFooterComponent={
177
+ lista.cargandoMas ? (
178
+ <ActivityIndicator color={c.primary} style={styles.espera} />
179
+ ) : null
180
+ }
181
+ onEndReachedThreshold={0.5}
182
+ onEndReached={() => {
183
+ if (lista.hayMas && !lista.cargandoMas) lista.onMas();
184
+ }}
185
+ keyboardShouldPersistTaps="handled"
186
+ contentContainerStyle={[
187
+ styles.contenido,
188
+ { paddingBottom: insets.bottom + 24 },
189
+ ]}
190
+ />
191
+ </EnHoja.Provider>
192
+ </BottomSheet>
193
+ );
194
+ }
195
+
196
+ const useStyles = crearEstilos((c) => ({
197
+ fondo: {
198
+ backgroundColor: c.superficie,
199
+ borderWidth: c.esOscuro ? 1 : 0,
200
+ borderColor: c.secondaryLighter,
201
+ },
202
+ agarradera: { backgroundColor: c.secondaryLight },
203
+ contenido: { paddingHorizontal: 20, gap: 10 },
204
+ cabecera: { gap: 12, paddingBottom: 2 },
205
+ titulos: { gap: 2 },
206
+ titulo: { ...renglon(tipografia.titulo), fontWeight: "900", color: c.texto },
207
+ subtitulo: { ...renglon(tipografia.secundario), color: c.secondary },
208
+ // Sobre la hoja (superficie), el buscador va del color del fondo para que se vea su caja.
209
+ buscador: { backgroundColor: c.fondo },
210
+ cuenta: { ...renglon(tipografia.secundario), color: c.secondary },
211
+ linea: { ...renglon(tipografia.secundario), color: c.secondary },
212
+ vacio: { gap: 12, paddingVertical: 16 },
213
+ espera: { marginVertical: 16 },
214
+ }));
@@ -0,0 +1,103 @@
1
+ import { crearEstilos, renglon, tipografia } from "../tema";
2
+ import { EnHoja } from "../componentes/Tarjeta";
3
+ import { Text } from "../texto";
4
+ import BottomSheet, {
5
+ BottomSheetBackdrop,
6
+ BottomSheetScrollView,
7
+ type BottomSheetBackdropProps,
8
+ } from "@gorhom/bottom-sheet";
9
+ import React, { useEffect, useRef } from "react";
10
+ import { BackHandler, useWindowDimensions, View } from "react-native";
11
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
12
+
13
+ /** Lo más que sube: arriba se sigue viendo un poco de la ficha, que dice dónde se está. */
14
+ const ALTO_MAXIMO = 0.85;
15
+
16
+ /**
17
+ * Una hoja que sube desde abajo sobre una ficha, para lo que se consulta y se cierra sin salir
18
+ * de ella: los hitos de un tramo, las excepciones, las comisiones. **Mide lo que
19
+ * su contenido**, hasta `ALTO_MAXIMO` de la pantalla; lo demás se recorre adentro.
20
+ *
21
+ * Se cierra deslizándola, tocando el velo de atrás o con atrás. Va **dentro de la pantalla** (no
22
+ * es un modal): en una ficha empujada no hay barra de pestañas que librar. Donde sí la hay (un
23
+ * mapa en una pestaña), la hoja es de la app.
24
+ *
25
+ * Está en `@acvl/mobile-components/hoja` y no en la entrada principal: usa `@gorhom/bottom-sheet`,
26
+ * que sólo instala la app que la usa.
27
+ */
28
+ export default function HojaInferior({
29
+ abierta,
30
+ onCerrar,
31
+ titulo,
32
+ subtitulo,
33
+ children,
34
+ }: {
35
+ abierta: boolean;
36
+ onCerrar: () => void;
37
+ titulo: string;
38
+ subtitulo?: string | null;
39
+ children: React.ReactNode;
40
+ }) {
41
+ const styles = useStyles();
42
+ const hoja = useRef<BottomSheet>(null);
43
+ const insets = useSafeAreaInsets();
44
+ const { height } = useWindowDimensions();
45
+
46
+ useEffect(() => {
47
+ if (abierta) hoja.current?.snapToIndex(0);
48
+ else hoja.current?.close();
49
+ }, [abierta]);
50
+
51
+ useEffect(() => {
52
+ if (!abierta) return;
53
+ const sub = BackHandler.addEventListener("hardwareBackPress", () => {
54
+ onCerrar();
55
+ return true;
56
+ });
57
+ return () => sub.remove();
58
+ }, [abierta, onCerrar]);
59
+
60
+ return (
61
+ <BottomSheet
62
+ ref={hoja}
63
+ index={-1}
64
+ enableDynamicSizing
65
+ maxDynamicContentSize={height * ALTO_MAXIMO}
66
+ enablePanDownToClose
67
+ onClose={onCerrar}
68
+ backdropComponent={(props: BottomSheetBackdropProps) => (
69
+ <BottomSheetBackdrop
70
+ {...props}
71
+ appearsOnIndex={0}
72
+ disappearsOnIndex={-1}
73
+ pressBehavior="close"
74
+ />
75
+ )}
76
+ backgroundStyle={styles.fondo}
77
+ handleIndicatorStyle={styles.agarradera}
78
+ >
79
+ <BottomSheetScrollView
80
+ contentContainerStyle={[styles.contenido, { paddingBottom: insets.bottom + 24 }]}
81
+ >
82
+ <View style={styles.cabecera}>
83
+ <Text style={styles.titulo}>{titulo}</Text>
84
+ {subtitulo ? <Text style={styles.subtitulo}>{subtitulo}</Text> : null}
85
+ </View>
86
+ <EnHoja.Provider value>{abierta ? children : null}</EnHoja.Provider>
87
+ </BottomSheetScrollView>
88
+ </BottomSheet>
89
+ );
90
+ }
91
+
92
+ const useStyles = crearEstilos((c) => ({
93
+ fondo: {
94
+ backgroundColor: c.superficie,
95
+ borderWidth: c.esOscuro ? 1 : 0,
96
+ borderColor: c.secondaryLighter,
97
+ },
98
+ agarradera: { backgroundColor: c.secondaryLight },
99
+ contenido: { paddingHorizontal: 20, gap: 12 },
100
+ cabecera: { gap: 2 },
101
+ titulo: { ...renglon(tipografia.titulo), fontWeight: "900", color: c.texto },
102
+ subtitulo: { ...renglon(tipografia.secundario), color: c.secondary },
103
+ }));
@@ -0,0 +1,5 @@
1
+ // `@acvl/mobile-components/hoja`: lo que sube desde abajo sobre una ficha. Va aparte de la entrada
2
+ // principal porque usa `@gorhom/bottom-sheet`, que sólo instala la app que la usa.
3
+ export { default as HojaDeLista, type ListaDeLaHoja } from "./HojaDeLista";
4
+ export { default as HojaInferior } from "./HojaInferior";
5
+ export { useMasDeLaFicha, type ListaDeLaFicha } from "./useMasDeLaFicha";