@acvl/mobile-components 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -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/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 +47 -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 +47 -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 +493 -0
- package/lib/module/formularios/campos.js.map +1 -0
- package/lib/module/formularios/controlados.js +197 -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 +178 -0
- package/lib/module/fotos/elegir.js.map +1 -0
- package/lib/module/fotos/index.js +8 -0
- package/lib/module/fotos/index.js.map +1 -0
- package/lib/module/fotos/reducir.js +93 -0
- package/lib/module/fotos/reducir.js.map +1 -0
- package/lib/module/hoja/HojaInferior.js +108 -0
- package/lib/module/hoja/HojaInferior.js.map +1 -0
- package/lib/module/hoja/index.js +7 -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 +66 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/listas/HojaDeFiltros.js +260 -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 +603 -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/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/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 +15 -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 +22 -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 +107 -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 +58 -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/fotos/reducir.d.ts +32 -0
- package/lib/typescript/src/fotos/reducir.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 +3 -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 +45 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/listas/HojaDeFiltros.d.ts +60 -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 +135 -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/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 +135 -5
- 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 +44 -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 +46 -0
- package/src/formularios/SelectorDeOpcion.tsx +407 -0
- package/src/formularios/TextoDeAccesoControlado.tsx +72 -0
- package/src/formularios/campos.tsx +483 -0
- package/src/formularios/controlados.tsx +198 -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 +194 -0
- package/src/fotos/index.ts +15 -0
- package/src/fotos/reducir.ts +87 -0
- package/src/hoja/HojaInferior.tsx +102 -0
- package/src/hoja/index.ts +4 -0
- package/src/hoja/useMasDeLaFicha.tsx +63 -0
- package/src/index.ts +143 -0
- package/src/listas/HojaDeFiltros.tsx +261 -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 +594 -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/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,189 @@
|
|
|
1
|
+
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Pressable, ScrollView, View } from "react-native";
|
|
4
|
+
import { crearEstilos, tipografia, usePaleta } from "../tema";
|
|
5
|
+
import { Text } from "../texto";
|
|
6
|
+
import Foto, { type FotoAVer } from "./Foto";
|
|
7
|
+
import VisorDeFoto from "./VisorDeFoto";
|
|
8
|
+
|
|
9
|
+
/** Una miniatura de la tira: qué se pinta, qué abre el visor y qué se puede hacer con ella. */
|
|
10
|
+
export interface FotoDeLaTira {
|
|
11
|
+
llave: string;
|
|
12
|
+
/** Un archivo que no es foto (un PDF) se ve con su icono y no entra al visor. */
|
|
13
|
+
esFoto: boolean;
|
|
14
|
+
miniatura: FotoAVer;
|
|
15
|
+
/** La foto completa, si la miniatura es una previa chica del servidor. Por omisión, ésa. */
|
|
16
|
+
completa?: FotoAVer;
|
|
17
|
+
/** El nombre del archivo: se ve en un documento que no es foto. */
|
|
18
|
+
nombre?: string;
|
|
19
|
+
/** Una nube en la esquina: verde si ya está en el servidor, ámbar si sigue en el teléfono. */
|
|
20
|
+
marca?: "enviada" | "sinSubir";
|
|
21
|
+
/** Con esto lleva su «×» para quitarla. */
|
|
22
|
+
onQuitar?: () => void;
|
|
23
|
+
/** Con esto, el visor lleva «descargar» en ella. Rechaza si no se pudo. */
|
|
24
|
+
onDescargar?: () => Promise<void>;
|
|
25
|
+
/** Lo que dice el lector de pantalla, si no basta con «Ver la foto». */
|
|
26
|
+
etiqueta?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Props {
|
|
30
|
+
fotos: FotoDeLaTira[];
|
|
31
|
+
/** El lado de cada miniatura. */
|
|
32
|
+
lado: number;
|
|
33
|
+
/** En cuadrícula (una hoja, donde hay lugar) o en un renglón que se desliza a los lados. */
|
|
34
|
+
cuadricula?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Unas fotos en miniatura; tocar una abre el visor en ésa y se desliza entre las demás, **en el
|
|
39
|
+
* mismo orden** que las miniaturas. Quien la usa arma las piezas con lo suyo (lo del servidor
|
|
40
|
+
* primero y lo del teléfono después, por ejemplo) y les pone su marca y sus acciones.
|
|
41
|
+
*/
|
|
42
|
+
const TiraDeFotos = ({ fotos: piezas, lado, cuadricula = false }: Props) => {
|
|
43
|
+
const styles = useStyles();
|
|
44
|
+
const [viendo, setViendo] = React.useState<number | null>(null);
|
|
45
|
+
if (piezas.length === 0) return null;
|
|
46
|
+
|
|
47
|
+
const fotos = piezas.filter((p) => p.esFoto);
|
|
48
|
+
const carrusel = fotos.map((p) => p.completa ?? p.miniatura);
|
|
49
|
+
const descargables = fotos.some((p) => p.onDescargar);
|
|
50
|
+
const tamano = { width: lado, height: lado };
|
|
51
|
+
|
|
52
|
+
const miniaturas = piezas.map((p) => (
|
|
53
|
+
<View key={p.llave}>
|
|
54
|
+
<Pressable
|
|
55
|
+
disabled={!p.esFoto}
|
|
56
|
+
onPress={() => setViendo(fotos.indexOf(p))}
|
|
57
|
+
accessibilityRole={p.esFoto ? "imagebutton" : undefined}
|
|
58
|
+
accessibilityLabel={
|
|
59
|
+
p.etiqueta ?? (p.esFoto ? "Ver la foto" : `Archivo ${p.nombre ?? ""}`.trim())
|
|
60
|
+
}
|
|
61
|
+
>
|
|
62
|
+
{p.esFoto ? (
|
|
63
|
+
<Foto
|
|
64
|
+
foto={p.miniatura}
|
|
65
|
+
style={[styles.miniatura, tamano]}
|
|
66
|
+
resizeMethod="resize"
|
|
67
|
+
/>
|
|
68
|
+
) : (
|
|
69
|
+
<Documento lado={lado} nombre={p.nombre} />
|
|
70
|
+
)}
|
|
71
|
+
{p.marca === "enviada" ? (
|
|
72
|
+
<View style={[styles.marca, styles.marcaEnviada]}>
|
|
73
|
+
<MaterialCommunityIcons name="cloud-check" size={16} color="#fff" />
|
|
74
|
+
</View>
|
|
75
|
+
) : null}
|
|
76
|
+
{p.marca === "sinSubir" ? (
|
|
77
|
+
<View style={[styles.marca, styles.marcaSinSubir]}>
|
|
78
|
+
<MaterialCommunityIcons name="cloud-clock-outline" size={16} color="#fff" />
|
|
79
|
+
</View>
|
|
80
|
+
) : null}
|
|
81
|
+
</Pressable>
|
|
82
|
+
{p.onQuitar ? (
|
|
83
|
+
<Pressable
|
|
84
|
+
onPress={p.onQuitar}
|
|
85
|
+
hitSlop={10}
|
|
86
|
+
accessibilityRole="button"
|
|
87
|
+
accessibilityLabel={p.esFoto ? "Quitar esta foto" : "Quitar este archivo"}
|
|
88
|
+
style={styles.quitar}
|
|
89
|
+
>
|
|
90
|
+
<MaterialCommunityIcons name="close" size={24} color="#fff" />
|
|
91
|
+
</Pressable>
|
|
92
|
+
) : null}
|
|
93
|
+
</View>
|
|
94
|
+
));
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
{cuadricula ? (
|
|
99
|
+
<View style={styles.cuadricula}>{miniaturas}</View>
|
|
100
|
+
) : (
|
|
101
|
+
// Un solo renglón que se desliza: con varias fotos no se parte en filas.
|
|
102
|
+
<ScrollView
|
|
103
|
+
horizontal
|
|
104
|
+
showsHorizontalScrollIndicator={false}
|
|
105
|
+
contentContainerStyle={styles.renglon}
|
|
106
|
+
>
|
|
107
|
+
{miniaturas}
|
|
108
|
+
</ScrollView>
|
|
109
|
+
)}
|
|
110
|
+
<VisorDeFoto
|
|
111
|
+
fotos={carrusel}
|
|
112
|
+
inicio={viendo}
|
|
113
|
+
onCerrar={() => setViendo(null)}
|
|
114
|
+
onDescargar={
|
|
115
|
+
descargables
|
|
116
|
+
? async (i) => {
|
|
117
|
+
const descargar = fotos[i]?.onDescargar;
|
|
118
|
+
if (!descargar) throw new Error("Esta foto no se puede descargar.");
|
|
119
|
+
await descargar();
|
|
120
|
+
}
|
|
121
|
+
: undefined
|
|
122
|
+
}
|
|
123
|
+
/>
|
|
124
|
+
</>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** La miniatura de un archivo que no es foto (un PDF): un icono y, si cabe, su nombre. */
|
|
129
|
+
function Documento({ lado, nombre }: { lado: number; nombre?: string }) {
|
|
130
|
+
const c = usePaleta();
|
|
131
|
+
const styles = useStyles();
|
|
132
|
+
const conNombre = Boolean(nombre) && lado >= 100;
|
|
133
|
+
return (
|
|
134
|
+
<View style={[styles.miniatura, styles.documento, { width: lado, height: lado }]}>
|
|
135
|
+
<MaterialCommunityIcons
|
|
136
|
+
name="file-document-outline"
|
|
137
|
+
size={Math.round(lado * 0.3)}
|
|
138
|
+
color={c.secondary}
|
|
139
|
+
/>
|
|
140
|
+
{conNombre ? (
|
|
141
|
+
// Dentro de un cuadro fijo: el nombre largo de un PDF tiene que cortarse.
|
|
142
|
+
<Text numberOfLines={2} style={styles.documentoNombre}>
|
|
143
|
+
{nombre}
|
|
144
|
+
</Text>
|
|
145
|
+
) : null}
|
|
146
|
+
</View>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Las marcas y la «×» van encima de la foto: sus colores son fijos para leerse sobre cualquier foto.
|
|
151
|
+
const useStyles = crearEstilos((c) => ({
|
|
152
|
+
renglon: { gap: 8 },
|
|
153
|
+
cuadricula: { flexDirection: "row", flexWrap: "wrap", gap: 12 },
|
|
154
|
+
miniatura: { borderRadius: 10, backgroundColor: c.secondaryLighter },
|
|
155
|
+
documento: { alignItems: "center", justifyContent: "center", padding: 6, gap: 2 },
|
|
156
|
+
documentoNombre: {
|
|
157
|
+
fontSize: tipografia.etiqueta,
|
|
158
|
+
color: c.secondaryDark,
|
|
159
|
+
textAlign: "center",
|
|
160
|
+
},
|
|
161
|
+
marca: {
|
|
162
|
+
position: "absolute",
|
|
163
|
+
bottom: 4,
|
|
164
|
+
right: 4,
|
|
165
|
+
width: 24,
|
|
166
|
+
height: 24,
|
|
167
|
+
borderRadius: 12,
|
|
168
|
+
alignItems: "center",
|
|
169
|
+
justifyContent: "center",
|
|
170
|
+
},
|
|
171
|
+
marcaEnviada: { backgroundColor: "#2e7d32" },
|
|
172
|
+
// La nube ámbar sobre una foto que sigue en el teléfono sin subir.
|
|
173
|
+
marcaSinSubir: { backgroundColor: "#b26a00" },
|
|
174
|
+
quitar: {
|
|
175
|
+
position: "absolute",
|
|
176
|
+
top: 6,
|
|
177
|
+
right: 6,
|
|
178
|
+
width: 40,
|
|
179
|
+
height: 40,
|
|
180
|
+
borderRadius: 20,
|
|
181
|
+
backgroundColor: c.primaryDark,
|
|
182
|
+
alignItems: "center",
|
|
183
|
+
justifyContent: "center",
|
|
184
|
+
borderWidth: 2,
|
|
185
|
+
borderColor: "#fff",
|
|
186
|
+
},
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
export default TiraDeFotos;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { renglon, tipografia } from "../tema";
|
|
2
|
+
import { Text } from "../texto";
|
|
3
|
+
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import {
|
|
6
|
+
ActivityIndicator,
|
|
7
|
+
FlatList,
|
|
8
|
+
Modal,
|
|
9
|
+
Pressable,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
useWindowDimensions,
|
|
12
|
+
View,
|
|
13
|
+
} from "react-native";
|
|
14
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
15
|
+
import Foto, { type FotoAVer } from "./Foto";
|
|
16
|
+
|
|
17
|
+
export type { FotoAVer };
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
/** Las fotos, en el orden de sus miniaturas. Los archivos que no son foto no entran. */
|
|
21
|
+
fotos: FotoAVer[];
|
|
22
|
+
/** La que se tocó; `null` cierra el visor. */
|
|
23
|
+
inicio: number | null;
|
|
24
|
+
onCerrar: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* Con esto, la barra lleva «descargar» para la foto que se ve.
|
|
27
|
+
* Recibe su lugar en `fotos`; rechaza si no se pudo.
|
|
28
|
+
*/
|
|
29
|
+
onDescargar?: (indice: number) => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Cómo va la descarga de la foto que se ve: el icono de la barra lo dice. */
|
|
33
|
+
type Descarga = "nada" | "bajando" | "lista" | "fallo";
|
|
34
|
+
const ICONO_DE_DESCARGA = {
|
|
35
|
+
nada: "download",
|
|
36
|
+
lista: "check",
|
|
37
|
+
fallo: "alert-circle-outline",
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
const llave = (foto: FotoAVer, i: number) => ("ruta" in foto ? foto.ruta : foto.uri) + i;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Unas fotos a pantalla completa, sobre fondo negro: se abre en la que se tocó y se pasa a la
|
|
44
|
+
* siguiente o la anterior deslizando a los lados, con «2 de 4» arriba. Mezcla las subidas y las del
|
|
45
|
+
* teléfono. Se cierra con la X o con atrás. Lo abre `TiraDeFotos`.
|
|
46
|
+
*/
|
|
47
|
+
const VisorDeFoto = ({ fotos, inicio, onCerrar, onDescargar }: Props) => (
|
|
48
|
+
<Modal
|
|
49
|
+
visible={inicio !== null}
|
|
50
|
+
transparent
|
|
51
|
+
animationType="fade"
|
|
52
|
+
statusBarTranslucent
|
|
53
|
+
onRequestClose={onCerrar}
|
|
54
|
+
>
|
|
55
|
+
{/* Con `key`, cada apertura nace en la foto que se tocó. */}
|
|
56
|
+
{inicio !== null ? (
|
|
57
|
+
<Carrusel
|
|
58
|
+
key={inicio}
|
|
59
|
+
fotos={fotos}
|
|
60
|
+
inicio={inicio}
|
|
61
|
+
onCerrar={onCerrar}
|
|
62
|
+
onDescargar={onDescargar}
|
|
63
|
+
/>
|
|
64
|
+
) : null}
|
|
65
|
+
</Modal>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
function Carrusel({
|
|
69
|
+
fotos,
|
|
70
|
+
inicio,
|
|
71
|
+
onCerrar,
|
|
72
|
+
onDescargar,
|
|
73
|
+
}: {
|
|
74
|
+
fotos: FotoAVer[];
|
|
75
|
+
inicio: number;
|
|
76
|
+
onCerrar: () => void;
|
|
77
|
+
onDescargar?: (indice: number) => Promise<void>;
|
|
78
|
+
}) {
|
|
79
|
+
const insets = useSafeAreaInsets();
|
|
80
|
+
const { width, height } = useWindowDimensions();
|
|
81
|
+
const primera = Math.min(inicio, Math.max(0, fotos.length - 1));
|
|
82
|
+
const [actual, setActual] = React.useState(primera);
|
|
83
|
+
const [descarga, setDescarga] = React.useState<Descarga>("nada");
|
|
84
|
+
|
|
85
|
+
// Lo que dice el icono es de la foto en la que estaba: al pasar a otra, vuelve a «descargar».
|
|
86
|
+
const pasarA = (indice: number) => {
|
|
87
|
+
if (indice !== actual) setDescarga("nada");
|
|
88
|
+
setActual(indice);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const descargar = async () => {
|
|
92
|
+
if (!onDescargar || descarga === "bajando") return;
|
|
93
|
+
setDescarga("bajando");
|
|
94
|
+
try {
|
|
95
|
+
await onDescargar(actual);
|
|
96
|
+
setDescarga("lista");
|
|
97
|
+
} catch {
|
|
98
|
+
setDescarga("fallo");
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<View style={styles.fondo}>
|
|
104
|
+
<FlatList
|
|
105
|
+
data={fotos}
|
|
106
|
+
keyExtractor={llave}
|
|
107
|
+
horizontal
|
|
108
|
+
pagingEnabled
|
|
109
|
+
showsHorizontalScrollIndicator={false}
|
|
110
|
+
initialScrollIndex={primera}
|
|
111
|
+
getItemLayout={(_, i) => ({ length: width, offset: width * i, index: i })}
|
|
112
|
+
onMomentumScrollEnd={(e) =>
|
|
113
|
+
pasarA(Math.round(e.nativeEvent.contentOffset.x / width))
|
|
114
|
+
}
|
|
115
|
+
renderItem={({ item }) => (
|
|
116
|
+
<View style={{ width, height }}>
|
|
117
|
+
<Foto foto={item} style={styles.foto} resizeMode="contain" />
|
|
118
|
+
</View>
|
|
119
|
+
)}
|
|
120
|
+
/>
|
|
121
|
+
|
|
122
|
+
<View style={[styles.barra, { top: insets.top + 12 }]}>
|
|
123
|
+
{fotos.length > 1 ? (
|
|
124
|
+
<Text style={styles.cuenta}>
|
|
125
|
+
{actual + 1} de {fotos.length}
|
|
126
|
+
</Text>
|
|
127
|
+
) : (
|
|
128
|
+
<View />
|
|
129
|
+
)}
|
|
130
|
+
<View style={styles.botones}>
|
|
131
|
+
{onDescargar ? (
|
|
132
|
+
<Pressable
|
|
133
|
+
onPress={descargar}
|
|
134
|
+
hitSlop={12}
|
|
135
|
+
accessibilityRole="button"
|
|
136
|
+
accessibilityLabel={
|
|
137
|
+
descarga === "lista"
|
|
138
|
+
? "Guardada en la galería"
|
|
139
|
+
: descarga === "fallo"
|
|
140
|
+
? "No se pudo guardar; intentar de nuevo"
|
|
141
|
+
: "Guardar en la galería"
|
|
142
|
+
}
|
|
143
|
+
accessibilityState={{ busy: descarga === "bajando" }}
|
|
144
|
+
>
|
|
145
|
+
{descarga === "bajando" ? (
|
|
146
|
+
<ActivityIndicator size={32} color="#fff" />
|
|
147
|
+
) : (
|
|
148
|
+
<MaterialCommunityIcons
|
|
149
|
+
name={ICONO_DE_DESCARGA[descarga]}
|
|
150
|
+
size={32}
|
|
151
|
+
color="#fff"
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
</Pressable>
|
|
155
|
+
) : null}
|
|
156
|
+
<Pressable onPress={onCerrar} hitSlop={12} accessibilityLabel="Cerrar">
|
|
157
|
+
<MaterialCommunityIcons name="close" size={32} color="#fff" />
|
|
158
|
+
</Pressable>
|
|
159
|
+
</View>
|
|
160
|
+
</View>
|
|
161
|
+
</View>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Sobre la foto: negro con texto blanco, en claro y en oscuro.
|
|
166
|
+
const styles = StyleSheet.create({
|
|
167
|
+
fondo: { flex: 1, backgroundColor: "rgba(0,0,0,0.95)" },
|
|
168
|
+
foto: { width: "100%", height: "100%" },
|
|
169
|
+
barra: {
|
|
170
|
+
position: "absolute",
|
|
171
|
+
left: 16,
|
|
172
|
+
right: 16,
|
|
173
|
+
flexDirection: "row",
|
|
174
|
+
alignItems: "center",
|
|
175
|
+
justifyContent: "space-between",
|
|
176
|
+
},
|
|
177
|
+
botones: { flexDirection: "row", alignItems: "center", gap: 28 },
|
|
178
|
+
cuenta: { ...renglon(tipografia.cuerpo), color: "#fff", fontWeight: "800" },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
export default VisorDeFoto;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { crearEstilos, renglon, tipografia, usePaleta } from "../tema";
|
|
2
|
+
import BotonGrande from "../componentes/BotonGrande";
|
|
3
|
+
import { Text } from "../texto";
|
|
4
|
+
import {
|
|
5
|
+
dialogoActual,
|
|
6
|
+
responderDialogo,
|
|
7
|
+
suscribirDialogos,
|
|
8
|
+
type BotonDeDialogo,
|
|
9
|
+
} from "./dialogos";
|
|
10
|
+
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { Modal, Pressable, ScrollView, useWindowDimensions, View } from "react-native";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pinta los diálogos que abre `mostrarDialogo` (`dialogos.ts`), de uno en uno. Se monta una
|
|
16
|
+
* sola vez, en el layout raíz de la app.
|
|
17
|
+
*
|
|
18
|
+
* Es el diálogo de la casa —el `ConfirmDialog` de las revisiones de wms— con la letra grande de las apps:
|
|
19
|
+
* una tarjeta blanca sobre fondo oscuro, el icono arriba, título y mensaje sin recortar, y los
|
|
20
|
+
* botones **apilados a lo ancho**: lado a lado no caben con la letra al 180%. «Cancelar» va como
|
|
21
|
+
* texto discreto al pie. Atrás y el toque fuera cierran como cancelar, salvo que el diálogo diga
|
|
22
|
+
* que no es cancelable.
|
|
23
|
+
*/
|
|
24
|
+
const HostDeDialogos = () => {
|
|
25
|
+
const c = usePaleta();
|
|
26
|
+
const styles = useStyles();
|
|
27
|
+
const dialogo = React.useSyncExternalStore(suscribirDialogos, dialogoActual);
|
|
28
|
+
const { height } = useWindowDimensions();
|
|
29
|
+
|
|
30
|
+
if (!dialogo) return null;
|
|
31
|
+
|
|
32
|
+
const cancelable = dialogo.cancelable ?? true;
|
|
33
|
+
const cerrar = () => {
|
|
34
|
+
if (cancelable) responderDialogo(dialogo, undefined);
|
|
35
|
+
};
|
|
36
|
+
const acciones = dialogo.botones.filter((b) => b.estilo !== "cancelar");
|
|
37
|
+
const cancelar = dialogo.botones.find((b) => b.estilo === "cancelar");
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Modal
|
|
41
|
+
visible
|
|
42
|
+
transparent
|
|
43
|
+
animationType="fade"
|
|
44
|
+
statusBarTranslucent
|
|
45
|
+
navigationBarTranslucent
|
|
46
|
+
onRequestClose={cerrar}
|
|
47
|
+
>
|
|
48
|
+
<Pressable style={styles.fondo} onPress={cerrar} accessibilityLabel="Cerrar">
|
|
49
|
+
{/* El `Pressable` vacío de la tarjeta se queda con el toque: tocar adentro no cierra. */}
|
|
50
|
+
<Pressable
|
|
51
|
+
style={[styles.tarjeta, { maxHeight: height * 0.85 }]}
|
|
52
|
+
onPress={() => {}}
|
|
53
|
+
>
|
|
54
|
+
<ScrollView
|
|
55
|
+
contentContainerStyle={styles.contenido}
|
|
56
|
+
showsVerticalScrollIndicator={false}
|
|
57
|
+
>
|
|
58
|
+
{dialogo.icono ? (
|
|
59
|
+
<View style={styles.icono}>
|
|
60
|
+
<MaterialCommunityIcons
|
|
61
|
+
name={dialogo.icono}
|
|
62
|
+
size={34}
|
|
63
|
+
color={c.primary}
|
|
64
|
+
/>
|
|
65
|
+
</View>
|
|
66
|
+
) : null}
|
|
67
|
+
<Text style={styles.titulo}>{dialogo.titulo}</Text>
|
|
68
|
+
{dialogo.mensaje ? (
|
|
69
|
+
<Text style={styles.mensaje}>{dialogo.mensaje}</Text>
|
|
70
|
+
) : null}
|
|
71
|
+
|
|
72
|
+
<View style={styles.botones}>
|
|
73
|
+
{acciones.map((boton, i) => (
|
|
74
|
+
<Boton
|
|
75
|
+
key={`${boton.texto}-${i}`}
|
|
76
|
+
boton={boton}
|
|
77
|
+
onPress={() => responderDialogo(dialogo, boton.valor)}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
{cancelar ? (
|
|
81
|
+
<Pressable
|
|
82
|
+
onPress={() => responderDialogo(dialogo, cancelar.valor)}
|
|
83
|
+
hitSlop={8}
|
|
84
|
+
accessibilityRole="button"
|
|
85
|
+
style={({ pressed }) => [
|
|
86
|
+
styles.cancelar,
|
|
87
|
+
pressed && { opacity: 0.6 },
|
|
88
|
+
]}
|
|
89
|
+
>
|
|
90
|
+
<Text style={styles.cancelarTexto}>{cancelar.texto}</Text>
|
|
91
|
+
</Pressable>
|
|
92
|
+
) : null}
|
|
93
|
+
</View>
|
|
94
|
+
</ScrollView>
|
|
95
|
+
</Pressable>
|
|
96
|
+
</Pressable>
|
|
97
|
+
</Modal>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function Boton({ boton, onPress }: { boton: BotonDeDialogo<unknown>; onPress: () => void }) {
|
|
102
|
+
const estilo = boton.estilo ?? "principal";
|
|
103
|
+
return (
|
|
104
|
+
<BotonGrande
|
|
105
|
+
icono={boton.icono}
|
|
106
|
+
modo={
|
|
107
|
+
estilo === "secundario"
|
|
108
|
+
? "contorno"
|
|
109
|
+
: estilo === "destructivo"
|
|
110
|
+
? "destructivo"
|
|
111
|
+
: "relleno"
|
|
112
|
+
}
|
|
113
|
+
onPress={onPress}
|
|
114
|
+
>
|
|
115
|
+
{boton.texto}
|
|
116
|
+
</BotonGrande>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const useStyles = crearEstilos((c) => ({
|
|
121
|
+
fondo: {
|
|
122
|
+
flex: 1,
|
|
123
|
+
backgroundColor: c.velo,
|
|
124
|
+
justifyContent: "center",
|
|
125
|
+
padding: 20,
|
|
126
|
+
},
|
|
127
|
+
tarjeta: {
|
|
128
|
+
width: "100%",
|
|
129
|
+
maxWidth: 480,
|
|
130
|
+
alignSelf: "center",
|
|
131
|
+
backgroundColor: c.superficie,
|
|
132
|
+
borderRadius: 20,
|
|
133
|
+
overflow: "hidden",
|
|
134
|
+
elevation: 12,
|
|
135
|
+
shadowColor: c.sombra,
|
|
136
|
+
shadowOffset: { width: 0, height: 10 },
|
|
137
|
+
shadowOpacity: 0.25,
|
|
138
|
+
shadowRadius: 24,
|
|
139
|
+
},
|
|
140
|
+
contenido: { padding: 22, gap: 10 },
|
|
141
|
+
icono: {
|
|
142
|
+
alignSelf: "center",
|
|
143
|
+
width: 60,
|
|
144
|
+
height: 60,
|
|
145
|
+
borderRadius: 30,
|
|
146
|
+
backgroundColor: c.primaryWash,
|
|
147
|
+
alignItems: "center",
|
|
148
|
+
justifyContent: "center",
|
|
149
|
+
marginBottom: 4,
|
|
150
|
+
},
|
|
151
|
+
titulo: {
|
|
152
|
+
...renglon(tipografia.titulo),
|
|
153
|
+
fontWeight: "900",
|
|
154
|
+
color: c.texto,
|
|
155
|
+
textAlign: "center",
|
|
156
|
+
},
|
|
157
|
+
mensaje: {
|
|
158
|
+
...renglon(tipografia.cuerpo),
|
|
159
|
+
color: c.secondaryDark,
|
|
160
|
+
textAlign: "center",
|
|
161
|
+
},
|
|
162
|
+
botones: { gap: 10, marginTop: 12 },
|
|
163
|
+
cancelar: { alignSelf: "center", paddingVertical: 12, paddingHorizontal: 20 },
|
|
164
|
+
cancelarTexto: {
|
|
165
|
+
...renglon(tipografia.cuerpo),
|
|
166
|
+
fontWeight: "700",
|
|
167
|
+
color: c.secondary,
|
|
168
|
+
textAlign: "center",
|
|
169
|
+
},
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
export default HostDeDialogos;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Los diálogos de las apps, con su diseño y no el `Alert` del sistema. Esto es la API: la abren las
|
|
2
|
+
// pantallas y también código que no es componente (una cola de envíos, la ubicación, la
|
|
3
|
+
// navegación). Los pinta `HostDeDialogos`, montado una vez en el layout raíz.
|
|
4
|
+
import type MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
|
|
5
|
+
import type React from "react";
|
|
6
|
+
|
|
7
|
+
type NombreDeIcono = React.ComponentProps<typeof MaterialCommunityIcons>["name"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* - `principal`: la acción esperada, en rojo.
|
|
11
|
+
* - `secundario`: otra opción válida, en contorno.
|
|
12
|
+
* - `destructivo`: borra o deshace algo (Deshacer, Descartar, Cerrar sesión), en rojo oscuro.
|
|
13
|
+
* - `cancelar`: no hacer nada, como texto discreto al pie.
|
|
14
|
+
*/
|
|
15
|
+
export type EstiloDeBoton = "principal" | "secundario" | "destructivo" | "cancelar";
|
|
16
|
+
|
|
17
|
+
export interface BotonDeDialogo<V> {
|
|
18
|
+
texto: string;
|
|
19
|
+
estilo?: EstiloDeBoton;
|
|
20
|
+
icono?: NombreDeIcono;
|
|
21
|
+
/** Lo que resuelve la promesa de `mostrarDialogo` al tocarlo. */
|
|
22
|
+
valor: V;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Dialogo<V> {
|
|
26
|
+
titulo: string;
|
|
27
|
+
mensaje?: string;
|
|
28
|
+
icono?: NombreDeIcono;
|
|
29
|
+
botones: BotonDeDialogo<V>[];
|
|
30
|
+
/**
|
|
31
|
+
* Si el botón atrás de Android y el toque fuera de la tarjeta lo cierran (resolviendo
|
|
32
|
+
* `undefined`, como cancelar). Por omisión, sí.
|
|
33
|
+
*/
|
|
34
|
+
cancelable?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface EnEspera {
|
|
38
|
+
dialogo: Dialogo<unknown>;
|
|
39
|
+
responder: (valor: unknown) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* La fila de diálogos. Vive en `globalThis` para que Fast Refresh, al re-evaluar este módulo, no
|
|
44
|
+
* deje al host escuchando una fila vieja con promesas que nunca se resuelven.
|
|
45
|
+
*/
|
|
46
|
+
const g = globalThis as typeof globalThis & {
|
|
47
|
+
__dialogos?: { fila: EnEspera[]; oyentes: Set<() => void> };
|
|
48
|
+
};
|
|
49
|
+
const estado = (g.__dialogos ??= { fila: [], oyentes: new Set() });
|
|
50
|
+
|
|
51
|
+
const avisarCambio = () => estado.oyentes.forEach((oyente) => oyente());
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Abre un diálogo y espera la respuesta: el `valor` del botón tocado, o `undefined` si se cerró
|
|
55
|
+
* sin elegir (atrás, toque fuera). Si ya hay uno abierto, éste espera su turno: se muestran de uno
|
|
56
|
+
* en uno, en el orden en que llegaron.
|
|
57
|
+
*/
|
|
58
|
+
export function mostrarDialogo<V>(dialogo: Dialogo<V>): Promise<V | undefined> {
|
|
59
|
+
return new Promise((resolver) => {
|
|
60
|
+
estado.fila.push({
|
|
61
|
+
dialogo: dialogo as Dialogo<unknown>,
|
|
62
|
+
responder: resolver as (valor: unknown) => void,
|
|
63
|
+
});
|
|
64
|
+
avisarCambio();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Una pregunta de sí o no. Resuelve `true` sólo si se tocó el botón de confirmar; cancelar,
|
|
70
|
+
* atrás y el toque fuera dan `false`.
|
|
71
|
+
*/
|
|
72
|
+
export async function confirmar({
|
|
73
|
+
titulo,
|
|
74
|
+
mensaje,
|
|
75
|
+
textoConfirmar,
|
|
76
|
+
textoCancelar = "Cancelar",
|
|
77
|
+
destructivo = false,
|
|
78
|
+
icono,
|
|
79
|
+
}: {
|
|
80
|
+
titulo: string;
|
|
81
|
+
mensaje?: string;
|
|
82
|
+
textoConfirmar: string;
|
|
83
|
+
textoCancelar?: string;
|
|
84
|
+
destructivo?: boolean;
|
|
85
|
+
icono?: NombreDeIcono;
|
|
86
|
+
}): Promise<boolean> {
|
|
87
|
+
const respuesta = await mostrarDialogo({
|
|
88
|
+
titulo,
|
|
89
|
+
mensaje,
|
|
90
|
+
icono: icono ?? (destructivo ? "alert-circle-outline" : "help-circle-outline"),
|
|
91
|
+
botones: [
|
|
92
|
+
{
|
|
93
|
+
texto: textoConfirmar,
|
|
94
|
+
estilo: destructivo ? "destructivo" : "principal",
|
|
95
|
+
valor: true,
|
|
96
|
+
},
|
|
97
|
+
{ texto: textoCancelar, estilo: "cancelar", valor: false },
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
return respuesta === true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Un aviso con un solo botón. Resuelve al cerrarse. */
|
|
104
|
+
export async function avisar({
|
|
105
|
+
titulo,
|
|
106
|
+
mensaje,
|
|
107
|
+
icono = "information-outline",
|
|
108
|
+
textoBoton = "Entendido",
|
|
109
|
+
cancelable = true,
|
|
110
|
+
}: {
|
|
111
|
+
titulo: string;
|
|
112
|
+
mensaje?: string;
|
|
113
|
+
icono?: NombreDeIcono;
|
|
114
|
+
textoBoton?: string;
|
|
115
|
+
cancelable?: boolean;
|
|
116
|
+
}): Promise<void> {
|
|
117
|
+
await mostrarDialogo({
|
|
118
|
+
titulo,
|
|
119
|
+
mensaje,
|
|
120
|
+
icono,
|
|
121
|
+
cancelable,
|
|
122
|
+
botones: [{ texto: textoBoton, estilo: "principal", valor: true }],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ==============================|| PARA EL HOST ||============================== //
|
|
127
|
+
|
|
128
|
+
/** El que se está mostrando, o `null`. */
|
|
129
|
+
export const dialogoActual = (): Dialogo<unknown> | null => estado.fila[0]?.dialogo ?? null;
|
|
130
|
+
|
|
131
|
+
export function suscribirDialogos(oyente: () => void): () => void {
|
|
132
|
+
estado.oyentes.add(oyente);
|
|
133
|
+
return () => {
|
|
134
|
+
estado.oyentes.delete(oyente);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Cierra `dialogo` con `valor` y pasa al siguiente de la fila. Sólo si `dialogo` sigue siendo el
|
|
140
|
+
* actual: el segundo toque de un doble toque llega con el diálogo que ya se contestó, y sin esta
|
|
141
|
+
* comparación contestaba también el siguiente de la fila sin que el operador lo viera.
|
|
142
|
+
*/
|
|
143
|
+
export function responderDialogo(dialogo: Dialogo<unknown>, valor: unknown): void {
|
|
144
|
+
if (estado.fila[0]?.dialogo !== dialogo) return;
|
|
145
|
+
const actual = estado.fila.shift();
|
|
146
|
+
actual?.responder(valor);
|
|
147
|
+
avisarCambio();
|
|
148
|
+
}
|