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