From 4d2b8509abdfa36b7aef22643e18aedeec4e411c Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sun, 27 Sep 2026 04:32:26 +0000 Subject: [PATCH] Ensure the engine stylesheet the library's way, and localize the selection cover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TWO ITEMS, both found by checking claims rather than accepting them. ★ 1. THE MAPBOX "missing CSS declarations" WARNING (rega END-REGA-SRV / dx-shepherd END-DXS-U100C). The routed hypothesis was "the leaf doesn't load mapbox-gl.css". It does not — and neither does cities-internet's leaf, which does NOT warn, so that was not the difference. The real difference, measured: connectivity-map.vue:398-399 calls `engineStylesheetUrl(provider)` then `await ensureEngineCSS(...)`. This profile called NEITHER; it hand-rolled its own injection, which failed two ways: (a) it ran only when the map element was inside a ShadowRoot, so on a standalone BUILT leaf — light DOM — no stylesheet was ensured at all; and (b) even in the shadow case it fired and forgot, so `new gl.Map` could run before the sheet parsed. Mapbox checks its declarations AT CONSTRUCTION, which is exactly when the warning appears. ensureEngineCSS covers both DOM cases and AWAITS load/error with a 2s cap. It also takes the PROVIDER's own URL, where the old line asked for mapbox's stylesheet even under MapLibre behind an !isMaplibre guard — so a maplibre page now gets maplibre's sheet or none, and never touches api.mapbox.com, which is the keyless contract the maplibre-embed gate enforces. The shadow injection stays as a belt for a partner embedding the custom element with no wrapper ; both paths check by href, so neither duplicates the other. ★ 2. THE SELECTION PACKET'S COVER WAS HARDCODED ENGLISH. Found while checking whether batch selection was already done (END-DXS-U100B — it is, and render-verified). selectionAsList built its label with `names.join(' + ')` and `` `+${n} more` ``, printed onto page 1 and into the print dialog title. So a reader choosing three territories under ?lang=ar got their names in Arabic joined by an English tail. zone_join / zone_join_more already exist in all eleven packet-strings blocks — translated once for exactly this shape when the zone-title generator landed — so this is reuse, not new strings. The block is resolved with per-KEY fallback to English, as packet.mjs and packet-render already do it, so a half-translated locale cannot blank a key that en has. check:sfc-parse PASS, check:no-free-identifiers PASS. ⚠ THE 0×0 CONTAINER IS NOT MINE AND NOT FIXED HERE — diagnosed and routed to studio. It is doxa-maps-spa's HEAVY_ELEMENTS set: the gallery computes `heavy: HEAVY_ELEMENTS.has(tag) || HEAVY_ELEMENTS.has(id)`, the set contains `my-upg-100-list-map` (the inner MAP element) and the gallery mounts the COMPOSITE, so neither the mfe tag nor the bundle id `my-upg-100-list` matches. A non-heavy tab mounts under `v-show` + `:hidden`, i.e. display:none, and a map built into a display:none container is 0×0 by construction. The gallery's own comment says heavy means "anything that owns OR NESTS a map" — the composite nests one, so it qualifies and is simply missing from the list. regions-dashboard/regions-mfe has the same shape and the same gap. NOT caused by my inert change, despite the timing pointing there (absent at a1c8f3f6, present at 5d83febd): `inert` sets no layout at all and nothing in the tree keys CSS on [inert], so it cannot collapse a box. The window contained seven candidate commits and the timing alone could not distinguish them. --- .../my-upg-100-list/upg-100-list.vue | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/plugin-designs/my-upg-100-list/upg-100-list.vue b/plugin-designs/my-upg-100-list/upg-100-list.vue index d0fec33ab..613b75b93 100644 --- a/plugin-designs/my-upg-100-list/upg-100-list.vue +++ b/plugin-designs/my-upg-100-list/upg-100-list.vue @@ -77,7 +77,7 @@ import ThemeToggleButton from '@map/components/map-controls/ThemeToggleButton.vu // 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, engineStylesheetUrl } from '@map/composables/useMapInstance.js' +import { ensureEngineScript, engineGlobal, ensureEngineCSS, engineStylesheetUrl } from '@map/composables/useMapInstance.js' // Shapes Arabic / Persian / Hebrew BASEMAP labels. A raw map inherits nothing from the bundle // entry, so this profile has to install it itself — see the call site below for why the entry's // registration only ever worked by accident. @@ -1567,6 +1567,22 @@ function selectedLists() { // HERE — once, against one fetch of the people-groups list — and handed on as the type every // consumer already handles. Resolving per zone would refetch the whole catalogue per zone and // still leave the caller to merge. +// The packet's own string block for the reader's locale, per-KEY fallback to English exactly as +// packet.mjs and packet-render do it — a half-translated block must not blank a key that en has. +let _packetStrings = null; +async function packetStrings() { + if (!_packetStrings) { + const all = await import('@map/scripts/posters/packet-strings.json').then((m) => m.default || m); + _packetStrings = { ...(all.en || {}), ...(all[packetLang()] || {}) }; + } + return _packetStrings; +} +/** `{a}` / `{b}` / `{n}` interpolation; null when the pattern is missing so the caller can default. */ +function fillJoin(pattern, params) { + if (!pattern) return null; + return String(pattern).replace(/\{(\w+)\}/g, (m, k) => (params[k] != null ? String(params[k]) : m)); +} + async function selectionAsList() { const lists = selectedLists(); if (!lists.length) throw new Error('nothing selected'); @@ -1587,10 +1603,20 @@ async function selectionAsList() { } // The label is what the reader sees on the cover and in the print dialog's title. Name the // territories they actually chose rather than inventing a title for their selection. + // ⚠ THE JOINERS WERE HARDCODED ENGLISH on a cover an Arabic reader sees: ' + ' and ' +N more', printed + // straight onto the packet's first page and into the print dialog's title. Found while checking whether + // batch selection was already done (dx-shepherd END-DXS-U100B) — the SELECTION path was, but its label + // was not localized, so a reader choosing three territories in Arabic got their names in Arabic joined + // by an English tail. + // zone_join / zone_join_more already exist in all eleven packet-strings blocks (landed with the zone + // title generator), translated once for exactly this shape, so this is a reuse rather than new strings. const names = lists.map((l) => l.label || l.id); + const S = await packetStrings(); + const join2 = (a, b) => fillJoin(S.zone_join, { a, b }) ?? `${a} + ${b}`; const label = names.length <= 3 - ? names.join(' + ') - : `${names.slice(0, 2).join(' + ')} +${names.length - 2} more`; + ? names.reduce(join2) + : fillJoin(S.zone_join_more, { a: names.slice(0, 2).reduce(join2), n: names.length - 2 }) + ?? `${names.slice(0, 2).join(' + ')} +${names.length - 2} more`; return { id: 'selection', label, count: slugs.size, selector: { type: 'pg_slugs', values: [...slugs] } }; } @@ -2042,6 +2068,29 @@ onMounted(async () => { // mapbox-under-maplibre reference in this profile, carried as a named exemption in the // maplibre-embed gate; guarding it lets that exemption be deleted and enforced instead. // MapLibre ships its own ctrl CSS via ensureEngineScript, so nothing is lost. + // ★ THE LIBRARY'S ensureEngineCSS, NOT A HAND-ROLLED COPY (rega END-REGA-SRV / END-DXS-U100C, + // 2026-09-27). Mapbox logged "missing CSS declarations" on the BUILT leaf and in the gallery, and the + // cause was this block, not a missing : + // (a) it only ran when the map element was inside a ShadowRoot — `root instanceof ShadowRoot` — so on + // a standalone leaf, where the element is in LIGHT DOM, no stylesheet was ensured at all; and + // (b) even in the shadow case it FIRED AND FORGOT, so `new gl.Map` could run before the sheet parsed. + // Mapbox checks its declarations AT CONSTRUCTION, which is exactly when the warning appears. + // ensureEngineCSS handles both DOM cases (light DOM returns early because the wrapper covers it) + // and AWAITS the load/error event with a 2s cap, so construction happens after the sheet is live. + // ⚠ AND IT TAKES THE PROVIDER'S OWN URL rather than 'mapbox' hardcoded — the old line asked the registry + // for mapbox's stylesheet even under MapLibre, guarded by !isMaplibre. Passing upgProvider means the + // guard is unnecessary: a maplibre page gets maplibre's sheet (or none) and never touches + // api.mapbox.com, which is the keyless contract the maplibre-embed gate enforces. + // MEASURED as the difference from cities-internet, which does not warn: connectivity-map.vue:398-399 + // calls exactly this pair and this profile called neither. + try { + const css = engineStylesheetUrl(upgProvider) + if (css) await ensureEngineCSS(mapEl.value, css) + } catch (_) { /* a stylesheet must never stop the map painting */ } + // The legacy shadow-root injection stays ONLY as the belt to that braces: ensureEngineCSS returns early + // for light DOM on the assumption a wrapper exists, and a partner embedding the custom element + // directly may have no wrapper at all. Harmless when ensureEngineCSS already added the same href — both + // paths check for it by href before inserting. try { const shadow = !isMaplibre && mapEl.value && mapEl.value.getRootNode() if (shadow instanceof ShadowRoot) {