my-upg-100-list joins the provider switch, and drops its search bar

The last plugin on the rollout list, and the only one the switch could not
reach: it builds its map RAW (new gl.Map on window.mapboxgl) instead of
through useMapInstance, so resolving the provider inside the factory --
which fixed every other bundle for free -- did nothing here. MEASURED at
?provider=maplibre before this: still Mapbox, 19 requests to api.mapbox.com,
while five sibling leaves swapped cleanly.

Three things blocked the keyless engine, not one:

  - the engine was hardcoded to window.mapboxgl. It now loads whatever the
    provider selects, through ensureEngineScript/engineGlobal EXPORTED from
    useMapInstance. Imported, not copied: a second ENGINES table living in a
    profile is how two copies of one fact drift apart.
  - a `if (!token) return` early exit. A token is a MAPBOX requirement, not
    a map requirement -- left in place it would have refused to draw the
    engine that needs no token, turning the keyless option into a blank
    cell. Now mapbox-only.
  - a hardcoded mapbox://styles/mapbox/light-v11. Now paired through
    resolveNamedStyle('paper'), the registry entry that already pairs
    light-v11 with OpenFreeMap positron, so both engines show one intent.

And the search bar follows the standing decision -- deferred to Mapbox,
because GeocoderComponent's maplibre path still geocodes through Mapbox, so
a keyless map with a search bar is not keyless. MapToolbar collapses its
reserved gap and the legend rises into the vacated space.

MEASURED on the built tree, both engines:
  mapbox    search@12   legend@58   19 mapbox requests   (all unchanged)
  maplibre  search@NONE legend@8     5 mapbox requests   maplibregl loaded
The remaining 5 are the leaf's own static preloads -- already filed, and
not something this file can reach.

Screenshot read, not just counted: positron basemap, region polygons, all
2107 pins, legend flush at the top, no gap and no collision.

22/22 bundles, 7/7 pre, 18/18 post, browser sweep of five StackLayout
consumers and all seven studio dashboards.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 03:04:11 +00:00
parent c36d431105
commit 4350113041
8 changed files with 89 additions and 52 deletions

View file

@ -77,8 +77,14 @@ const OPENFREEMAP_DARK_STYLE = 'https://tiles.openfreemap.org/styles/fiord'
import { resolveNamedStyle, applyStylePaint } from '../styles/registry.js'
import { resolveMapProvider } from './useMapProvider.js'
/** The live engine global for a provider, or undefined if not yet loaded. */
function engineGlobal(provider) {
/**
* The live engine global for a provider, or undefined if not yet loaded.
* EXPORTED for the profiles that build their map RAW (new gl.Map) instead of through this
* composable — my-upg-100-list is one. They still need the RIGHT engine for the selected provider,
* and the alternative is a second copy of the ENGINES table living in a profile, which is how two
* copies of one fact drift apart. Borrow the loader; do not re-declare the registry.
*/
export function engineGlobal(provider) {
const name = ENGINES[provider]?.global || ENGINES.mapbox.global
return typeof window !== 'undefined' ? window[name] : undefined
}
@ -88,7 +94,7 @@ function engineGlobal(provider) {
* Idempotent — resolves immediately if the global already exists (preloaded via
* index.html), otherwise injects the script and awaits its load event.
*/
async function ensureEngineScript(provider) {
export async function ensureEngineScript(provider) {
const cfg = ENGINES[provider] || ENGINES.mapbox
if (engineGlobal(provider)) return // already loaded — no-op
if (typeof document === 'undefined') return

View file

@ -70,6 +70,9 @@ import ThemeToggleButton from '@map/components/map-controls/ThemeToggleButton.vu
// `__upgKind` to route PG→detail / zone→fitBounds / territory→fitBounds. Never
// edits src/ (zero blast radius — md5 unchanged). (research-map.vue:141,1815.)
import GeocoderComponent from '@map/components/map-controls/GeocoderComponent.vue'
import { resolveMapProvider } from '@map/composables/useMapProvider.js'
import { ensureEngineScript, engineGlobal } from '@map/composables/useMapInstance.js'
import { resolveNamedStyle } from '@map/styles/registry.js'
// Named imports: only the list arrays ship in the bundle; the file's bookkeeping fields stay out.
import { lists as clusterLists, asia_lists as clusterAsiaLists, uncovered_lists as clusterUncoveredLists } from './data/clusters.json'
const clusters = { lists: clusterLists, asia_lists: clusterAsiaLists, uncovered_lists: clusterUncoveredLists }
@ -100,6 +103,23 @@ import { buildSearchIndex, buildGeocoderFeed, matchSearch } from './data/search-
const mapboxToken = inject('mapboxToken', ref(''))
const profileConfig = inject('profileConfig', ref(null))
// ── THIS PROFILE BUILDS ITS MAP RAW, SO IT HAS TO HONOUR THE SWITCH ITSELF ───
// Every other map bundle reaches the provider switch for free because useMapInstance resolves it.
// This one calls `new gl.Map` directly (see onMounted), so it was the ONE map the switch could not
// move: MEASURED at ?provider=maplibre it still loaded Mapbox and made 19 requests to
// api.mapbox.com while five sibling leaves swapped cleanly.
// The engine loader is IMPORTED from useMapInstance rather than re-declared here — a second copy of
// the ENGINES table in a profile is how two copies of one fact drift apart.
const upgProvider = resolveMapProvider(profileConfig)
const isMaplibre = upgProvider === 'maplibre'
// The geocoder is deferred to Mapbox by decision (Driver 2026-09-24): GeocoderComponent's maplibre
// path still calls makeMapboxForwardGeocode(accessToken), so the SEARCH REQUEST goes to Mapbox
// whichever engine draws the tiles — a keyless map with a search bar is not keyless.
const hasGeocoder = computed(() => {
if (profileConfig?.value?.search === false) return false
return upgProvider === 'mapbox'
})
// EDGE SCROLL AFFORDANCE — the gray accessibility side bars that tell you the page
// scrolls PAST the map. Feature-flagged per embed, because it is a promise: a map
// with nothing beneath it must not advertise a scroll escape that goes nowhere.
@ -1278,24 +1298,31 @@ onMounted(async () => {
// hidden) to explain it. Sibling poster-wall.vue survives the same early return
// precisely because its content does not depend on state onMounted populates.
bootstrapLegend()
const gl = window.mapboxgl
// Load the engine the PROVIDER selects, not the one this file used to assume.
try { await ensureEngineScript(upgProvider) } catch (_) { /* fall through to the !gl notice below */ }
const gl = engineGlobal(upgProvider) || window.mapboxgl
const token = mapboxToken?.value || profileConfig?.value?.tk || ''
// Degraded exits: keep the terse telemetry in `status`, put the human-readable
// notice in the MAP CELL, and still enrich the legend with live population data
// (a plain fetch — it needs no map). The list stays fully usable without tiles.
if (!gl) {
status.value = '⚠ mapbox-gl not loaded'
mapError.value = 'mapbox-gl did not load. The list and legend are unaffected.'
status.value = '⚠ map engine not loaded'
mapError.value = 'The map engine did not load. The list and legend are unaffected.'
enrichLegendFromRecords()
return
}
if (!token) {
status.value = '⚠ no mapbox token'
mapError.value = 'No Mapbox token configured. The list and legend are unaffected.'
enrichLegendFromRecords()
return
// ★ A TOKEN IS A MAPBOX REQUIREMENT, NOT A MAP REQUIREMENT. This gate returned early whenever
// there was no token, which under maplibre would have refused to draw a map that needs no
// token at all — turning the keyless option into a blank cell. MapLibre skips it entirely.
if (!isMaplibre) {
if (!token) {
status.value = '⚠ no mapbox token'
mapError.value = 'No Mapbox token configured. The list and legend are unaffected.'
enrichLegendFromRecords()
return
}
gl.accessToken = token
}
gl.accessToken = token
// P0 fix (LESSON-03 L4): mapbox-gl.css must live INSIDE the shadow root —
// the host page's <link> cannot penetrate it. Without it the geocoder's
// .mapboxgl-ctrl-top-left container is position:static and the search pill
@ -1316,7 +1343,11 @@ onMounted(async () => {
}
} catch (_) { /* CSS injection is best-effort; map still works without it */ }
map = new gl.Map({
container: mapEl.value, style: 'mapbox://styles/mapbox/light-v11',
container: mapEl.value,
// 'paper' is the registry's near-white minimal entry — mapbox light-v11 paired with
// OpenFreeMap positron — so the two engines show the same intent instead of this file
// hardcoding one engine's URL. Falls back to the literal if the name ever stops resolving.
style: resolveNamedStyle('paper', upgProvider).style || 'mapbox://styles/mapbox/light-v11',
center: [35, 20], zoom: 1.5, projection: 'mercator',
})
mapRef.value = map // expose the reactive instance to the controls
@ -1755,7 +1786,7 @@ onBeforeUnmount(() => {
`.value`; template scope must not. -->
<GeocoderComponent
v-if="mapRef"
v-if="mapRef && hasGeocoder"
ref="geocoderRef"
:map-instance="mapRef"
:access-token="mapboxToken || profileConfig?.tk || ''"
@ -1770,7 +1801,7 @@ onBeforeUnmount(() => {
<!-- Shared control rail — same set + ordering as research-map. MapToolbar
self-positions top-right; the status badge is bottom-right (no collision).
Gated on mapRef so the buttons only mount once the map instance exists. -->
<MapToolbar v-if="mapRef">
<MapToolbar v-if="mapRef" :has-geocoder="hasGeocoder">
<ZoomInButton :map="mapRef" :is-dark="isDark" />
<ZoomOutButton :map="mapRef" :is-dark="isDark" />
<LocationButton :map="mapRef" :is-dark="isDark" />

File diff suppressed because one or more lines are too long

View file

@ -640,7 +640,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-wsI/kC7ernWrD4UZydHEUyyjK65h9NRs50CbZAWYAFMziSSuCdeIsTleezVVbsyV",
"integrity": "sha384-JhtuB6o4/AnwqNK5oJ7/pf95ao5S3mr66nLgJ1tpB0n1IJDFiyxHkUK+WXUm62Z4",
"elements": [
"my-upg-100-list-map"
],
@ -660,7 +660,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-wsI/kC7ernWrD4UZydHEUyyjK65h9NRs50CbZAWYAFMziSSuCdeIsTleezVVbsyV\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-JhtuB6o4/AnwqNK5oJ7/pf95ao5S3mr66nLgJ1tpB0n1IJDFiyxHkUK+WXUm62Z4\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -674,7 +674,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-wsI/kC7ernWrD4UZydHEUyyjK65h9NRs50CbZAWYAFMziSSuCdeIsTleezVVbsyV\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-JhtuB6o4/AnwqNK5oJ7/pf95ao5S3mr66nLgJ1tpB0n1IJDFiyxHkUK+WXUm62Z4\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -688,7 +688,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-wsI/kC7ernWrD4UZydHEUyyjK65h9NRs50CbZAWYAFMziSSuCdeIsTleezVVbsyV\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-JhtuB6o4/AnwqNK5oJ7/pf95ao5S3mr66nLgJ1tpB0n1IJDFiyxHkUK+WXUm62Z4\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
}
]

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-wsI/kC7ernWrD4UZydHEUyyjK65h9NRs50CbZAWYAFMziSSuCdeIsTleezVVbsyV" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-JhtuB6o4/AnwqNK5oJ7/pf95ao5S3mr66nLgJ1tpB0n1IJDFiyxHkUK+WXUm62Z4" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-wsI/kC7ernWrD4UZydHEUyyjK65h9NRs50CbZAWYAFMziSSuCdeIsTleezVVbsyV" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-JhtuB6o4/AnwqNK5oJ7/pf95ao5S3mr66nLgJ1tpB0n1IJDFiyxHkUK+WXUm62Z4" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-wsI/kC7ernWrD4UZydHEUyyjK65h9NRs50CbZAWYAFMziSSuCdeIsTleezVVbsyV" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-JhtuB6o4/AnwqNK5oJ7/pf95ao5S3mr66nLgJ1tpB0n1IJDFiyxHkUK+WXUm62Z4" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: