@acvl/mobile-components 0.0.1 → 0.0.3
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 +6 -5
- 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.map +1 -0
- package/lib/module/componentes/ContenidoDePantalla.js +2 -1
- package/lib/module/componentes/ContenidoDePantalla.js.map +1 -1
- package/lib/module/componentes/Tarjeta.js +19 -3
- package/lib/module/componentes/Tarjeta.js.map +1 -1
- package/lib/module/formularios/CampoDeSeleccion.js +27 -2
- package/lib/module/formularios/CampoDeSeleccion.js.map +1 -1
- package/lib/module/formularios/campos.js +10 -2
- package/lib/module/formularios/campos.js.map +1 -1
- package/lib/module/formularios/controlados.js +3 -1
- package/lib/module/formularios/controlados.js.map +1 -1
- package/lib/module/fotos/elegir.js +5 -78
- package/lib/module/fotos/elegir.js.map +1 -1
- package/lib/module/fotos/index.js +4 -3
- package/lib/module/fotos/index.js.map +1 -1
- package/lib/module/hoja/HojaDeLista.js +211 -0
- package/lib/module/hoja/HojaDeLista.js.map +1 -0
- package/lib/module/hoja/HojaInferior.js +5 -1
- package/lib/module/hoja/HojaInferior.js.map +1 -1
- package/lib/module/hoja/index.js +1 -0
- package/lib/module/hoja/index.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/listas/HojaDeFiltros.js +21 -11
- package/lib/module/listas/HojaDeFiltros.js.map +1 -1
- package/lib/module/listas/piezas.js +79 -16
- package/lib/module/listas/piezas.js.map +1 -1
- package/lib/module/pantallas/PantallaDeHistorial.js +328 -0
- package/lib/module/pantallas/PantallaDeHistorial.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.map +1 -0
- package/lib/typescript/src/componentes/ContenidoDePantalla.d.ts +6 -1
- package/lib/typescript/src/componentes/ContenidoDePantalla.d.ts.map +1 -1
- package/lib/typescript/src/componentes/Tarjeta.d.ts +11 -1
- package/lib/typescript/src/componentes/Tarjeta.d.ts.map +1 -1
- package/lib/typescript/src/formularios/CampoDeSeleccion.d.ts +4 -2
- package/lib/typescript/src/formularios/CampoDeSeleccion.d.ts.map +1 -1
- package/lib/typescript/src/formularios/campos.d.ts +8 -2
- package/lib/typescript/src/formularios/campos.d.ts.map +1 -1
- package/lib/typescript/src/formularios/controlados.d.ts.map +1 -1
- package/lib/typescript/src/fotos/elegir.d.ts +1 -31
- package/lib/typescript/src/fotos/elegir.d.ts.map +1 -1
- package/lib/typescript/src/fotos/index.d.ts +2 -2
- package/lib/typescript/src/fotos/index.d.ts.map +1 -1
- 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.map +1 -1
- package/lib/typescript/src/hoja/index.d.ts +1 -0
- package/lib/typescript/src/hoja/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/listas/HojaDeFiltros.d.ts +20 -4
- package/lib/typescript/src/listas/HojaDeFiltros.d.ts.map +1 -1
- package/lib/typescript/src/listas/piezas.d.ts +27 -5
- package/lib/typescript/src/listas/piezas.d.ts.map +1 -1
- package/lib/typescript/src/pantallas/PantallaDeHistorial.d.ts +65 -0
- package/lib/typescript/src/pantallas/PantallaDeHistorial.d.ts.map +1 -0
- package/package.json +6 -1
- package/src/camara/CampoDeFotos.tsx +132 -0
- package/src/camara/elegir.ts +113 -0
- package/src/camara/index.ts +15 -0
- package/src/componentes/ContenidoDePantalla.tsx +12 -2
- package/src/componentes/Tarjeta.tsx +16 -2
- package/src/formularios/CampoDeSeleccion.tsx +18 -4
- package/src/formularios/campos.tsx +14 -0
- package/src/formularios/controlados.tsx +3 -1
- package/src/fotos/elegir.ts +10 -88
- package/src/fotos/index.ts +4 -13
- package/src/hoja/HojaDeLista.tsx +214 -0
- package/src/hoja/HojaInferior.tsx +2 -1
- package/src/hoja/index.ts +1 -0
- package/src/index.ts +8 -0
- package/src/listas/HojaDeFiltros.tsx +39 -14
- package/src/listas/piezas.tsx +93 -17
- package/src/pantallas/PantallaDeHistorial.tsx +293 -0
- package/lib/module/fotos/reducir.js.map +0 -1
- package/lib/typescript/src/fotos/reducir.d.ts.map +0 -1
- /package/lib/module/{fotos → camara}/reducir.js +0 -0
- /package/lib/typescript/src/{fotos → camara}/reducir.d.ts +0 -0
- /package/src/{fotos → camara}/reducir.ts +0 -0
|
@@ -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
|
+
}));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { crearEstilos, renglon, tipografia } from "../tema";
|
|
2
|
+
import { EnHoja } from "../componentes/Tarjeta";
|
|
2
3
|
import { Text } from "../texto";
|
|
3
4
|
import BottomSheet, {
|
|
4
5
|
BottomSheetBackdrop,
|
|
@@ -82,7 +83,7 @@ export default function HojaInferior({
|
|
|
82
83
|
<Text style={styles.titulo}>{titulo}</Text>
|
|
83
84
|
{subtitulo ? <Text style={styles.subtitulo}>{subtitulo}</Text> : null}
|
|
84
85
|
</View>
|
|
85
|
-
{abierta ? children : null}
|
|
86
|
+
<EnHoja.Provider value>{abierta ? children : null}</EnHoja.Provider>
|
|
86
87
|
</BottomSheetScrollView>
|
|
87
88
|
</BottomSheet>
|
|
88
89
|
);
|
package/src/hoja/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// `@acvl/mobile-components/hoja`: lo que sube desde abajo sobre una ficha. Va aparte de la entrada
|
|
2
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";
|
|
3
4
|
export { default as HojaInferior } from "./HojaInferior";
|
|
4
5
|
export { useMasDeLaFicha, type ListaDeLaFicha } from "./useMasDeLaFicha";
|
package/src/index.ts
CHANGED
|
@@ -112,6 +112,7 @@ export {
|
|
|
112
112
|
OpcionDeFiltro,
|
|
113
113
|
OpcionesMultiples,
|
|
114
114
|
OrdenDeLista,
|
|
115
|
+
type CampoDeOrden,
|
|
115
116
|
SeccionDeFiltros,
|
|
116
117
|
} from "./listas/HojaDeFiltros";
|
|
117
118
|
export { default as ListaPaginada, type ConsultaPaginada } from "./listas/ListaPaginada";
|
|
@@ -123,6 +124,7 @@ export {
|
|
|
123
124
|
EstadoDeHoja,
|
|
124
125
|
FilaConContador,
|
|
125
126
|
RenglonDeContacto,
|
|
127
|
+
RenglonDeDireccion,
|
|
126
128
|
RenglonDeHoja,
|
|
127
129
|
RenglonDeLiga,
|
|
128
130
|
SeccionDeFicha,
|
|
@@ -140,4 +142,10 @@ export {
|
|
|
140
142
|
} from "./pantallas/PantallaDeAcceso";
|
|
141
143
|
export { EsperaDeEdicion, FormularioDeEdicion } from "./pantallas/PantallaDeEdicion";
|
|
142
144
|
export { default as PantallaDeFicha } from "./pantallas/PantallaDeFicha";
|
|
145
|
+
export {
|
|
146
|
+
default as PantallaDeHistorial,
|
|
147
|
+
TarjetaDeEvento,
|
|
148
|
+
type EventoDeHistorial,
|
|
149
|
+
type Historial,
|
|
150
|
+
} from "./pantallas/PantallaDeHistorial";
|
|
143
151
|
export { default as PantallaDePerfil } from "./pantallas/PantallaDePerfil";
|
|
@@ -172,6 +172,23 @@ export function InterruptorDeFiltro({
|
|
|
172
172
|
);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/** Un campo por el que se ordena una lista. */
|
|
176
|
+
export interface CampoDeOrden {
|
|
177
|
+
/** El `ordering` sin signo: `codigo`. */
|
|
178
|
+
value: string;
|
|
179
|
+
label: string;
|
|
180
|
+
/**
|
|
181
|
+
* Cómo se dicen sus dos sentidos. «Ascendente» no dice nada de una fecha ni de un importe:
|
|
182
|
+
* ahí van «Antiguos primero» / «Recientes primero», «Menor primero» / «Mayor primero».
|
|
183
|
+
*/
|
|
184
|
+
sentidos?: { asc: string; desc: string };
|
|
185
|
+
/**
|
|
186
|
+
* El sentido con el que arranca al elegirlo: lo más nuevo y lo más grande arriba, el texto
|
|
187
|
+
* alfabético. Sin esto conserva el del campo de antes.
|
|
188
|
+
*/
|
|
189
|
+
primero?: "asc" | "desc";
|
|
190
|
+
}
|
|
191
|
+
|
|
175
192
|
/**
|
|
176
193
|
* «Ordenar por» y su sentido. `orden` es el `ordering` que se manda (`-codigo`), o `undefined` si
|
|
177
194
|
* se deja el del servidor; con `porOmision`, ese «sin orden» se ofrece como opción.
|
|
@@ -182,7 +199,7 @@ export function OrdenDeLista({
|
|
|
182
199
|
onOrdenar,
|
|
183
200
|
porOmision,
|
|
184
201
|
}: {
|
|
185
|
-
campos: readonly
|
|
202
|
+
campos: readonly CampoDeOrden[];
|
|
186
203
|
orden: string | undefined;
|
|
187
204
|
/** El nuevo `ordering`, o `null` para el del servidor. */
|
|
188
205
|
onOrdenar: (orden: string | null) => void;
|
|
@@ -190,6 +207,12 @@ export function OrdenDeLista({
|
|
|
190
207
|
}) {
|
|
191
208
|
const descendente = orden?.startsWith("-") ?? false;
|
|
192
209
|
const campo = orden?.replace(/^-/, "");
|
|
210
|
+
const elegido = campos.find((c) => c.value === campo);
|
|
211
|
+
const sentidos = elegido?.sentidos ?? { asc: "Ascendente", desc: "Descendente" };
|
|
212
|
+
// El sentido con el que arranca va primero: en una fecha se lee «Recientes primero» antes
|
|
213
|
+
// que «Antiguos primero».
|
|
214
|
+
const ordenDeSentidos: ("asc" | "desc")[] =
|
|
215
|
+
elegido?.primero === "desc" ? ["desc", "asc"] : ["asc", "desc"];
|
|
193
216
|
return (
|
|
194
217
|
<>
|
|
195
218
|
<SeccionDeFiltros>Ordenar por</SeccionDeFiltros>
|
|
@@ -207,24 +230,26 @@ export function OrdenDeLista({
|
|
|
207
230
|
texto={c.label}
|
|
208
231
|
unica
|
|
209
232
|
elegida={campo === c.value}
|
|
210
|
-
onPress={() =>
|
|
233
|
+
onPress={() => {
|
|
234
|
+
// Volver a tocar el elegido no le cambia el sentido.
|
|
235
|
+
if (campo === c.value) return;
|
|
236
|
+
const desc = c.primero ? c.primero === "desc" : descendente;
|
|
237
|
+
onOrdenar(`${desc ? "-" : ""}${c.value}`);
|
|
238
|
+
}}
|
|
211
239
|
/>
|
|
212
240
|
))}
|
|
213
241
|
{campo !== undefined ? (
|
|
214
242
|
<>
|
|
215
243
|
<SeccionDeFiltros>Sentido</SeccionDeFiltros>
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
elegida={descendente}
|
|
226
|
-
onPress={() => onOrdenar(`-${campo}`)}
|
|
227
|
-
/>
|
|
244
|
+
{ordenDeSentidos.map((sentido) => (
|
|
245
|
+
<OpcionDeFiltro
|
|
246
|
+
key={sentido}
|
|
247
|
+
texto={sentidos[sentido]}
|
|
248
|
+
unica
|
|
249
|
+
elegida={descendente === (sentido === "desc")}
|
|
250
|
+
onPress={() => onOrdenar(sentido === "desc" ? `-${campo}` : campo)}
|
|
251
|
+
/>
|
|
252
|
+
))}
|
|
228
253
|
</>
|
|
229
254
|
) : null}
|
|
230
255
|
</>
|
package/src/listas/piezas.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import Chip from "../componentes/Chip";
|
|
|
2
2
|
import IconoDeRenglon from "../componentes/IconoDeRenglon";
|
|
3
3
|
import Pestanas from "../componentes/Pestanas";
|
|
4
4
|
import Tarjeta from "../componentes/Tarjeta";
|
|
5
|
-
import { crearEstilos, renglon, tipografia, usePaleta } from "../tema";
|
|
5
|
+
import { crearEstilos, fuenteBase, renglon, tipografia, usePaleta } from "../tema";
|
|
6
6
|
import { type Pestana } from "../componentes/Pestanas";
|
|
7
7
|
import { Text, TextInput } from "../texto";
|
|
8
8
|
import { type TonoDeChip } from "../componentes/Chip";
|
|
@@ -13,9 +13,11 @@ import React from "react";
|
|
|
13
13
|
import {
|
|
14
14
|
ActivityIndicator,
|
|
15
15
|
Linking,
|
|
16
|
+
Platform,
|
|
16
17
|
Pressable,
|
|
17
18
|
View,
|
|
18
19
|
type StyleProp,
|
|
20
|
+
type TextInputProps,
|
|
19
21
|
type ViewStyle,
|
|
20
22
|
} from "react-native";
|
|
21
23
|
|
|
@@ -32,6 +34,9 @@ import {
|
|
|
32
34
|
* **El texto de sugerencia va en un renglón, con «…» si no cabe.** Es un `Text` encima del campo y
|
|
33
35
|
* no el `placeholder` del `TextInput`: el de Android parte en dos renglones lo que no cabe, y el
|
|
34
36
|
* segundo queda cortado a la mitad.
|
|
37
|
+
*
|
|
38
|
+
* Dentro de una hoja de `@gorhom/bottom-sheet` el campo es el suyo (`entrada`, su
|
|
39
|
+
* `BottomSheetTextInput`): con el de React Native el teclado tapa la hoja en vez de subirla.
|
|
35
40
|
*/
|
|
36
41
|
export function Buscador({
|
|
37
42
|
texto,
|
|
@@ -40,6 +45,7 @@ export function Buscador({
|
|
|
40
45
|
autoFocus,
|
|
41
46
|
onBlur,
|
|
42
47
|
style,
|
|
48
|
+
entrada: Entrada,
|
|
43
49
|
}: {
|
|
44
50
|
texto: string;
|
|
45
51
|
onCambiar: (texto: string) => void;
|
|
@@ -47,9 +53,12 @@ export function Buscador({
|
|
|
47
53
|
autoFocus?: boolean;
|
|
48
54
|
onBlur?: () => void;
|
|
49
55
|
style?: StyleProp<ViewStyle>;
|
|
56
|
+
/** El campo en el que se escribe, si no es el `TextInput` de la app. */
|
|
57
|
+
entrada?: React.ComponentType<TextInputProps>;
|
|
50
58
|
}) {
|
|
51
59
|
const c = usePaleta();
|
|
52
60
|
const styles = useStyles();
|
|
61
|
+
const Campo = Entrada ?? TextInput;
|
|
53
62
|
return (
|
|
54
63
|
<View style={[styles.buscador, style]}>
|
|
55
64
|
<MaterialCommunityIcons name="magnify" size={24} color={c.secondary} />
|
|
@@ -64,7 +73,7 @@ export function Buscador({
|
|
|
64
73
|
{placeholder}
|
|
65
74
|
</Text>
|
|
66
75
|
) : null}
|
|
67
|
-
<
|
|
76
|
+
<Campo
|
|
68
77
|
value={texto}
|
|
69
78
|
onChangeText={onCambiar}
|
|
70
79
|
accessibilityLabel={placeholder}
|
|
@@ -72,7 +81,9 @@ export function Buscador({
|
|
|
72
81
|
autoFocus={autoFocus}
|
|
73
82
|
onBlur={onBlur}
|
|
74
83
|
returnKeyType="search"
|
|
75
|
-
|
|
84
|
+
selectionColor={c.primary}
|
|
85
|
+
// El de la app ya trae su letra y su color; uno de fuera, no.
|
|
86
|
+
style={[fuenteBase, { color: c.texto }, styles.entrada]}
|
|
76
87
|
/>
|
|
77
88
|
</View>
|
|
78
89
|
{texto ? (
|
|
@@ -138,6 +149,10 @@ function BotonDeFiltros({ cuenta, punto, onPress }: FiltrosDeCabecera) {
|
|
|
138
149
|
/**
|
|
139
150
|
* Lo de arriba de una lista: el buscador, con el botón de sus filtros a la derecha si los tiene, y
|
|
140
151
|
* las pestañas que la cortan («En curso», «Por iniciar»). Con una sola pestaña no se pintan.
|
|
152
|
+
*
|
|
153
|
+
* Una lista cuyo servidor no busca por texto (una bandeja que sólo se filtra) va sin `placeholder`:
|
|
154
|
+
* entonces no lleva buscador —uno que no filtrara nada engañaría— y el botón de los filtros queda
|
|
155
|
+
* solo, a la derecha.
|
|
141
156
|
*/
|
|
142
157
|
export function CabeceraDeLista<K extends string>({
|
|
143
158
|
texto,
|
|
@@ -148,9 +163,10 @@ export function CabeceraDeLista<K extends string>({
|
|
|
148
163
|
onPestana,
|
|
149
164
|
filtros,
|
|
150
165
|
}: {
|
|
151
|
-
texto
|
|
152
|
-
onBuscar
|
|
153
|
-
|
|
166
|
+
texto?: string;
|
|
167
|
+
onBuscar?: (texto: string) => void;
|
|
168
|
+
/** Sin esto no hay buscador. */
|
|
169
|
+
placeholder?: string;
|
|
154
170
|
pestanas?: readonly Pestana<K>[];
|
|
155
171
|
activa?: K;
|
|
156
172
|
onPestana?: (key: K) => void;
|
|
@@ -159,19 +175,21 @@ export function CabeceraDeLista<K extends string>({
|
|
|
159
175
|
const styles = useStyles();
|
|
160
176
|
return (
|
|
161
177
|
<View style={styles.cabecera}>
|
|
162
|
-
{filtros ? (
|
|
178
|
+
{placeholder !== undefined || filtros ? (
|
|
163
179
|
<View style={styles.filaDeBusqueda}>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
180
|
+
{placeholder !== undefined ? (
|
|
181
|
+
<Buscador
|
|
182
|
+
texto={texto ?? ""}
|
|
183
|
+
onCambiar={onBuscar ?? (() => {})}
|
|
184
|
+
placeholder={placeholder}
|
|
185
|
+
style={styles.flex}
|
|
186
|
+
/>
|
|
187
|
+
) : (
|
|
188
|
+
<View style={styles.flex} />
|
|
189
|
+
)}
|
|
190
|
+
{filtros ? <BotonDeFiltros {...filtros} /> : null}
|
|
171
191
|
</View>
|
|
172
|
-
) :
|
|
173
|
-
<Buscador texto={texto} onCambiar={onBuscar} placeholder={placeholder} />
|
|
174
|
-
)}
|
|
192
|
+
) : null}
|
|
175
193
|
{pestanas && pestanas.length > 1 && activa && onPestana ? (
|
|
176
194
|
<Pestanas pestanas={pestanas} activa={activa} onSelect={onPestana} />
|
|
177
195
|
) : null}
|
|
@@ -382,6 +400,64 @@ export function RenglonDeContacto({
|
|
|
382
400
|
);
|
|
383
401
|
}
|
|
384
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Abre una dirección en la app de mapas del teléfono: Apple Maps en iOS y, en Android, la que
|
|
405
|
+
* atienda `geo:` (Google Maps, Waze…). Si ninguna la atiende, Google Maps en el navegador.
|
|
406
|
+
*/
|
|
407
|
+
const abrirEnMapas = (direccion: string) => {
|
|
408
|
+
const q = encodeURIComponent(direccion);
|
|
409
|
+
const nativa = Platform.OS === "ios" ? `maps:0,0?q=${q}` : `geo:0,0?q=${q}`;
|
|
410
|
+
Linking.openURL(nativa).catch(() =>
|
|
411
|
+
Linking.openURL(`https://www.google.com/maps/search/?api=1&query=${q}`).catch(() => {}),
|
|
412
|
+
);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Una dirección **que se abre en el mapa del teléfono**: el renglón entero es el botón, para
|
|
417
|
+
* llegar a donde se entrega sin copiarla a mano. Las líneas que falten no se pintan; sin ninguna,
|
|
418
|
+
* no hay renglón. Es el hermano de `RenglonDeContacto`.
|
|
419
|
+
*/
|
|
420
|
+
export function RenglonDeDireccion({
|
|
421
|
+
etiqueta = "Dirección",
|
|
422
|
+
lineas,
|
|
423
|
+
enHoja,
|
|
424
|
+
}: {
|
|
425
|
+
etiqueta?: string;
|
|
426
|
+
/** La calle, la colonia, «Ciudad, Estado, CP», el país: un renglón cada una. */
|
|
427
|
+
lineas: (string | null | undefined)[];
|
|
428
|
+
/** En una hoja (`HojaInferior`), del color del fondo, como `RenglonDeContacto`. */
|
|
429
|
+
enHoja?: boolean;
|
|
430
|
+
}) {
|
|
431
|
+
const c = usePaleta();
|
|
432
|
+
const styles = useStyles();
|
|
433
|
+
const presentes = lineas.map((l) => l?.trim()).filter((l): l is string => Boolean(l));
|
|
434
|
+
if (!presentes.length) return null;
|
|
435
|
+
return (
|
|
436
|
+
<Pressable
|
|
437
|
+
onPress={() => abrirEnMapas(presentes.join(", "))}
|
|
438
|
+
accessibilityRole="link"
|
|
439
|
+
accessibilityLabel={`Abrir en el mapa: ${presentes.join(", ")}`}
|
|
440
|
+
style={({ pressed }) => [
|
|
441
|
+
styles.renglon,
|
|
442
|
+
enHoja && styles.renglonEnHoja,
|
|
443
|
+
pressed && styles.presionada,
|
|
444
|
+
]}
|
|
445
|
+
>
|
|
446
|
+
<View style={styles.renglonTexto}>
|
|
447
|
+
<Text style={styles.etiqueta}>{etiqueta}</Text>
|
|
448
|
+
{presentes.map((l, i) => (
|
|
449
|
+
<Text key={i} style={i === 0 ? styles.valor : styles.linea}>
|
|
450
|
+
{l}
|
|
451
|
+
</Text>
|
|
452
|
+
))}
|
|
453
|
+
</View>
|
|
454
|
+
<View style={styles.accion}>
|
|
455
|
+
<MaterialCommunityIcons name="map-marker-outline" size={24} color={c.primary} />
|
|
456
|
+
</View>
|
|
457
|
+
</Pressable>
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
385
461
|
/**
|
|
386
462
|
* Un renglón de una hoja (`HojaInferior`): el título con su chip y lo que acompaña. **No es
|
|
387
463
|
* `TarjetaDeLista`**: ésa es del color de la hoja y adentro de ella no se distinguía un renglón del
|