@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.
- package/README.md +147 -2
- package/assets/fonts/Inter_400Regular.ttf +0 -0
- package/assets/fonts/Inter_500Medium.ttf +0 -0
- package/assets/fonts/Inter_600SemiBold.ttf +0 -0
- package/assets/fonts/Inter_700Bold.ttf +0 -0
- package/assets/fonts/Inter_800ExtraBold.ttf +0 -0
- package/assets/fonts/Inter_900Black.ttf +0 -0
- package/assets/fonts/OFL.txt +93 -0
- package/lib/module/camara/CampoDeFotos.js +124 -0
- package/lib/module/camara/CampoDeFotos.js.map +1 -0
- package/lib/module/camara/elegir.js +104 -0
- package/lib/module/camara/elegir.js.map +1 -0
- package/lib/module/camara/index.js +10 -0
- package/lib/module/camara/index.js.map +1 -0
- package/lib/module/camara/reducir.js +93 -0
- package/lib/module/camara/reducir.js.map +1 -0
- package/lib/module/componentes/AccionesDelEncabezado.js +212 -0
- package/lib/module/componentes/AccionesDelEncabezado.js.map +1 -0
- package/lib/module/componentes/BotonDeAvatar.js +55 -0
- package/lib/module/componentes/BotonDeAvatar.js.map +1 -0
- package/lib/module/componentes/BotonGrande.js +105 -0
- package/lib/module/componentes/BotonGrande.js.map +1 -0
- package/lib/module/componentes/CabeceraEmpujada.js +101 -0
- package/lib/module/componentes/CabeceraEmpujada.js.map +1 -0
- package/lib/module/componentes/Chip.js +77 -0
- package/lib/module/componentes/Chip.js.map +1 -0
- package/lib/module/componentes/ContenidoDePantalla.js +56 -0
- package/lib/module/componentes/ContenidoDePantalla.js.map +1 -0
- package/lib/module/componentes/EstadoVacio.js +65 -0
- package/lib/module/componentes/EstadoVacio.js.map +1 -0
- package/lib/module/componentes/Foto.js +106 -0
- package/lib/module/componentes/Foto.js.map +1 -0
- package/lib/module/componentes/IconoDeRenglon.js +44 -0
- package/lib/module/componentes/IconoDeRenglon.js.map +1 -0
- package/lib/module/componentes/Logo.js +44 -0
- package/lib/module/componentes/Logo.js.map +1 -0
- package/lib/module/componentes/Nota.js +51 -0
- package/lib/module/componentes/Nota.js.map +1 -0
- package/lib/module/componentes/OpcionesExcluyentes.js +69 -0
- package/lib/module/componentes/OpcionesExcluyentes.js.map +1 -0
- package/lib/module/componentes/PantallaDeError.js +84 -0
- package/lib/module/componentes/PantallaDeError.js.map +1 -0
- package/lib/module/componentes/Pestanas.js +87 -0
- package/lib/module/componentes/Pestanas.js.map +1 -0
- package/lib/module/componentes/Rotulo.js +28 -0
- package/lib/module/componentes/Rotulo.js.map +1 -0
- package/lib/module/componentes/RotuloDeSeccion.js +87 -0
- package/lib/module/componentes/RotuloDeSeccion.js.map +1 -0
- package/lib/module/componentes/Tarjeta.js +63 -0
- package/lib/module/componentes/Tarjeta.js.map +1 -0
- package/lib/module/componentes/TarjetaDeDatos.js +79 -0
- package/lib/module/componentes/TarjetaDeDatos.js.map +1 -0
- package/lib/module/componentes/TiraDeFotos.js +184 -0
- package/lib/module/componentes/TiraDeFotos.js.map +1 -0
- package/lib/module/componentes/VisorDeFoto.js +171 -0
- package/lib/module/componentes/VisorDeFoto.js.map +1 -0
- package/lib/module/componentes/logos/bola-blanca.png +0 -0
- package/lib/module/componentes/logos/bola.png +0 -0
- package/lib/module/componentes/logos/logo-blanco.png +0 -0
- package/lib/module/componentes/logos/logo.png +0 -0
- package/lib/module/dialogos/HostDeDialogos.js +170 -0
- package/lib/module/dialogos/HostDeDialogos.js.map +1 -0
- package/lib/module/dialogos/dialogos.js +112 -0
- package/lib/module/dialogos/dialogos.js.map +1 -0
- package/lib/module/formularios/CampoDeSeleccion.js +72 -0
- package/lib/module/formularios/CampoDeSeleccion.js.map +1 -0
- package/lib/module/formularios/SelectorDeOpcion.js +406 -0
- package/lib/module/formularios/SelectorDeOpcion.js.map +1 -0
- package/lib/module/formularios/TextoDeAccesoControlado.js +73 -0
- package/lib/module/formularios/TextoDeAccesoControlado.js.map +1 -0
- package/lib/module/formularios/campos.js +501 -0
- package/lib/module/formularios/campos.js.map +1 -0
- package/lib/module/formularios/controlados.js +199 -0
- package/lib/module/formularios/controlados.js.map +1 -0
- package/lib/module/formularios/errores.js +91 -0
- package/lib/module/formularios/errores.js.map +1 -0
- package/lib/module/formularios/guardado.js +78 -0
- package/lib/module/formularios/guardado.js.map +1 -0
- package/lib/module/formularios/useBusqueda.js +22 -0
- package/lib/module/formularios/useBusqueda.js.map +1 -0
- package/lib/module/fotos/elegir.js +105 -0
- package/lib/module/fotos/elegir.js.map +1 -0
- package/lib/module/fotos/index.js +9 -0
- package/lib/module/fotos/index.js.map +1 -0
- package/lib/module/hoja/HojaDeLista.js +211 -0
- package/lib/module/hoja/HojaDeLista.js.map +1 -0
- package/lib/module/hoja/HojaInferior.js +112 -0
- package/lib/module/hoja/HojaInferior.js.map +1 -0
- package/lib/module/hoja/index.js +8 -0
- package/lib/module/hoja/index.js.map +1 -0
- package/lib/module/hoja/useMasDeLaFicha.js +48 -0
- package/lib/module/hoja/useMasDeLaFicha.js.map +1 -0
- package/lib/module/index.js +67 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/listas/HojaDeFiltros.js +270 -0
- package/lib/module/listas/HojaDeFiltros.js.map +1 -0
- package/lib/module/listas/ListaPaginada.js +103 -0
- package/lib/module/listas/ListaPaginada.js.map +1 -0
- package/lib/module/listas/TarjetaDeslizable.js +118 -0
- package/lib/module/listas/TarjetaDeslizable.js.map +1 -0
- package/lib/module/listas/paginado.js +11 -0
- package/lib/module/listas/paginado.js.map +1 -0
- package/lib/module/listas/piezas.js +666 -0
- package/lib/module/listas/piezas.js.map +1 -0
- package/lib/module/navegacion/BarraDePestanas.js +162 -0
- package/lib/module/navegacion/BarraDePestanas.js.map +1 -0
- package/lib/module/navegacion/BotonDeBarra.js +58 -0
- package/lib/module/navegacion/BotonDeBarra.js.map +1 -0
- package/lib/module/navegacion/encabezado.js +103 -0
- package/lib/module/navegacion/encabezado.js.map +1 -0
- package/lib/module/pantallas/PantallaDeAcceso.js +195 -0
- package/lib/module/pantallas/PantallaDeAcceso.js.map +1 -0
- package/lib/module/pantallas/PantallaDeEdicion.js +121 -0
- package/lib/module/pantallas/PantallaDeEdicion.js.map +1 -0
- package/lib/module/pantallas/PantallaDeFicha.js +94 -0
- package/lib/module/pantallas/PantallaDeFicha.js.map +1 -0
- package/lib/module/pantallas/PantallaDeHistorial.js +328 -0
- package/lib/module/pantallas/PantallaDeHistorial.js.map +1 -0
- package/lib/module/pantallas/PantallaDePerfil.js +137 -0
- package/lib/module/pantallas/PantallaDePerfil.js.map +1 -0
- package/lib/module/tema/ProveedorDeTema.js +122 -0
- package/lib/module/tema/ProveedorDeTema.js.map +1 -0
- package/lib/module/tema/estilos.js +54 -0
- package/lib/module/tema/estilos.js.map +1 -0
- package/lib/module/tema/fuentes.js +28 -0
- package/lib/module/tema/fuentes.js.map +1 -0
- package/lib/module/tema/index.js +8 -0
- package/lib/module/tema/index.js.map +1 -0
- package/lib/module/tema/paleta.js +107 -0
- package/lib/module/tema/paleta.js.map +1 -0
- package/lib/module/tema/tipografia.js +32 -0
- package/lib/module/tema/tipografia.js.map +1 -0
- package/lib/module/texto.js +48 -0
- package/lib/module/texto.js.map +1 -0
- package/lib/typescript/src/camara/CampoDeFotos.d.ts +30 -0
- package/lib/typescript/src/camara/CampoDeFotos.d.ts.map +1 -0
- package/lib/typescript/src/camara/elegir.d.ts +47 -0
- package/lib/typescript/src/camara/elegir.d.ts.map +1 -0
- package/lib/typescript/src/camara/index.d.ts +4 -0
- package/lib/typescript/src/camara/index.d.ts.map +1 -0
- package/lib/typescript/src/camara/reducir.d.ts +32 -0
- package/lib/typescript/src/camara/reducir.d.ts.map +1 -0
- package/lib/typescript/src/componentes/AccionesDelEncabezado.d.ts +34 -0
- package/lib/typescript/src/componentes/AccionesDelEncabezado.d.ts.map +1 -0
- package/lib/typescript/src/componentes/BotonDeAvatar.d.ts +17 -0
- package/lib/typescript/src/componentes/BotonDeAvatar.d.ts.map +1 -0
- package/lib/typescript/src/componentes/BotonGrande.d.ts +26 -0
- package/lib/typescript/src/componentes/BotonGrande.d.ts.map +1 -0
- package/lib/typescript/src/componentes/CabeceraEmpujada.d.ts +18 -0
- package/lib/typescript/src/componentes/CabeceraEmpujada.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Chip.d.ts +14 -0
- package/lib/typescript/src/componentes/Chip.d.ts.map +1 -0
- package/lib/typescript/src/componentes/ContenidoDePantalla.d.ts +17 -0
- package/lib/typescript/src/componentes/ContenidoDePantalla.d.ts.map +1 -0
- package/lib/typescript/src/componentes/EstadoVacio.d.ts +17 -0
- package/lib/typescript/src/componentes/EstadoVacio.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Foto.d.ts +41 -0
- package/lib/typescript/src/componentes/Foto.d.ts.map +1 -0
- package/lib/typescript/src/componentes/IconoDeRenglon.d.ts +25 -0
- package/lib/typescript/src/componentes/IconoDeRenglon.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Logo.d.ts +13 -0
- package/lib/typescript/src/componentes/Logo.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Nota.d.ts +7 -0
- package/lib/typescript/src/componentes/Nota.d.ts.map +1 -0
- package/lib/typescript/src/componentes/OpcionesExcluyentes.d.ts +14 -0
- package/lib/typescript/src/componentes/OpcionesExcluyentes.d.ts.map +1 -0
- package/lib/typescript/src/componentes/PantallaDeError.d.ts +23 -0
- package/lib/typescript/src/componentes/PantallaDeError.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Pestanas.d.ts +28 -0
- package/lib/typescript/src/componentes/Pestanas.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Rotulo.d.ts +6 -0
- package/lib/typescript/src/componentes/Rotulo.d.ts.map +1 -0
- package/lib/typescript/src/componentes/RotuloDeSeccion.d.ts +20 -0
- package/lib/typescript/src/componentes/RotuloDeSeccion.d.ts.map +1 -0
- package/lib/typescript/src/componentes/Tarjeta.d.ts +25 -0
- package/lib/typescript/src/componentes/Tarjeta.d.ts.map +1 -0
- package/lib/typescript/src/componentes/TarjetaDeDatos.d.ts +13 -0
- package/lib/typescript/src/componentes/TarjetaDeDatos.d.ts.map +1 -0
- package/lib/typescript/src/componentes/TiraDeFotos.d.ts +36 -0
- package/lib/typescript/src/componentes/TiraDeFotos.d.ts.map +1 -0
- package/lib/typescript/src/componentes/VisorDeFoto.d.ts +23 -0
- package/lib/typescript/src/componentes/VisorDeFoto.d.ts.map +1 -0
- package/lib/typescript/src/dialogos/HostDeDialogos.d.ts +14 -0
- package/lib/typescript/src/dialogos/HostDeDialogos.d.ts.map +1 -0
- package/lib/typescript/src/dialogos/dialogos.d.ts +65 -0
- package/lib/typescript/src/dialogos/dialogos.d.ts.map +1 -0
- package/lib/typescript/src/formularios/CampoDeSeleccion.d.ts +24 -0
- package/lib/typescript/src/formularios/CampoDeSeleccion.d.ts.map +1 -0
- package/lib/typescript/src/formularios/SelectorDeOpcion.d.ts +59 -0
- package/lib/typescript/src/formularios/SelectorDeOpcion.d.ts.map +1 -0
- package/lib/typescript/src/formularios/TextoDeAccesoControlado.d.ts +18 -0
- package/lib/typescript/src/formularios/TextoDeAccesoControlado.d.ts.map +1 -0
- package/lib/typescript/src/formularios/campos.d.ts +113 -0
- package/lib/typescript/src/formularios/campos.d.ts.map +1 -0
- package/lib/typescript/src/formularios/controlados.d.ts +29 -0
- package/lib/typescript/src/formularios/controlados.d.ts.map +1 -0
- package/lib/typescript/src/formularios/errores.d.ts +36 -0
- package/lib/typescript/src/formularios/errores.d.ts.map +1 -0
- package/lib/typescript/src/formularios/guardado.d.ts +36 -0
- package/lib/typescript/src/formularios/guardado.d.ts.map +1 -0
- package/lib/typescript/src/formularios/useBusqueda.d.ts +7 -0
- package/lib/typescript/src/formularios/useBusqueda.d.ts.map +1 -0
- package/lib/typescript/src/fotos/elegir.d.ts +28 -0
- package/lib/typescript/src/fotos/elegir.d.ts.map +1 -0
- package/lib/typescript/src/fotos/index.d.ts +3 -0
- package/lib/typescript/src/fotos/index.d.ts.map +1 -0
- package/lib/typescript/src/hoja/HojaDeLista.d.ts +50 -0
- package/lib/typescript/src/hoja/HojaDeLista.d.ts.map +1 -0
- package/lib/typescript/src/hoja/HojaInferior.d.ts +21 -0
- package/lib/typescript/src/hoja/HojaInferior.d.ts.map +1 -0
- package/lib/typescript/src/hoja/index.d.ts +4 -0
- package/lib/typescript/src/hoja/index.d.ts.map +1 -0
- package/lib/typescript/src/hoja/useMasDeLaFicha.d.ts +27 -0
- package/lib/typescript/src/hoja/useMasDeLaFicha.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +46 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/listas/HojaDeFiltros.d.ts +76 -0
- package/lib/typescript/src/listas/HojaDeFiltros.d.ts.map +1 -0
- package/lib/typescript/src/listas/ListaPaginada.d.ts +39 -0
- package/lib/typescript/src/listas/ListaPaginada.d.ts.map +1 -0
- package/lib/typescript/src/listas/TarjetaDeslizable.d.ts +33 -0
- package/lib/typescript/src/listas/TarjetaDeslizable.d.ts.map +1 -0
- package/lib/typescript/src/listas/paginado.d.ts +16 -0
- package/lib/typescript/src/listas/paginado.d.ts.map +1 -0
- package/lib/typescript/src/listas/piezas.d.ts +157 -0
- package/lib/typescript/src/listas/piezas.d.ts.map +1 -0
- package/lib/typescript/src/navegacion/BarraDePestanas.d.ts +57 -0
- package/lib/typescript/src/navegacion/BarraDePestanas.d.ts.map +1 -0
- package/lib/typescript/src/navegacion/BotonDeBarra.d.ts +15 -0
- package/lib/typescript/src/navegacion/BotonDeBarra.d.ts.map +1 -0
- package/lib/typescript/src/navegacion/encabezado.d.ts +44 -0
- package/lib/typescript/src/navegacion/encabezado.d.ts.map +1 -0
- package/lib/typescript/src/pantallas/PantallaDeAcceso.d.ts +32 -0
- package/lib/typescript/src/pantallas/PantallaDeAcceso.d.ts.map +1 -0
- package/lib/typescript/src/pantallas/PantallaDeEdicion.d.ts +38 -0
- package/lib/typescript/src/pantallas/PantallaDeEdicion.d.ts.map +1 -0
- package/lib/typescript/src/pantallas/PantallaDeFicha.d.ts +35 -0
- package/lib/typescript/src/pantallas/PantallaDeFicha.d.ts.map +1 -0
- package/lib/typescript/src/pantallas/PantallaDeHistorial.d.ts +65 -0
- package/lib/typescript/src/pantallas/PantallaDeHistorial.d.ts.map +1 -0
- package/lib/typescript/src/pantallas/PantallaDePerfil.d.ts +19 -0
- package/lib/typescript/src/pantallas/PantallaDePerfil.d.ts.map +1 -0
- package/lib/typescript/src/tema/ProveedorDeTema.d.ts +32 -0
- package/lib/typescript/src/tema/ProveedorDeTema.d.ts.map +1 -0
- package/lib/typescript/src/tema/estilos.d.ts +31 -0
- package/lib/typescript/src/tema/estilos.d.ts.map +1 -0
- package/lib/typescript/src/tema/fuentes.d.ts +20 -0
- package/lib/typescript/src/tema/fuentes.d.ts.map +1 -0
- package/lib/typescript/src/tema/index.d.ts +6 -0
- package/lib/typescript/src/tema/index.d.ts.map +1 -0
- package/lib/typescript/src/tema/paleta.d.ts +86 -0
- package/lib/typescript/src/tema/paleta.d.ts.map +1 -0
- package/lib/typescript/src/tema/tipografia.d.ts +29 -0
- package/lib/typescript/src/tema/tipografia.d.ts.map +1 -0
- package/lib/typescript/src/texto.d.ts +16 -0
- package/lib/typescript/src/texto.d.ts.map +1 -0
- package/package.json +140 -5
- package/src/camara/CampoDeFotos.tsx +132 -0
- package/src/camara/elegir.ts +113 -0
- package/src/camara/index.ts +15 -0
- package/src/camara/reducir.ts +87 -0
- package/src/componentes/AccionesDelEncabezado.tsx +247 -0
- package/src/componentes/BotonDeAvatar.tsx +59 -0
- package/src/componentes/BotonGrande.tsx +100 -0
- package/src/componentes/CabeceraEmpujada.tsx +69 -0
- package/src/componentes/Chip.tsx +60 -0
- package/src/componentes/ContenidoDePantalla.tsx +53 -0
- package/src/componentes/EstadoVacio.tsx +45 -0
- package/src/componentes/Foto.tsx +92 -0
- package/src/componentes/IconoDeRenglon.tsx +42 -0
- package/src/componentes/Logo.tsx +34 -0
- package/src/componentes/Nota.tsx +37 -0
- package/src/componentes/OpcionesExcluyentes.tsx +62 -0
- package/src/componentes/PantallaDeError.tsx +76 -0
- package/src/componentes/Pestanas.tsx +86 -0
- package/src/componentes/Rotulo.tsx +21 -0
- package/src/componentes/RotuloDeSeccion.tsx +71 -0
- package/src/componentes/Tarjeta.tsx +58 -0
- package/src/componentes/TarjetaDeDatos.tsx +62 -0
- package/src/componentes/TiraDeFotos.tsx +189 -0
- package/src/componentes/VisorDeFoto.tsx +181 -0
- package/src/componentes/logos/bola-blanca.png +0 -0
- package/src/componentes/logos/bola.png +0 -0
- package/src/componentes/logos/logo-blanco.png +0 -0
- package/src/componentes/logos/logo.png +0 -0
- package/src/dialogos/HostDeDialogos.tsx +172 -0
- package/src/dialogos/dialogos.ts +148 -0
- package/src/formularios/CampoDeSeleccion.tsx +60 -0
- package/src/formularios/SelectorDeOpcion.tsx +407 -0
- package/src/formularios/TextoDeAccesoControlado.tsx +72 -0
- package/src/formularios/campos.tsx +497 -0
- package/src/formularios/controlados.tsx +200 -0
- package/src/formularios/errores.ts +105 -0
- package/src/formularios/guardado.ts +76 -0
- package/src/formularios/useBusqueda.ts +19 -0
- package/src/fotos/elegir.ts +116 -0
- package/src/fotos/index.ts +6 -0
- package/src/hoja/HojaDeLista.tsx +214 -0
- package/src/hoja/HojaInferior.tsx +103 -0
- package/src/hoja/index.ts +5 -0
- package/src/hoja/useMasDeLaFicha.tsx +63 -0
- package/src/index.ts +151 -0
- package/src/listas/HojaDeFiltros.tsx +286 -0
- package/src/listas/ListaPaginada.tsx +117 -0
- package/src/listas/TarjetaDeslizable.tsx +126 -0
- package/src/listas/paginado.ts +15 -0
- package/src/listas/piezas.tsx +670 -0
- package/src/navegacion/BarraDePestanas.tsx +198 -0
- package/src/navegacion/BotonDeBarra.tsx +61 -0
- package/src/navegacion/encabezado.tsx +88 -0
- package/src/pantallas/PantallaDeAcceso.tsx +184 -0
- package/src/pantallas/PantallaDeEdicion.tsx +116 -0
- package/src/pantallas/PantallaDeFicha.tsx +115 -0
- package/src/pantallas/PantallaDeHistorial.tsx +293 -0
- package/src/pantallas/PantallaDePerfil.tsx +126 -0
- package/src/tema/ProveedorDeTema.tsx +150 -0
- package/src/tema/estilos.ts +51 -0
- package/src/tema/fuentes.ts +20 -0
- package/src/tema/index.ts +10 -0
- package/src/tema/paleta.ts +147 -0
- package/src/tema/tipografia.ts +29 -0
- 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";
|