The pin click handler was registered on two layers that always overlap: the
visible pins and the invisible hitbox drawn over them. A layer-scoped click
fires once per registered layer with a feature under the point, and every
visible pin has a hitbox twin at its own point -- same source, larger radius,
added alongside it so taps are easier to land. So every pin click ran the
handler twice, 3ms apart. Measured on the leaf at 1440, on two different pins,
both reporting both layers at the click point; exactly twice, never more.
That already shipped as a visible bug once: the second call overwrote the
legend state the first had saved, so closing a people-group detail reopened a
legend the reader had deliberately collapsed. The store is now idempotent, so
that particular symptom cannot return, but the duplicate call was the cause and
is still here -- anything that counts, toggles, appends or reports from a pin
click is wrong by a factor of two, and nothing in the code warns the next
author.
Bind the click to the hitbox only. It covers every click the visible layer
would catch: both layers are created together by addLanguageFamilyLayer, from
the same source, and the two places that filter the hitbox mirror the pin
filter rather than narrowing it, so the two layers always carry the same
features. The handler reads only properties and geometry, which are identical
between them. Cursor handlers stay on both layers -- mouseenter and mouseleave
are idempotent, so they were never part of this.
Checked before relying on it: exactly one addLayer creates each of the two
layers, both inside addLanguageFamilyLayer as consecutive statements with no
branch between them; no flag gates the hitbox; no removeLayer anywhere touches
either; and the only layout writes on the pin layer set circle-sort-key. The
invariant is now stated at the binding, because it is not visible from the call
site: a pins layer added without a hitbox would get no clicks.
Also fixes a dead trap in the same file. The people-groups-pins branch called
the handler with no argument, so it fell back to the language-family pins and
bound the wrong layer. Nothing creates that layer today -- the id occurs only
in this file -- so the branch never runs, but whoever first adds it would have
got a duplicate binding, or no click handler at all if the language-family
layer were absent.
The library's pin and region popups (useMapEvents) interpolated data fields raw into the HTML handed to
setHTML(); a name like <img src=x onerror=…> from an API or a partner's list would run in the host page
(CRITIC-3). library/utils/html.js adds escapeHtml and safeUrl (http(s) only); every field of both popups
goes through them. check:popup-escaping (headless, gates:post) mounts the real composable, renders both
popups from hostile data and requires no markup, handler or javascript: URL to survive: it FAILs on the
previous code (both popups) and PASSes here. check:cdn-pins and check:no-eval join gates:post too.
Every generated leaf <head> preloaded mapbox-gl.js whatever the provider, so code that used the
bare `mapboxgl` global always found one, even on a MapLibre map. Removing that peer (it sends
every visitor's IP to Mapbox under provider=maplibre) turns each such site into an error.
engineFor(map), next to isMapboxMap in useMapProvider.js, asks the map which engine built it:
- useSelectedPin: the selected-pin Marker
- useMapEvents: the pin-fallback and region Popups
- useMapPoster: main already builds the hidden map with the source map's engine (1cbfb3aa);
its choice now goes through this one helper instead of a second inline sniff
- pplr-data-maps: glFor is now this helper; the geocoder awaits ensureEngineScript first
- key-cities: ensureEngineScript('mapbox') before reading the global
Measured with every api.mapbox.com tag stripped from the served leaf (no peer at all for
key-cities), before -> after:
pin select under MapLibre, on cities-internet, country-posters, doxa-research-map and
doxa-simple-map: "reading 'Marker'" -> marker drawn; country-posters region popup: none ->
popup; PPLR search: no input -> suggestions (both engines); key-cities: "mapbox-gl not
loaded" -> map loaded. Mapbox rows unchanged (they passed before and after).
The poster UI could not be driven: PosterDialog never opens in country-posters or
doxa-research-map (they pass props it does not declare and no modelValue/getMap), reported
separately. useMapPoster itself was rendered from a real map with only its engine on the page:
MapLibre "mapboxgl global not found" on the pre-1cbfb3aa library -> a 400x300 PNG with 88 colours.
Standalone repo that builds embeddable JAMstack map bundles (web components),
composes them into a lazy-loaded SPA via layouts/, and optionally wraps a
Capacitor Android/iOS app — one build, choose your output/CDN. Diátaxis docs.
History reset to a single clean commit; no PII, IPs, or internal terms.