@cerca.red/mapa-react 0.2.0

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/dist/Mapa.d.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { type ReactNode, type Ref } from "react";
2
+ import type { Caja, Coordenada } from "@cerca.red/geo";
3
+ import type { BandaDeIsocrona, EventosDelMapa, FabricaDeEstilo, MapaDeCerca, Margenes, NegocioEnMapa, PaletaDelMapa, ResultadosEnMapa, RutaEnMapa, SimboloDeMapa, Tema, UbicacionDeUsuarioEnMapa, VehiculoEnMapa, ZonaEnMapa } from "@cerca.red/mapa";
4
+ /**
5
+ * `<Mapa>` — el mapa de Cerca dentro de React.
6
+ *
7
+ * ## Las dos cosas que este componente hace por dentro para que nadie tenga
8
+ * que acordarse
9
+ *
10
+ * **Carga el motor de forma diferida y solo en el cliente.** El `import()` de
11
+ * `@cerca.red/mapa/navegador` está dentro de un efecto, y los efectos no
12
+ * corren en el servidor: así el build de Next sobre Workers nunca ve
13
+ * `maplibre-gl` en el grafo del servidor, sin que la superficie tenga que
14
+ * envolver nada. Se hace con `import()` y no con `next/dynamic` para que este
15
+ * paquete sirva también fuera de Next — la landing, el laboratorio, lo que
16
+ * venga.
17
+ *
18
+ * Como efecto secundario, el motor tampoco entra en la primera pintura: en
19
+ * una pantalla de resultados el estado por defecto es la lista, y el
20
+ * presupuesto de carga no paga los 250 KB hasta que alguien pide el mapa.
21
+ *
22
+ * **Exige un respaldo.** `respaldo` no lleva `?`, y esa es toda la diferencia
23
+ * entre una degradación que existe y una que se documentó. MapLibre 6 exige
24
+ * WebGL2 sin alternativa; cuando no lo hay, lo que se ve sin respaldo es un
25
+ * rectángulo vacío que parece estar cargando para siempre. SDD §13 pide la
26
+ * degradación mapa→lista, y aquí no se puede omitir porque el tipo no deja
27
+ * compilar sin ella.
28
+ */
29
+ interface MapaPropsBase {
30
+ /** Quien sabe construir el estilo. Normalmente, el de `@cerca.red/mapa-estilos`. */
31
+ readonly estilo: FabricaDeEstilo;
32
+ /** La URL del archivo de teselas, tal como vino del manifiesto. */
33
+ readonly urlDeTeselas: string;
34
+ /**
35
+ * Lo que se enseña cuando no hay mapa: sin WebGL2, o mientras el motor
36
+ * carga, o si la carga falla.
37
+ *
38
+ * **No es opcional.** Ver arriba.
39
+ */
40
+ readonly respaldo: ReactNode;
41
+ readonly seleccionado?: string | undefined;
42
+ readonly ubicacionDeUsuario?: UbicacionDeUsuarioEnMapa | undefined;
43
+ readonly vehiculos?: readonly VehiculoEnMapa[];
44
+ readonly rutas?: readonly RutaEnMapa[];
45
+ readonly zonas?: readonly ZonaEnMapa[];
46
+ readonly isocronas?: readonly BandaDeIsocrona[];
47
+ readonly simbolos?: readonly SimboloDeMapa[];
48
+ readonly tema?: Tema;
49
+ readonly idioma?: string;
50
+ readonly margenes?: Margenes;
51
+ readonly colores?: Partial<PaletaDelMapa>;
52
+ readonly centro?: Coordenada;
53
+ readonly zoom?: number;
54
+ readonly cajaMaxima?: Caja;
55
+ /**
56
+ * De dónde bajar `maplibre-gl-worker.mjs`.
57
+ *
58
+ * Hace falta en Next.js, que reescribe el `import.meta.url` con el que
59
+ * MapLibre deduce esa URL y deja al motor pidiendo un archivo que no
60
+ * existe. Bajo Vite no hace falta.
61
+ *
62
+ * Es una prop y no una llamada suelta al arrancar porque este componente
63
+ * carga el motor de forma diferida: `@cerca.red/mapa/navegador` no existe
64
+ * hasta que el efecto lo importa, y llamar antes a `fijarUrlDelWorker`
65
+ * obligaría a la superficie a importar el motor en el módulo — que es justo
66
+ * lo que la carga diferida evita.
67
+ */
68
+ readonly urlDelWorker?: string;
69
+ /**
70
+ * Adónde lleva el enlace de licencias en la esquina inferior derecha.
71
+ *
72
+ * Se pasa tal cual a `crearMapa`; sin esta prop no se añade el enlace. No
73
+ * reemplaza la atribución de OpenStreetMap del control de MapLibre —esa
74
+ * viene sola desde los metadatos del `.pmtiles`—, es el enlace al
75
+ * documento que junta esa atribución con el resto de la cadena de
76
+ * licencias.
77
+ */
78
+ readonly urlDeLicencias?: string;
79
+ readonly onListo?: (evento: EventosDelMapa["mapa:listo"]) => void;
80
+ readonly onNegocioClic?: (evento: EventosDelMapa["negocio:clic"]) => void;
81
+ readonly onNegocioEncima?: (evento: EventosDelMapa["negocio:encima"]) => void;
82
+ readonly onSedeClic?: (evento: EventosDelMapa["sede:clic"]) => void;
83
+ readonly onGrupoClic?: (evento: EventosDelMapa["grupo:clic"]) => void;
84
+ readonly onRutaEleccion?: (evento: EventosDelMapa["ruta:eleccion"]) => void;
85
+ readonly onVehiculoClic?: (evento: EventosDelMapa["vehiculo:clic"]) => void;
86
+ readonly onMovimiento?: (evento: EventosDelMapa["mapa:movimiento"]) => void;
87
+ readonly onError?: (evento: EventosDelMapa["mapa:error"]) => void;
88
+ /** Para llamar a `encuadrar`, `volarA` y demás desde fuera. */
89
+ readonly ref?: Ref<MapaDeCerca | null>;
90
+ readonly className?: string;
91
+ readonly "aria-label"?: string;
92
+ }
93
+ /**
94
+ * `resultados` reemplaza grupos o negocios en una sola orden. `negocios`
95
+ * sigue disponible para consumidores anteriores, pero no se pueden combinar:
96
+ * no habría una regla inequívoca para decidir cuál prop gana.
97
+ */
98
+ export type MapaProps = MapaPropsBase & ({
99
+ readonly resultados: ResultadosEnMapa;
100
+ readonly negocios?: never;
101
+ } | {
102
+ readonly resultados?: never;
103
+ readonly negocios?: readonly NegocioEnMapa[];
104
+ });
105
+ export declare function Mapa({ estilo, urlDeTeselas, respaldo, resultados, negocios, seleccionado, ubicacionDeUsuario, vehiculos, rutas, zonas, isocronas, simbolos, tema, idioma, margenes, colores, centro, zoom, cajaMaxima, urlDelWorker, urlDeLicencias, onListo, onNegocioClic, onNegocioEncima, onSedeClic, onGrupoClic, onRutaEleccion, onVehiculoClic, onMovimiento, onError, ref, className, "aria-label": etiqueta, }: MapaProps): import("react").JSX.Element;
106
+ export {};
package/dist/Mapa.js ADDED
@@ -0,0 +1,190 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useImperativeHandle, useRef, useState, } from "react";
4
+ import { atarEventos } from "./suscripciones.js";
5
+ export function Mapa({ estilo, urlDeTeselas, respaldo, resultados, negocios, seleccionado, ubicacionDeUsuario, vehiculos, rutas, zonas, isocronas, simbolos, tema = "claro", idioma = "es", margenes, colores, centro, zoom, cajaMaxima, urlDelWorker, urlDeLicencias, onListo, onNegocioClic, onNegocioEncima, onSedeClic, onGrupoClic, onRutaEleccion, onVehiculoClic, onMovimiento, onError, ref, className, "aria-label": etiqueta = "Mapa de negocios cercanos", }) {
6
+ if (resultados !== undefined && negocios !== undefined) {
7
+ throw new Error("Mapa: usa `resultados` o `negocios`, no ambas props a la vez.");
8
+ }
9
+ const contenedor = useRef(null);
10
+ const mapa = useRef(null);
11
+ const [estado, setEstado] = useState("cargando");
12
+ /**
13
+ * Los callbacks más recientes, en una caja que no cambia de identidad.
14
+ *
15
+ * Sin esto habría que volver a suscribirse en cada render en el que la
16
+ * superficie pase una función nueva —o sea, en cada render, porque casi
17
+ * nadie memoriza sus manejadores—, y cada re-suscripción es un `off` y un
18
+ * `on` sobre el motor. Con la caja, la suscripción se hace una vez y
19
+ * siempre llama a la última versión.
20
+ */
21
+ const manejadores = useRef({
22
+ onListo,
23
+ onNegocioClic,
24
+ onNegocioEncima,
25
+ onSedeClic,
26
+ onGrupoClic,
27
+ onRutaEleccion,
28
+ onVehiculoClic,
29
+ onMovimiento,
30
+ onError,
31
+ });
32
+ manejadores.current = {
33
+ onListo,
34
+ onNegocioClic,
35
+ onNegocioEncima,
36
+ onSedeClic,
37
+ onGrupoClic,
38
+ onRutaEleccion,
39
+ onVehiculoClic,
40
+ onMovimiento,
41
+ onError,
42
+ };
43
+ // Sin array de dependencias: se recalcula en cada render, que es lo que
44
+ // hace que la referencia deje de ser `null` en cuanto el mapa existe. Con
45
+ // `[estado]` el linter avisa de que la dependencia no se usa dentro, y
46
+ // tiene razón: lo que cambia no es `estado`, es `mapa.current`, que es un
47
+ // ref y por definición no dispara renders.
48
+ // Los dos parámetros de tipo explícitos: sin ellos, React infiere el
49
+ // primero del `Ref` y el segundo del valor devuelto, y se queja de que
50
+ // `null` no encaja en `MapaDeCerca`. `null` es justo lo que hay antes de
51
+ // que el motor cargue, así que el tipo correcto lo incluye.
52
+ useImperativeHandle(ref, () => mapa.current);
53
+ // — montaje —
54
+ //
55
+ // Sin dependencias: el mapa se construye una vez y vive hasta que el
56
+ // componente se va. Cambiar el estilo, el tema o los datos son órdenes que
57
+ // se le dan al mapa vivo, no un motivo para tirarlo y volver a levantarlo
58
+ // —que costaría un contexto WebGL nuevo y una recarga entera de teselas—.
59
+ useEffect(() => {
60
+ let cancelado = false;
61
+ void (async () => {
62
+ try {
63
+ const { crearMapa, hayWebGL2 } = await import("@cerca.red/mapa/navegador");
64
+ // Dos comprobaciones y no una: el componente puede haberse desmontado
65
+ // mientras se descargaba el motor, y en React 19 en modo estricto eso
66
+ // pasa siempre en desarrollo.
67
+ if (cancelado)
68
+ return;
69
+ if (!hayWebGL2()) {
70
+ setEstado("sin-webgl");
71
+ return;
72
+ }
73
+ const elemento = contenedor.current;
74
+ if (elemento === null)
75
+ return;
76
+ const construido = crearMapa({
77
+ contenedor: elemento,
78
+ estilo,
79
+ urlDeTeselas,
80
+ tema,
81
+ idioma,
82
+ ...(centro === undefined ? {} : { centro }),
83
+ ...(zoom === undefined ? {} : { zoom }),
84
+ ...(margenes === undefined ? {} : { margenes }),
85
+ ...(colores === undefined ? {} : { colores }),
86
+ ...(cajaMaxima === undefined ? {} : { cajaMaxima }),
87
+ ...(simbolos === undefined ? {} : { simbolos }),
88
+ ...(urlDelWorker === undefined ? {} : { urlDelWorker }),
89
+ ...(urlDeLicencias === undefined ? {} : { urlDeLicencias }),
90
+ });
91
+ if (cancelado) {
92
+ // Se desmontó entre el `hayWebGL2` y aquí. Sin este destruir, el
93
+ // contexto WebGL y el protocolo pmtiles se quedan colgados, y un
94
+ // navegador aguanta muy pocos contextos a la vez.
95
+ construido.destruir();
96
+ return;
97
+ }
98
+ mapa.current = construido;
99
+ atarEventos(construido, manejadores);
100
+ setEstado("listo");
101
+ }
102
+ catch (fallo) {
103
+ if (cancelado)
104
+ return;
105
+ setEstado("fallo");
106
+ manejadores.current.onError?.({
107
+ error: fallo instanceof Error ? fallo : new Error(String(fallo)),
108
+ });
109
+ }
110
+ })();
111
+ return () => {
112
+ cancelado = true;
113
+ mapa.current?.destruir();
114
+ mapa.current = null;
115
+ };
116
+ // eslint-disable-next-line react-hooks/exhaustive-deps
117
+ }, []);
118
+ // — datos —
119
+ useEffect(() => {
120
+ if (estado !== "listo")
121
+ return;
122
+ mapa.current?.ponerResultados(resultados ?? { modo: "negocios", negocios: negocios ?? [] });
123
+ }, [estado, negocios, resultados]);
124
+ useEffect(() => {
125
+ if (estado !== "listo")
126
+ return;
127
+ // Sin `emitirEvento`: seleccionar desde una prop **no** debe emitir
128
+ // `negocio:clic`. Si lo hiciera, tocar un resultado en la lista abriría la
129
+ // ficha dos veces —una por el toque y otra por el eco del mapa— y la
130
+ // pantalla entraría en un ciclo que no se ve como un ciclo.
131
+ mapa.current?.seleccionarNegocio(resultados?.modo === "grupos" ? undefined : seleccionado);
132
+ }, [estado, seleccionado, negocios, resultados]);
133
+ useEffect(() => {
134
+ if (estado !== "listo")
135
+ return;
136
+ mapa.current?.ponerUbicacionDeUsuario(ubicacionDeUsuario);
137
+ }, [estado, ubicacionDeUsuario]);
138
+ useEffect(() => {
139
+ if (estado !== "listo")
140
+ return;
141
+ mapa.current?.ponerVehiculos(vehiculos ?? []);
142
+ }, [estado, vehiculos]);
143
+ useEffect(() => {
144
+ if (estado !== "listo")
145
+ return;
146
+ mapa.current?.mostrarRuta(rutas ?? []);
147
+ }, [estado, rutas]);
148
+ useEffect(() => {
149
+ if (estado !== "listo")
150
+ return;
151
+ mapa.current?.mostrarZonaDeCobertura(zonas ?? []);
152
+ }, [estado, zonas]);
153
+ useEffect(() => {
154
+ if (estado !== "listo")
155
+ return;
156
+ mapa.current?.mostrarIsocrona(isocronas ?? []);
157
+ }, [estado, isocronas]);
158
+ useEffect(() => {
159
+ if (estado !== "listo" || simbolos === undefined)
160
+ return;
161
+ mapa.current?.registrarSimbolos(simbolos);
162
+ }, [estado, simbolos]);
163
+ // — apariencia —
164
+ useEffect(() => {
165
+ if (estado !== "listo")
166
+ return;
167
+ mapa.current?.ponerTema(tema);
168
+ }, [estado, tema]);
169
+ useEffect(() => {
170
+ if (estado !== "listo")
171
+ return;
172
+ mapa.current?.ponerIdioma(idioma);
173
+ }, [estado, idioma]);
174
+ useEffect(() => {
175
+ if (estado !== "listo" || margenes === undefined)
176
+ return;
177
+ mapa.current?.ponerMargenes(margenes);
178
+ }, [estado, margenes]);
179
+ const enContenedor = useCallback((elemento) => {
180
+ contenedor.current = elemento;
181
+ }, []);
182
+ if (estado === "sin-webgl" || estado === "fallo")
183
+ return _jsx(_Fragment, { children: respaldo });
184
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: enContenedor, className: className,
185
+ // El canvas no es accesible por sí solo: quien navegue con teclado o
186
+ // con lector de pantalla necesita la lista, no esto. El rol y la
187
+ // etiqueta son lo mínimo para que no se anuncie como un div vacío.
188
+ role: "application", "aria-label": etiqueta, style: { width: "100%", height: "100%" } }), estado === "cargando" ? respaldo : null] }));
189
+ }
190
+ //# sourceMappingURL=Mapa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Mapa.js","sourceRoot":"","sources":["../src/Mapa.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EACL,WAAW,EACX,SAAS,EACT,mBAAmB,EACnB,MAAM,EACN,QAAQ,GAGT,MAAM,OAAO,CAAC;AAGf,OAAO,EAAE,WAAW,EAA2B,MAAM,oBAAoB,CAAC;AAoI1E,MAAM,UAAU,IAAI,CAAC,EACnB,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,kBAAkB,EAClB,SAAS,EACT,KAAK,EACL,KAAK,EACL,SAAS,EACT,QAAQ,EACR,IAAI,GAAG,OAAO,EACd,MAAM,GAAG,IAAI,EACb,QAAQ,EACR,OAAO,EACP,MAAM,EACN,IAAI,EACJ,UAAU,EACV,YAAY,EACZ,cAAc,EACd,OAAO,EACP,aAAa,EACb,eAAe,EACf,UAAU,EACV,WAAW,EACX,cAAc,EACd,cAAc,EACd,YAAY,EACZ,OAAO,EACP,GAAG,EACH,SAAS,EACT,YAAY,EAAE,QAAQ,GAAG,2BAA2B,GAC1C;IACV,IAAI,UAAU,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CACb,+DAA+D,CAChE,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAwB,IAAI,CAAC,CAAC;IACvD,MAAM,IAAI,GAAG,MAAM,CAAqB,IAAI,CAAC,CAAC;IAC9C,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAgB,UAAU,CAAC,CAAC;IAEhE;;;;;;;;OAQG;IACH,MAAM,WAAW,GAAG,MAAM,CAAqB;QAC7C,OAAO;QACP,aAAa;QACb,eAAe;QACf,UAAU;QACV,WAAW;QACX,cAAc;QACd,cAAc;QACd,YAAY;QACZ,OAAO;KACR,CAAC,CAAC;IACH,WAAW,CAAC,OAAO,GAAG;QACpB,OAAO;QACP,aAAa;QACb,eAAe;QACf,UAAU;QACV,WAAW;QACX,cAAc;QACd,cAAc;QACd,YAAY;QACZ,OAAO;KACR,CAAC;IAEF,wEAAwE;IACxE,0EAA0E;IAC1E,uEAAuE;IACvE,0EAA0E;IAC1E,2CAA2C;IAC3C,qEAAqE;IACrE,uEAAuE;IACvE,yEAAyE;IACzE,4DAA4D;IAC5D,mBAAmB,CACjB,GAAG,EACH,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CACnB,CAAC;IAEF,cAAc;IACd,EAAE;IACF,qEAAqE;IACrE,2EAA2E;IAC3E,0EAA0E;IAC1E,0EAA0E;IAC1E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,SAAS,GAAG,KAAK,CAAC;QAEtB,KAAK,CAAC,KAAK,IAAI,EAAE;YACf,IAAI,CAAC;gBACH,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAC5B,MAAM,MAAM,CAAC,2BAA2B,CAAC,CAAC;gBAE5C,sEAAsE;gBACtE,sEAAsE;gBACtE,8BAA8B;gBAC9B,IAAI,SAAS;oBAAE,OAAO;gBAEtB,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;oBACjB,SAAS,CAAC,WAAW,CAAC,CAAC;oBACvB,OAAO;gBACT,CAAC;gBAED,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAC;gBACpC,IAAI,QAAQ,KAAK,IAAI;oBAAE,OAAO;gBAE9B,MAAM,UAAU,GAAG,SAAS,CAAC;oBAC3B,UAAU,EAAE,QAAQ;oBACpB,MAAM;oBACN,YAAY;oBACZ,IAAI;oBACJ,MAAM;oBACN,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;oBAC3C,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;oBACvC,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;oBAC/C,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;oBAC7C,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC;oBACnD,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;oBAC/C,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC;oBACvD,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC;iBAC5D,CAAC,CAAC;gBAEH,IAAI,SAAS,EAAE,CAAC;oBACd,iEAAiE;oBACjE,iEAAiE;oBACjE,kDAAkD;oBAClD,UAAU,CAAC,QAAQ,EAAE,CAAC;oBACtB,OAAO;gBACT,CAAC;gBAED,IAAI,CAAC,OAAO,GAAG,UAAU,CAAC;gBAC1B,WAAW,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;gBACrC,SAAS,CAAC,OAAO,CAAC,CAAC;YACrB,CAAC;YAAC,OAAO,KAAc,EAAE,CAAC;gBACxB,IAAI,SAAS;oBAAE,OAAO;gBACtB,SAAS,CAAC,OAAO,CAAC,CAAC;gBACnB,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;oBAC5B,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;iBACjE,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QAEL,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAC;YACjB,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,CAAC;YACzB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,CAAC,CAAC;QACF,uDAAuD;IACzD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,YAAY;IAEZ,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO;QAC/B,IAAI,CAAC,OAAO,EAAE,eAAe,CAC3B,UAAU,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,IAAI,EAAE,EAAE,CAC7D,CAAC;IACJ,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;IAEnC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO;QAC/B,oEAAoE;QACpE,2EAA2E;QAC3E,qEAAqE;QACrE,4DAA4D;QAC5D,IAAI,CAAC,OAAO,EAAE,kBAAkB,CAC9B,UAAU,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CACzD,CAAC;IACJ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;IAEjD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO;QAC/B,IAAI,CAAC,OAAO,EAAE,uBAAuB,CAAC,kBAAkB,CAAC,CAAC;IAC5D,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAEjC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO;QAC/B,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAExB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO;QAC/B,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IACzC,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;IAEpB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO;QAC/B,IAAI,CAAC,OAAO,EAAE,sBAAsB,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;IAEpB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO;QAC/B,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAExB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,OAAO,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO;QACzD,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAC5C,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAEvB,iBAAiB;IAEjB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO;QAC/B,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;IAEnB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO;QAC/B,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAErB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,KAAK,OAAO,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO;QACzD,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAEvB,MAAM,YAAY,GAAG,WAAW,CAAC,CAAC,QAA+B,EAAE,EAAE;QACnE,UAAU,CAAC,OAAO,GAAG,QAAQ,CAAC;IAChC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,IAAI,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,4BAAG,QAAQ,GAAI,CAAC;IAEzE,OAAO,CACL,8BACE,cACE,GAAG,EAAE,YAAY,EACjB,SAAS,EAAE,SAAS;gBACpB,qEAAqE;gBACrE,iEAAiE;gBACjE,mEAAmE;gBACnE,IAAI,EAAC,aAAa,gBACN,QAAQ,EACpB,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GACxC,EACD,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,IACvC,CACJ,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type ReactNode,\n type Ref,\n} from \"react\";\n\nimport type { Caja, Coordenada } from \"@cerca.red/geo\";\nimport { atarEventos, type ManejadoresDelMapa } from \"./suscripciones.ts\";\nimport type {\n BandaDeIsocrona,\n EventosDelMapa,\n FabricaDeEstilo,\n MapaDeCerca,\n Margenes,\n NegocioEnMapa,\n PaletaDelMapa,\n ResultadosEnMapa,\n RutaEnMapa,\n SimboloDeMapa,\n Tema,\n UbicacionDeUsuarioEnMapa,\n VehiculoEnMapa,\n ZonaEnMapa,\n} from \"@cerca.red/mapa\";\n\n/**\n * `<Mapa>` — el mapa de Cerca dentro de React.\n *\n * ## Las dos cosas que este componente hace por dentro para que nadie tenga\n * que acordarse\n *\n * **Carga el motor de forma diferida y solo en el cliente.** El `import()` de\n * `@cerca.red/mapa/navegador` está dentro de un efecto, y los efectos no\n * corren en el servidor: así el build de Next sobre Workers nunca ve\n * `maplibre-gl` en el grafo del servidor, sin que la superficie tenga que\n * envolver nada. Se hace con `import()` y no con `next/dynamic` para que este\n * paquete sirva también fuera de Next — la landing, el laboratorio, lo que\n * venga.\n *\n * Como efecto secundario, el motor tampoco entra en la primera pintura: en\n * una pantalla de resultados el estado por defecto es la lista, y el\n * presupuesto de carga no paga los 250 KB hasta que alguien pide el mapa.\n *\n * **Exige un respaldo.** `respaldo` no lleva `?`, y esa es toda la diferencia\n * entre una degradación que existe y una que se documentó. MapLibre 6 exige\n * WebGL2 sin alternativa; cuando no lo hay, lo que se ve sin respaldo es un\n * rectángulo vacío que parece estar cargando para siempre. SDD §13 pide la\n * degradación mapa→lista, y aquí no se puede omitir porque el tipo no deja\n * compilar sin ella.\n */\n\ninterface MapaPropsBase {\n /** Quien sabe construir el estilo. Normalmente, el de `@cerca.red/mapa-estilos`. */\n readonly estilo: FabricaDeEstilo;\n /** La URL del archivo de teselas, tal como vino del manifiesto. */\n readonly urlDeTeselas: string;\n /**\n * Lo que se enseña cuando no hay mapa: sin WebGL2, o mientras el motor\n * carga, o si la carga falla.\n *\n * **No es opcional.** Ver arriba.\n */\n readonly respaldo: ReactNode;\n\n readonly seleccionado?: string | undefined;\n readonly ubicacionDeUsuario?: UbicacionDeUsuarioEnMapa | undefined;\n readonly vehiculos?: readonly VehiculoEnMapa[];\n readonly rutas?: readonly RutaEnMapa[];\n readonly zonas?: readonly ZonaEnMapa[];\n readonly isocronas?: readonly BandaDeIsocrona[];\n readonly simbolos?: readonly SimboloDeMapa[];\n\n readonly tema?: Tema;\n readonly idioma?: string;\n readonly margenes?: Margenes;\n readonly colores?: Partial<PaletaDelMapa>;\n readonly centro?: Coordenada;\n readonly zoom?: number;\n readonly cajaMaxima?: Caja;\n /**\n * De dónde bajar `maplibre-gl-worker.mjs`.\n *\n * Hace falta en Next.js, que reescribe el `import.meta.url` con el que\n * MapLibre deduce esa URL y deja al motor pidiendo un archivo que no\n * existe. Bajo Vite no hace falta.\n *\n * Es una prop y no una llamada suelta al arrancar porque este componente\n * carga el motor de forma diferida: `@cerca.red/mapa/navegador` no existe\n * hasta que el efecto lo importa, y llamar antes a `fijarUrlDelWorker`\n * obligaría a la superficie a importar el motor en el módulo — que es justo\n * lo que la carga diferida evita.\n */\n readonly urlDelWorker?: string;\n /**\n * Adónde lleva el enlace de licencias en la esquina inferior derecha.\n *\n * Se pasa tal cual a `crearMapa`; sin esta prop no se añade el enlace. No\n * reemplaza la atribución de OpenStreetMap del control de MapLibre —esa\n * viene sola desde los metadatos del `.pmtiles`—, es el enlace al\n * documento que junta esa atribución con el resto de la cadena de\n * licencias.\n */\n readonly urlDeLicencias?: string;\n\n readonly onListo?: (evento: EventosDelMapa[\"mapa:listo\"]) => void;\n readonly onNegocioClic?: (evento: EventosDelMapa[\"negocio:clic\"]) => void;\n readonly onNegocioEncima?: (evento: EventosDelMapa[\"negocio:encima\"]) => void;\n readonly onSedeClic?: (evento: EventosDelMapa[\"sede:clic\"]) => void;\n readonly onGrupoClic?: (evento: EventosDelMapa[\"grupo:clic\"]) => void;\n readonly onRutaEleccion?: (evento: EventosDelMapa[\"ruta:eleccion\"]) => void;\n readonly onVehiculoClic?: (evento: EventosDelMapa[\"vehiculo:clic\"]) => void;\n readonly onMovimiento?: (evento: EventosDelMapa[\"mapa:movimiento\"]) => void;\n readonly onError?: (evento: EventosDelMapa[\"mapa:error\"]) => void;\n\n /** Para llamar a `encuadrar`, `volarA` y demás desde fuera. */\n readonly ref?: Ref<MapaDeCerca | null>;\n readonly className?: string;\n readonly \"aria-label\"?: string;\n}\n\n/**\n * `resultados` reemplaza grupos o negocios en una sola orden. `negocios`\n * sigue disponible para consumidores anteriores, pero no se pueden combinar:\n * no habría una regla inequívoca para decidir cuál prop gana.\n */\nexport type MapaProps = MapaPropsBase &\n (\n | {\n readonly resultados: ResultadosEnMapa;\n readonly negocios?: never;\n }\n | {\n readonly resultados?: never;\n readonly negocios?: readonly NegocioEnMapa[];\n }\n );\n\ntype EstadoDeCarga = \"cargando\" | \"listo\" | \"sin-webgl\" | \"fallo\";\n\nexport function Mapa({\n estilo,\n urlDeTeselas,\n respaldo,\n resultados,\n negocios,\n seleccionado,\n ubicacionDeUsuario,\n vehiculos,\n rutas,\n zonas,\n isocronas,\n simbolos,\n tema = \"claro\",\n idioma = \"es\",\n margenes,\n colores,\n centro,\n zoom,\n cajaMaxima,\n urlDelWorker,\n urlDeLicencias,\n onListo,\n onNegocioClic,\n onNegocioEncima,\n onSedeClic,\n onGrupoClic,\n onRutaEleccion,\n onVehiculoClic,\n onMovimiento,\n onError,\n ref,\n className,\n \"aria-label\": etiqueta = \"Mapa de negocios cercanos\",\n}: MapaProps) {\n if (resultados !== undefined && negocios !== undefined) {\n throw new Error(\n \"Mapa: usa `resultados` o `negocios`, no ambas props a la vez.\",\n );\n }\n\n const contenedor = useRef<HTMLDivElement | null>(null);\n const mapa = useRef<MapaDeCerca | null>(null);\n const [estado, setEstado] = useState<EstadoDeCarga>(\"cargando\");\n\n /**\n * Los callbacks más recientes, en una caja que no cambia de identidad.\n *\n * Sin esto habría que volver a suscribirse en cada render en el que la\n * superficie pase una función nueva —o sea, en cada render, porque casi\n * nadie memoriza sus manejadores—, y cada re-suscripción es un `off` y un\n * `on` sobre el motor. Con la caja, la suscripción se hace una vez y\n * siempre llama a la última versión.\n */\n const manejadores = useRef<ManejadoresDelMapa>({\n onListo,\n onNegocioClic,\n onNegocioEncima,\n onSedeClic,\n onGrupoClic,\n onRutaEleccion,\n onVehiculoClic,\n onMovimiento,\n onError,\n });\n manejadores.current = {\n onListo,\n onNegocioClic,\n onNegocioEncima,\n onSedeClic,\n onGrupoClic,\n onRutaEleccion,\n onVehiculoClic,\n onMovimiento,\n onError,\n };\n\n // Sin array de dependencias: se recalcula en cada render, que es lo que\n // hace que la referencia deje de ser `null` en cuanto el mapa existe. Con\n // `[estado]` el linter avisa de que la dependencia no se usa dentro, y\n // tiene razón: lo que cambia no es `estado`, es `mapa.current`, que es un\n // ref y por definición no dispara renders.\n // Los dos parámetros de tipo explícitos: sin ellos, React infiere el\n // primero del `Ref` y el segundo del valor devuelto, y se queja de que\n // `null` no encaja en `MapaDeCerca`. `null` es justo lo que hay antes de\n // que el motor cargue, así que el tipo correcto lo incluye.\n useImperativeHandle<MapaDeCerca | null, MapaDeCerca | null>(\n ref,\n () => mapa.current,\n );\n\n // — montaje —\n //\n // Sin dependencias: el mapa se construye una vez y vive hasta que el\n // componente se va. Cambiar el estilo, el tema o los datos son órdenes que\n // se le dan al mapa vivo, no un motivo para tirarlo y volver a levantarlo\n // —que costaría un contexto WebGL nuevo y una recarga entera de teselas—.\n useEffect(() => {\n let cancelado = false;\n\n void (async () => {\n try {\n const { crearMapa, hayWebGL2 } =\n await import(\"@cerca.red/mapa/navegador\");\n\n // Dos comprobaciones y no una: el componente puede haberse desmontado\n // mientras se descargaba el motor, y en React 19 en modo estricto eso\n // pasa siempre en desarrollo.\n if (cancelado) return;\n\n if (!hayWebGL2()) {\n setEstado(\"sin-webgl\");\n return;\n }\n\n const elemento = contenedor.current;\n if (elemento === null) return;\n\n const construido = crearMapa({\n contenedor: elemento,\n estilo,\n urlDeTeselas,\n tema,\n idioma,\n ...(centro === undefined ? {} : { centro }),\n ...(zoom === undefined ? {} : { zoom }),\n ...(margenes === undefined ? {} : { margenes }),\n ...(colores === undefined ? {} : { colores }),\n ...(cajaMaxima === undefined ? {} : { cajaMaxima }),\n ...(simbolos === undefined ? {} : { simbolos }),\n ...(urlDelWorker === undefined ? {} : { urlDelWorker }),\n ...(urlDeLicencias === undefined ? {} : { urlDeLicencias }),\n });\n\n if (cancelado) {\n // Se desmontó entre el `hayWebGL2` y aquí. Sin este destruir, el\n // contexto WebGL y el protocolo pmtiles se quedan colgados, y un\n // navegador aguanta muy pocos contextos a la vez.\n construido.destruir();\n return;\n }\n\n mapa.current = construido;\n atarEventos(construido, manejadores);\n setEstado(\"listo\");\n } catch (fallo: unknown) {\n if (cancelado) return;\n setEstado(\"fallo\");\n manejadores.current.onError?.({\n error: fallo instanceof Error ? fallo : new Error(String(fallo)),\n });\n }\n })();\n\n return () => {\n cancelado = true;\n mapa.current?.destruir();\n mapa.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // — datos —\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.ponerResultados(\n resultados ?? { modo: \"negocios\", negocios: negocios ?? [] },\n );\n }, [estado, negocios, resultados]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n // Sin `emitirEvento`: seleccionar desde una prop **no** debe emitir\n // `negocio:clic`. Si lo hiciera, tocar un resultado en la lista abriría la\n // ficha dos veces —una por el toque y otra por el eco del mapa— y la\n // pantalla entraría en un ciclo que no se ve como un ciclo.\n mapa.current?.seleccionarNegocio(\n resultados?.modo === \"grupos\" ? undefined : seleccionado,\n );\n }, [estado, seleccionado, negocios, resultados]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.ponerUbicacionDeUsuario(ubicacionDeUsuario);\n }, [estado, ubicacionDeUsuario]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.ponerVehiculos(vehiculos ?? []);\n }, [estado, vehiculos]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.mostrarRuta(rutas ?? []);\n }, [estado, rutas]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.mostrarZonaDeCobertura(zonas ?? []);\n }, [estado, zonas]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.mostrarIsocrona(isocronas ?? []);\n }, [estado, isocronas]);\n\n useEffect(() => {\n if (estado !== \"listo\" || simbolos === undefined) return;\n mapa.current?.registrarSimbolos(simbolos);\n }, [estado, simbolos]);\n\n // — apariencia —\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.ponerTema(tema);\n }, [estado, tema]);\n\n useEffect(() => {\n if (estado !== \"listo\") return;\n mapa.current?.ponerIdioma(idioma);\n }, [estado, idioma]);\n\n useEffect(() => {\n if (estado !== \"listo\" || margenes === undefined) return;\n mapa.current?.ponerMargenes(margenes);\n }, [estado, margenes]);\n\n const enContenedor = useCallback((elemento: HTMLDivElement | null) => {\n contenedor.current = elemento;\n }, []);\n\n if (estado === \"sin-webgl\" || estado === \"fallo\") return <>{respaldo}</>;\n\n return (\n <>\n <div\n ref={enContenedor}\n className={className}\n // El canvas no es accesible por sí solo: quien navegue con teclado o\n // con lector de pantalla necesita la lista, no esto. El rol y la\n // etiqueta son lo mínimo para que no se anuncie como un div vacío.\n role=\"application\"\n aria-label={etiqueta}\n style={{ width: \"100%\", height: \"100%\" }}\n />\n {estado === \"cargando\" ? respaldo : null}\n </>\n );\n}\n"]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `@cerca.red/mapa-react` — el mapa de Cerca dentro de React.
3
+ *
4
+ * El único paquete de este repositorio con JSX, y el único que puede
5
+ * renderizar. Lo que hace por dentro, y que por eso ninguna superficie tiene
6
+ * que repetir: cargar `maplibre-gl` de forma diferida y solo en el cliente, y
7
+ * exigir un respaldo para cuando no hay WebGL2.
8
+ */
9
+ export { Mapa, type MapaProps } from "./Mapa.tsx";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@cerca.red/mapa-react` — el mapa de Cerca dentro de React.
3
+ *
4
+ * El único paquete de este repositorio con JSX, y el único que puede
5
+ * renderizar. Lo que hace por dentro, y que por eso ninguna superficie tiene
6
+ * que repetir: cargar `maplibre-gl` de forma diferida y solo en el cliente, y
7
+ * exigir un respaldo para cuando no hay WebGL2.
8
+ */
9
+ export { Mapa } from "./Mapa.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAkB,MAAM,WAAY,CAAC","sourcesContent":["/**\n * `@cerca.red/mapa-react` — el mapa de Cerca dentro de React.\n *\n * El único paquete de este repositorio con JSX, y el único que puede\n * renderizar. Lo que hace por dentro, y que por eso ninguna superficie tiene\n * que repetir: cargar `maplibre-gl` de forma diferida y solo en el cliente, y\n * exigir un respaldo para cuando no hay WebGL2.\n */\nexport { Mapa, type MapaProps } from \"./Mapa.tsx\";\n"]}
@@ -0,0 +1,46 @@
1
+ import type { Desuscribir, EventosDelMapa, MapaDeCerca } from "@cerca.red/mapa";
2
+ /**
3
+ * Lo único de este paquete que no es React, sacado del componente para poder
4
+ * probarlo.
5
+ *
6
+ * `<Mapa>` es efectos y renderizado: montar, desmontar, reaccionar a props.
7
+ * Medir eso con vitest exigiría un DOM falso y una biblioteca de renderizado,
8
+ * y lo que se comprobaría sería sobre todo React. Esto de aquí sí tiene
9
+ * comportamiento propio —qué eventos se atan y a qué versión del callback— y
10
+ * se prueba con un objeto plano.
11
+ */
12
+ /** Los manejadores que la superficie pasó, en su versión más reciente. */
13
+ export interface ManejadoresDelMapa {
14
+ onListo?: (evento: EventosDelMapa["mapa:listo"]) => void;
15
+ onNegocioClic?: (evento: EventosDelMapa["negocio:clic"]) => void;
16
+ onNegocioEncima?: (evento: EventosDelMapa["negocio:encima"]) => void;
17
+ onSedeClic?: (evento: EventosDelMapa["sede:clic"]) => void;
18
+ onGrupoClic?: (evento: EventosDelMapa["grupo:clic"]) => void;
19
+ onRutaEleccion?: (evento: EventosDelMapa["ruta:eleccion"]) => void;
20
+ onVehiculoClic?: (evento: EventosDelMapa["vehiculo:clic"]) => void;
21
+ onMovimiento?: (evento: EventosDelMapa["mapa:movimiento"]) => void;
22
+ onError?: (evento: EventosDelMapa["mapa:error"]) => void;
23
+ }
24
+ /**
25
+ * Una caja que no cambia de identidad y siempre tiene lo último.
26
+ *
27
+ * Es la forma de un `useRef`, escrita como interfaz para que esta función no
28
+ * dependa de React y se pueda probar con `{ current: … }`.
29
+ */
30
+ export interface CajaDeManejadores {
31
+ readonly current: ManejadoresDelMapa;
32
+ }
33
+ /** Los eventos que este componente reenvía, para poder afirmarlo en una prueba. */
34
+ export declare const EVENTOS_ATADOS: readonly (keyof EventosDelMapa)[];
35
+ /**
36
+ * Ata cada evento del SDK a la prop que le corresponde.
37
+ *
38
+ * **Se suscribe una vez y lee el callback en el momento de llamarlo**, en vez
39
+ * de volver a suscribirse en cada render. La diferencia importa porque casi
40
+ * nadie memoriza sus manejadores: sin la caja, cada render de la superficie
41
+ * sería un `off` y un `on` sobre el motor por cada uno de los nueve eventos.
42
+ *
43
+ * Devuelve cómo soltar todo, aunque `destruir()` del SDK ya cancela sus
44
+ * propias suscripciones: el componente puede querer soltarlas antes.
45
+ */
46
+ export declare function atarEventos(mapa: MapaDeCerca, manejadores: CajaDeManejadores): Desuscribir;
@@ -0,0 +1,40 @@
1
+ /** Qué evento del SDK va a qué prop. Un solo sitio, para que no falte ninguno. */
2
+ const ATADURAS = [
3
+ ["mapa:listo", "onListo"],
4
+ ["negocio:clic", "onNegocioClic"],
5
+ ["negocio:encima", "onNegocioEncima"],
6
+ ["sede:clic", "onSedeClic"],
7
+ ["grupo:clic", "onGrupoClic"],
8
+ ["ruta:eleccion", "onRutaEleccion"],
9
+ ["vehiculo:clic", "onVehiculoClic"],
10
+ ["mapa:movimiento", "onMovimiento"],
11
+ ["mapa:error", "onError"],
12
+ ];
13
+ /** Los eventos que este componente reenvía, para poder afirmarlo en una prueba. */
14
+ export const EVENTOS_ATADOS = ATADURAS.map(([evento]) => evento);
15
+ /**
16
+ * Ata cada evento del SDK a la prop que le corresponde.
17
+ *
18
+ * **Se suscribe una vez y lee el callback en el momento de llamarlo**, en vez
19
+ * de volver a suscribirse en cada render. La diferencia importa porque casi
20
+ * nadie memoriza sus manejadores: sin la caja, cada render de la superficie
21
+ * sería un `off` y un `on` sobre el motor por cada uno de los nueve eventos.
22
+ *
23
+ * Devuelve cómo soltar todo, aunque `destruir()` del SDK ya cancela sus
24
+ * propias suscripciones: el componente puede querer soltarlas antes.
25
+ */
26
+ export function atarEventos(mapa, manejadores) {
27
+ const cancelar = ATADURAS.map(([evento, prop]) => mapa.cuando(evento, (dato) => {
28
+ // La lectura va **dentro**: `manejadores.current[prop]` en el momento
29
+ // de suscribir congelaría la primera versión del callback, y la
30
+ // superficie que cambia de manejador entre renders dejaría de recibir
31
+ // eventos sin que nada fallara.
32
+ const manejador = manejadores.current[prop];
33
+ manejador?.(dato);
34
+ }));
35
+ return () => {
36
+ for (const soltar of cancelar)
37
+ soltar();
38
+ };
39
+ }
40
+ //# sourceMappingURL=suscripciones.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suscripciones.js","sourceRoot":"","sources":["../src/suscripciones.ts"],"names":[],"mappings":"AAoCA,kFAAkF;AAClF,MAAM,QAAQ,GAAG;IACf,CAAC,YAAY,EAAE,SAAS,CAAC;IACzB,CAAC,cAAc,EAAE,eAAe,CAAC;IACjC,CAAC,gBAAgB,EAAE,iBAAiB,CAAC;IACrC,CAAC,WAAW,EAAE,YAAY,CAAC;IAC3B,CAAC,YAAY,EAAE,aAAa,CAAC;IAC7B,CAAC,eAAe,EAAE,gBAAgB,CAAC;IACnC,CAAC,eAAe,EAAE,gBAAgB,CAAC;IACnC,CAAC,iBAAiB,EAAE,cAAc,CAAC;IACnC,CAAC,YAAY,EAAE,SAAS,CAAC;CAG1B,CAAC;AAEF,mFAAmF;AACnF,MAAM,CAAC,MAAM,cAAc,GAAsC,QAAQ,CAAC,GAAG,CAC3E,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,CACrB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CACzB,IAAiB,EACjB,WAA8B;IAE9B,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAC/C,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QAC3B,sEAAsE;QACtE,gEAAgE;QAChE,sEAAsE;QACtE,gCAAgC;QAChC,MAAM,SAAS,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3C,SAAqD,EAAE,CAAC,IAAI,CAAC,CAAC;IACjE,CAAC,CAAC,CACH,CAAC;IAEF,OAAO,GAAG,EAAE;QACV,KAAK,MAAM,MAAM,IAAI,QAAQ;YAAE,MAAM,EAAE,CAAC;IAC1C,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import type { Desuscribir, EventosDelMapa, MapaDeCerca } from \"@cerca.red/mapa\";\n\n/**\n * Lo único de este paquete que no es React, sacado del componente para poder\n * probarlo.\n *\n * `<Mapa>` es efectos y renderizado: montar, desmontar, reaccionar a props.\n * Medir eso con vitest exigiría un DOM falso y una biblioteca de renderizado,\n * y lo que se comprobaría sería sobre todo React. Esto de aquí sí tiene\n * comportamiento propio —qué eventos se atan y a qué versión del callback— y\n * se prueba con un objeto plano.\n */\n\n/** Los manejadores que la superficie pasó, en su versión más reciente. */\nexport interface ManejadoresDelMapa {\n onListo?: (evento: EventosDelMapa[\"mapa:listo\"]) => void;\n onNegocioClic?: (evento: EventosDelMapa[\"negocio:clic\"]) => void;\n onNegocioEncima?: (evento: EventosDelMapa[\"negocio:encima\"]) => void;\n onSedeClic?: (evento: EventosDelMapa[\"sede:clic\"]) => void;\n onGrupoClic?: (evento: EventosDelMapa[\"grupo:clic\"]) => void;\n onRutaEleccion?: (evento: EventosDelMapa[\"ruta:eleccion\"]) => void;\n onVehiculoClic?: (evento: EventosDelMapa[\"vehiculo:clic\"]) => void;\n onMovimiento?: (evento: EventosDelMapa[\"mapa:movimiento\"]) => void;\n onError?: (evento: EventosDelMapa[\"mapa:error\"]) => void;\n}\n\n/**\n * Una caja que no cambia de identidad y siempre tiene lo último.\n *\n * Es la forma de un `useRef`, escrita como interfaz para que esta función no\n * dependa de React y se pueda probar con `{ current: … }`.\n */\nexport interface CajaDeManejadores {\n readonly current: ManejadoresDelMapa;\n}\n\n/** Qué evento del SDK va a qué prop. Un solo sitio, para que no falte ninguno. */\nconst ATADURAS = [\n [\"mapa:listo\", \"onListo\"],\n [\"negocio:clic\", \"onNegocioClic\"],\n [\"negocio:encima\", \"onNegocioEncima\"],\n [\"sede:clic\", \"onSedeClic\"],\n [\"grupo:clic\", \"onGrupoClic\"],\n [\"ruta:eleccion\", \"onRutaEleccion\"],\n [\"vehiculo:clic\", \"onVehiculoClic\"],\n [\"mapa:movimiento\", \"onMovimiento\"],\n [\"mapa:error\", \"onError\"],\n] as const satisfies ReadonlyArray<\n readonly [keyof EventosDelMapa, keyof ManejadoresDelMapa]\n>;\n\n/** Los eventos que este componente reenvía, para poder afirmarlo en una prueba. */\nexport const EVENTOS_ATADOS: readonly (keyof EventosDelMapa)[] = ATADURAS.map(\n ([evento]) => evento,\n);\n\n/**\n * Ata cada evento del SDK a la prop que le corresponde.\n *\n * **Se suscribe una vez y lee el callback en el momento de llamarlo**, en vez\n * de volver a suscribirse en cada render. La diferencia importa porque casi\n * nadie memoriza sus manejadores: sin la caja, cada render de la superficie\n * sería un `off` y un `on` sobre el motor por cada uno de los nueve eventos.\n *\n * Devuelve cómo soltar todo, aunque `destruir()` del SDK ya cancela sus\n * propias suscripciones: el componente puede querer soltarlas antes.\n */\nexport function atarEventos(\n mapa: MapaDeCerca,\n manejadores: CajaDeManejadores,\n): Desuscribir {\n const cancelar = ATADURAS.map(([evento, prop]) =>\n mapa.cuando(evento, (dato) => {\n // La lectura va **dentro**: `manejadores.current[prop]` en el momento\n // de suscribir congelaría la primera versión del callback, y la\n // superficie que cambia de manejador entre renders dejaría de recibir\n // eventos sin que nada fallara.\n const manejador = manejadores.current[prop];\n (manejador as ((evento: unknown) => void) | undefined)?.(dato);\n }),\n );\n\n return () => {\n for (const soltar of cancelar) soltar();\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@cerca.red/mapa-react",
3
+ "version": "0.2.0",
4
+ "description": "La envoltura React del mapa de Cerca. Carga el motor de forma diferida y solo en el cliente, y exige un respaldo para cuando no hay WebGL2.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "dependencies": {
21
+ "@cerca.red/geo": "0.2.0",
22
+ "@cerca.red/mapa": "0.2.0"
23
+ },
24
+ "peerDependencies": {
25
+ "maplibre-gl": "^6.8.0",
26
+ "pmtiles": "^4.5.0",
27
+ "react": ">=18"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^19.2.0",
31
+ "maplibre-gl": "6.8.0",
32
+ "pmtiles": "4.5.0",
33
+ "react": "^19.2.0"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/Exus-Agencia-Web/cerca-maps.git",
38
+ "directory": "packages/mapa-react"
39
+ },
40
+ "scripts": {
41
+ "construir": "tsc -p tsconfig.build.json"
42
+ }
43
+ }