Compare commits

...

9 commits

Author SHA1 Message Date
Doxa Maps
bad61918ff Rebuild poster-sections for the type-script loading; tree and SRI regenerated 2026-10-02 16:50:31 +00:00
Doxa Maps
e96b6fb4de Poster Sections: a preset of another plugin's poster type loads that plugin's bundle
A poster type registered by another plugin's bundle (upg100-list, by
my-upg-100-list.js) could only say No posters on a Poster Sections preset
page, which loads poster-sections.js alone. src/typeScripts.js names such
types and their bundles; the page adds the bundle as a second <script>, once
per page, when the type is not registered yet (the registry is one per page),
and re-resolves the type when its registration is announced, so all regions
lists the type's regions as they arrive. Copy embed carries the same second
script. The Configure picker offers these types before their script runs,
named from the catalogue (posterSections.typeUpg100List, 11 locales).

check:poster-sections: every preset's type is the library's own or listed in
src/typeScripts.js with its bundle in the built tree; its self-test proves a
preset whose type no script registers fails.
2026-10-02 16:11:50 +00:00
Doxa Maps
7138bf61cf Rebuild my-upg-100-list for the populated scopes; the tree gains the 9 upg100 Poster Sections preset pages; SRI 2026-10-02 15:57:05 +00:00
Doxa Maps
09cf034214 UPG-100 poster type: scopes are the regions that have zones; the Poster Sections presets regenerated
scopes() now returns only regions some UPG-100 zone falls into, so no section or
preset can exist only to say "No posters". Today that drops 'Other', the same
correction the country type had ("countries-other gone"). With the type file on
the tree, check:poster-sections requires its presets, so
poster-sections.instances.json is regenerated by its own generator
(gen:poster-sections-presets): upg100-all plus one per region, 9 presets, and no
hand edit.
2026-10-02 15:52:46 +00:00
Doxa Maps
519e43d58b Rebuild my-upg-100-list on the merged pilot (the page-global poster-type registry), and regenerate the tree and SRI 2026-10-02 15:50:28 +00:00
Doxa Maps
9793f8bd21 Merge remote-tracking branch 'origin/development' into upg100/poster-sections
# Conflicts:
#	package.json
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/manifest.json
#	public/dx-maps/my-upg-100-list/my-upg-100-list.js
#	public/dx-maps/my-upg-100-list/poster-wall/index.html
#	public/dx-maps/my-upg-100-list/upg-100-list/index.html
#	public/dx-maps/my-upg-100-list/upg-100-mfe/index.html
2026-10-02 15:49:55 +00:00
Doxa Maps
32d669deea check:label-key-coverage follows the region table to data/upg100-regions.js, and reads the labels regionOf returns as literals (a removed 'China + Mongolia' key FAILS it) 2026-10-02 08:05:44 +00:00
Doxa Maps
1faf1c9d49 Rebuild my-upg-100-list for the collapsible region sections and the upg100-list poster type, and regenerate the tree and SRI 2026-10-02 08:03:29 +00:00
Doxa Maps
07b28e405e UPG-100: region sections are the shared poster section, collapsible, and an 'upg100-list' poster type
UPG-100's poster wall groups its zones into regions (India, China + Mongolia,
Africa, Middle East, Central Asia, Southeast Asia + Oceania, Americas, Europe).
Those sections are now collapsible like Country Posters' and start open. They
are also available on their own as <dxm-poster-section type="upg100-list"
scope="africa">.

- data/upg100-regions.js (pure): the region table (REGION_OF, moved verbatim
  from poster-wall.vue), regionOf(id) and each region's scope slug.
- data/upg100-poster-type.js: the poster-type adapter (scopes, items, title,
  actions), built on the wall's own zones (upg100Zones), headings (regionTitle)
  and print path (the records provider with this plugin's evaluator, and
  zone-sheet-print.js).
- index.js registers the type ('upg100-list') and the element. The element
  registration is a no-op if another bundle on the page registered it first.
- poster-wall.vue groups through the same regionOf, draws the same zones, and
  passes collapsible + collapsedBy (all open). Its other props are unchanged.

check:upg100-poster-type (gates:pre) checks:
- every zone sits in a named region, with a negative control;
- the 8 sections exist, and the scopes are unique and round-trip;
- the headings exist ×11;
- the wiring holds.
Three mutants each fail it.
No library code changed.
2026-10-02 07:58:49 +00:00
49 changed files with 3241 additions and 400 deletions

View file

@ -50,11 +50,14 @@ const ok = (cond, msg) => { if (!cond) fails.push(msg) }
// directly (legend-tree's regionFor returns 'India' / 'China + Mongolia' / 'Other' without a helper call).
const TABLES = [
{
file: 'plugin-designs/my-upg-100-list/poster-wall.vue',
// the poster wall's region sections (and <dxm-poster-section type="upg100-list">): the table moved out of
// poster-wall.vue into data/upg100-regions.js; regionOf RETURNS 'India' / 'China + Mongolia' / 'Other' directly
file: 'plugin-designs/my-upg-100-list/data/upg100-regions.js',
produces: 'REGION_OF',
mapsFile: 'plugin-designs/my-upg-100-list/data/legend-tree.js',
maps: 'REGION_KEY',
via: 'regionLabel',
via: 'regionTitle',
producerReturns: 'regionOf',
area: 'poster',
},
{
@ -87,6 +90,13 @@ function producerLabels(src, fn) {
const m = new RegExp(`const ${fn}\\s*=\\s*\\([^)]*\\)\\s*=>([\\s\\S]*?);\\n`).exec(src)
return m ? [...m[1].matchAll(/[?:]\s*'([^']+)'/g)].map((x) => x[1]) : null
}
/** The labels a function declaration RETURNS as literals: `return 'India'`. */
function returnedLabels(src, fn) {
const at = src.search(new RegExp(`function ${fn}\\s*\\(`))
if (at < 0) return null
const body = src.slice(at, src.indexOf('\n}\n', at))
return [...body.matchAll(/return\s+'([^']+)'/g)].map((x) => x[1])
}
/** Quoted values on the right of a colon — the English labels a table produces. */
const valuesOf = (body) => new Set([...body.matchAll(/:\s*'([^']+)'/g)].map((m) => m[1]))
/** Quoted keys on the left of a colon — the English labels a table covers. */
@ -119,6 +129,11 @@ for (const t of TABLES) {
if (!got || !got.length) { ok(false, `${t.file}: no \`const ${t.producerFn} = (…) =>\` returning labels — this gate has gone blind`); continue }
fromFn = got
}
if (t.producerReturns) {
const got = returnedLabels(src, t.producerReturns)
if (!got || !got.length) { ok(false, `${t.file}: no \`function ${t.producerReturns}(…)\` returning labels — this gate has gone blind`); continue }
fromFn = [...fromFn, ...got]
}
const produced = new Set([...fromTable, ...fromCalls, ...fromFn])
const covered = keysOf(maps)

View file

@ -0,0 +1,84 @@
#!/usr/bin/env node
// check-upg100-poster-type — UPG-100's region sections are ONE section, whether the poster wall draws them or
// <dxm-poster-section type="upg100-list" scope="…"> does. Headless. Asserts:
// A. every zone the wall shows (clusters.json: lists + asia_lists + uncovered_lists) belongs to a NAMED region,
// never 'Other', and the sections the wall promises exist (India, China + Mongolia, Africa, Middle East,
// Central Asia, Southeast Asia + Oceania, Americas, Europe). NEGATIVE CONTROL: a planted unknown zone falls to
// 'Other', so this check can fail;
// B. each region's scope (its slug) is unique and round-trips from the English name, so a scope can't name two
// sections;
// C. each region's heading has its poster.region_* key in all 11 locales;
// D. the wiring: the bundle registers the 'upg100-list' type and the element; the type's adapter has the four
// parts a section reads; the wall groups through the SAME regionOf and draws the SAME zones (upg100Zones), and
// its sections are collapsible.
import { readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const P = (...a) => join(REPO, ...a)
const PLUGIN = 'plugin-designs/my-upg-100-list'
const fails = [], notes = []
const ok = (cond, msg, note) => { if (!cond) fails.push(msg); else if (note) notes.push(` ok ${note}`) }
const LANGS = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ro', 'ru', 'ar', 'hi', 'zh']
let R, clusters, REGION_KEY
try {
R = await import(pathToFileURL(P(PLUGIN, 'data/upg100-regions.js')).href)
clusters = JSON.parse(readFileSync(P(PLUGIN, 'data/clusters.json'), 'utf8'))
// REGION_KEY is a plain table in legend-tree.js; read it from the source rather than importing the module (it
// pulls JSON the node loader cannot take without import attributes).
const src = readFileSync(P(PLUGIN, 'data/legend-tree.js'), 'utf8')
REGION_KEY = Object.fromEntries([...src.slice(src.indexOf('export const REGION_KEY'), src.indexOf('};', src.indexOf('export const REGION_KEY'))).matchAll(/'([^']+)': '([^']+)'/g)].map((m) => [m[1], m[2]]))
} catch (e) {
console.error(`UPG100-POSTER-TYPE: NOT MEASURED (exit 75) — an input did not load: ${e.message}`)
process.exit(75)
}
// ── A ──
const ids = [...(clusters.lists || []), ...(clusters.asia_lists || []), ...(clusters.uncovered_lists || [])].map((l) => l.id).filter(Boolean)
const others = ids.filter((id) => R.regionOf(id) === 'Other')
const used = new Set(ids.map((id) => R.regionOf(id)))
const PROMISED = ['India', 'China + Mongolia', 'Africa', 'Middle East', 'Central Asia', 'Southeast Asia + Oceania', 'Americas', 'Europe']
ok(R.regionOf('a-zone-nobody-placed') === 'Other', 'NEGATIVE CONTROL: an unknown zone did not fall to Other, so this check cannot see a missing region',
'negative control: an unplaced zone falls to Other')
ok(ids.length > 20 && !others.length && PROMISED.every((r) => used.has(r)),
`zones without a region: ${others.join(', ') || '(none)'}; sections missing: ${PROMISED.filter((r) => !used.has(r)).join(', ') || '(none)'}`,
`${ids.length} zones, every one in a named region; all ${PROMISED.length} sections present`)
// ── B ──
const scopes = R.REGIONS.map(R.regionScope)
ok(new Set(scopes).size === scopes.length && R.REGIONS.every((r) => R.regionScope(R.regionScope(r)) === R.regionScope(r)) && R.regionScope('Southeast Asia + Oceania') === 'southeast-asia-oceania',
`scopes are not unique or do not round-trip: ${scopes.join(', ')}`, `scopes unique and stable: ${scopes.join(', ')}`)
// ── C ──
const missingKeys = []
for (const r of R.REGIONS) {
const key = REGION_KEY[r]
if (!key) { missingKeys.push(`${r}: no REGION_KEY`); continue }
for (const l of LANGS) { const cat = JSON.parse(readFileSync(P('library/i18n/locales', l, 'poster.json'), 'utf8')); if (typeof cat[key] !== 'string' || !cat[key].trim()) missingKeys.push(`${l} poster.${key}`) }
}
ok(!missingKeys.length, `region headings missing: ${missingKeys.slice(0, 6).join(', ')}`, `every region heading exists in all ${LANGS.length} locales`)
// ── D ──
const index = readFileSync(P(PLUGIN, 'index.js'), 'utf8')
const type = readFileSync(P(PLUGIN, 'data/upg100-poster-type.js'), 'utf8')
const wall = readFileSync(P(PLUGIN, 'poster-wall.vue'), 'utf8')
const wired = {
'the bundle registers the upg100-list type': /registerPosterType\('upg100-list', \(\) => import\('\.\/data\/upg100-poster-type\.js'\)\)/.test(index),
'the bundle registers the element': /\nregisterPosterSectionElement\(\)/.test(index),
'the adapter has scopes, items, title and actions': ['scopes:', 'items(scope, locale)', 'title(scope, t)', 'actions({'].every((s) => type.includes(s)),
'the wall groups through the same regionOf': /const regionFor = \(id\) => regionTitle\(regionOf\(id\), t\)/.test(wall) && !/const REGION_OF = /.test(wall),
'the wall draws the same zones': /return upg100Zones\(profileConfig, locale\)/.test(wall),
'the wall sections are collapsible': /:collapsible="true"/.test(wall) && /:collapsed-by="allOpen"/.test(wall),
}
const missingWires = Object.entries(wired).filter(([, v]) => !v).map(([k]) => k)
ok(!missingWires.length, `missing: ${missingWires.join('; ')}`, 'the wall and <dxm-poster-section type="upg100-list"> share one region table, one zone list and the collapsible section')
for (const n of notes) console.log(n)
if (fails.length) {
console.error('UPG100-POSTER-TYPE: FAIL')
for (const f of fails) console.error(` ✗ ${f}`)
process.exit(1)
}
console.log('UPG100-POSTER-TYPE: PASS — UPG-100\'s region sections are one section, on the wall and as <dxm-poster-section type="upg100-list">.')

View file

@ -3,6 +3,7 @@
"intro": "اختر الملصقات وشاهدها أدناه، ثم انسخ الرمز لوضعها على أي موقع.",
"type": "نوع الملصق",
"typeCountry": "الدول",
"typeUpg100List": "مناطق UPG-100",
"region": "المنطقة",
"allRegions": "كل المناطق",
"language": "اللغة",

View file

@ -3,6 +3,7 @@
"intro": "Wähle die Plakate, sieh sie dir unten an und kopiere dann den Code für eine beliebige Website.",
"type": "Plakattyp",
"typeCountry": "Länder",
"typeUpg100List": "UPG-100-Gebiete",
"region": "Region",
"allRegions": "Alle Regionen",
"language": "Sprache",

View file

@ -3,6 +3,7 @@
"intro": "Pick the posters, see them below, then copy the tag to put them on any website.",
"type": "Poster type",
"typeCountry": "Countries",
"typeUpg100List": "UPG-100 territories",
"region": "Region",
"allRegions": "All regions",
"language": "Language",

View file

@ -3,6 +3,7 @@
"intro": "Elige los carteles, míralos abajo y copia la etiqueta para ponerlos en cualquier sitio web.",
"type": "Tipo de cartel",
"typeCountry": "Países",
"typeUpg100List": "Territorios UPG-100",
"region": "Región",
"allRegions": "Todas las regiones",
"language": "Idioma",

View file

@ -3,6 +3,7 @@
"intro": "Choisissez les affiches, regardez-les ci-dessous, puis copiez la balise pour les placer sur n’importe quel site.",
"type": "Type d’affiche",
"typeCountry": "Pays",
"typeUpg100List": "Territoires UPG-100",
"region": "Région",
"allRegions": "Toutes les régions",
"language": "Langue",

View file

@ -3,6 +3,7 @@
"intro": "पोस्टर चुनें, नीचे देखें, फिर किसी भी वेबसाइट पर लगाने के लिए टैग कॉपी करें।",
"type": "पोस्टर का प्रकार",
"typeCountry": "देश",
"typeUpg100List": "UPG-100 क्षेत्र",
"region": "क्षेत्र",
"allRegions": "सभी क्षेत्र",
"language": "भाषा",

View file

@ -3,6 +3,7 @@
"intro": "Scegli i poster, guardali qui sotto, poi copia il tag per metterli su qualsiasi sito.",
"type": "Tipo di poster",
"typeCountry": "Paesi",
"typeUpg100List": "Territori UPG-100",
"region": "Regione",
"allRegions": "Tutte le regioni",
"language": "Lingua",

View file

@ -3,6 +3,7 @@
"intro": "Escolha os cartazes, veja-os abaixo e copie a etiqueta para colocá-los em qualquer site.",
"type": "Tipo de cartaz",
"typeCountry": "Países",
"typeUpg100List": "Territórios UPG-100",
"region": "Região",
"allRegions": "Todas as regiões",
"language": "Idioma",

View file

@ -3,6 +3,7 @@
"intro": "Alege posterele, vezi-le mai jos, apoi copiază eticheta ca să le pui pe orice site.",
"type": "Tipul posterului",
"typeCountry": "Țări",
"typeUpg100List": "Teritorii UPG-100",
"region": "Regiune",
"allRegions": "Toate regiunile",
"language": "Limbă",

View file

@ -3,6 +3,7 @@
"intro": "Выберите плакаты, посмотрите их ниже, затем скопируйте тег, чтобы разместить их на любом сайте.",
"type": "Тип плаката",
"typeCountry": "Страны",
"typeUpg100List": "Территории UPG-100",
"region": "Регион",
"allRegions": "Все регионы",
"language": "Язык",

View file

@ -3,6 +3,7 @@
"intro": "选择海报,在下方查看,然后复制标签,放到任何网站上。",
"type": "海报类型",
"typeCountry": "国家",
"typeUpg100List": "UPG-100 区域",
"region": "地区",
"allRegions": "所有地区",
"language": "语言",

View file

@ -245,6 +245,7 @@
"check:list-pattern-doc": "node internal/check-list-pattern-doc.mjs",
"check:pg-name-gate": "node internal/check-pg-name-gate.mjs",
"check:pin-selection": "node internal/check-pin-selection.mjs",
"check:upg100-poster-type": "node internal/check-upg100-poster-type.mjs",
"check:poster-action-print": "node internal/check-poster-action-print.mjs",
"check:popup-href-escaped": "node internal/check-popup-href-escaped.mjs",
"check:popup-pan-controls": "node internal/check-popup-pan-clears-controls.mjs",
@ -355,7 +356,7 @@
"fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test check:shell-header:self-test check:preview-flag-dev-only:self-test check:poster-type-registry:self-test check:host-floor:self-test",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:pins-in-state check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only check:poster-type-registry check:host-floor",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:pins-in-state check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only check:poster-type-registry check:host-floor check:upg100-poster-type",
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
"library-index": "node internal/generate-library-index.mjs",
@ -602,6 +603,7 @@
"check:popup-escaping": "the library's pin and region popups render hostile data (an <img onerror>, a <script>, a javascript: URL) as text, never markup: every field goes through escapeHtml/safeUrl (library/utils/html.js). Headless.",
"check:ported-globals-identical": "the DX Life base/_globals.scss rules that shadow-root components used to copy now come from one module (library/styles/portedGlobals.js), and the CSS each component actually INJECTS must be byte-identical to the pre-refactor baseline: it runs each component's setup with stubbed imports and hashes the real useShadowStyles argument, not the source text. Deduplication with a proof that no pixel can change. Node only, no browser.",
"check:pg-name-gate": "a people group's localized name (assets/pplr/pg-names.json) is shown only when it is a transliteration: library/utils/peopleGroupName.js falls back to English for EVERY locale of a row whose Latin-script names are far from the English one in two or more locales (bigram Dice < 0.5), because about a third were machine-translated as words (Balantak → 'Balance', Balo → 'Sac à dos'). A row with fewer than two Latin names cannot be checked and shows English too. Proved on the real table: the 4 known translated rows show English in 7 locales, Balangao Bontoc keeps its names, planted rows AT the threshold (exactly two far → English, one → kept, one Latin name → English), the whole-table fallback stays 30-45% (38.2%), and no file outside the helper reads or indexes pg-names rows (pplr-data-maps carded; the scanner is negative-controlled both ways). Headless.",
"check:upg100-poster-type": "UPG-100's region sections are ONE section on the poster wall and as <dxm-poster-section type=\"upg100-list\">: every zone in clusters.json belongs to a named region (a planted unknown zone falls to Other: the negative control), the 8 promised sections exist, each scope slug is unique and round-trips, each heading has its poster.region_* key in all 11 locales, and the wiring holds (the bundle registers the type and the element; the wall groups through the same regionOf, draws the same zones and its sections are collapsible). Headless.",
"check:pin-selection": "the shared source reader understands BOTH engines: mapbox-gl stores a GeoJSON source _data as the FeatureCollection, maplibre-gl wraps it as { geojson } (or { url } when remote). Reading only the Mapbox shape returned an EMPTY list on MapLibre — the keyless engine public builds use — which silently killed zoomToLegendRow camera fits on three maps and the research packet row selection, with no error because every caller no-ops on no features. Asserts both shapes, that a remote source returns [] AND warns, the serialize fallback, and that selectedPinIds agrees with the reader. Headless, no map. Exit 0 pass, 1 fail.",
"check:popup-href-escaped": "a URL interpolated into an HTML string is escaped before it lands in href= or src=. Written for RegionCountryMap's popup, which escaped every value except its two link URLs -- both built from joinBase(hostAttribute, apiSlug), so a quote in either escaped the attribute. Data URLs, blob URLs, encodeURIComponent and safeUrl() are recognised as safe; anything else needs an href-escaped-ok: reason. 6 self-tests. Source-only, headless.",
"check:poster-action-print": "a poster button opens the print dialog (the rule since 2026-09-28: never a direct download). Two arms: PosterWall's posterAction defaults to 'print' and any consumer opting into 'download' carries a posterAction-download: comment saying why; and no shipped file hand-rolls an <a download> for a poster IMAGE (svg/png/webp/pdf) without first attempting to print, which the prop arm cannot see. Data exports (csv/txt/json) are out of scope. 6 self-tests. Source-only, headless.",

View file

@ -0,0 +1,62 @@
/**
* The 'upg100-list' poster type: the UPG-100 zone posters, one section per region. Registered from this plugin
* (index.js, registerPosterType), so <dxm-poster-section type="upg100-list" scope="africa"> draws exactly the section
* the UPG-100 poster wall draws. The same zones (zonesForList, list-aware), the same region of each zone
* (upg100-regions.js), the same headings (poster.region_*), and the same print path (the library's records provider
* with this plugin's selector evaluator, and zone-sheet-print.js for the sheet).
* scope = a region's key in either spelling: the slug ("southeast-asia-oceania") or the English name.
*/
import { zonesForList } from './zones-for-list.js'
import { regionOf, regionScope, REGIONS } from './upg100-regions.js'
import { REGION_KEY } from './legend-tree.js'
import { zoneTitle } from './zone-title.js'
import { evaluate as evaluateSelector } from './selector-evaluator.js'
import { makeZoneSheet } from './zone-sheet-print.js'
import { resolvePeopleGroupList } from '@map/lists/peopleGroupList.js'
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
import { countryName } from '@map/utils/countryName.js'
import { keyedError } from '@map/i18n/keyedError.js'
/**
* Every zone the wall shows for the list in force, its card named in the reader's language. zoneTitle declines
* (null) for a zone it cannot name, and that zone keeps its hand-written label, exactly as on the legend and the
* packet.
*/
export function upg100Zones(profileConfig, locale) {
const z = zonesForList(resolvePeopleGroupList(profileConfig))
return [...(z.lists || []), ...(z.asia_lists || []), ...(z.uncovered_lists || [])]
.filter((l) => l && l.id)
.map((l) => { const title = zoneTitle(l, null, { countryName, locale }); return title ? { ...l, label: title } : l })
}
/** A region's heading in the reader's language (poster.region_*, in all 11 locales); the English key if none. */
export function regionTitle(english, t) {
const key = REGION_KEY[english]
if (!key) return english
const v = t(`poster.${key}`)
return v && v !== `poster.${key}` ? v : english
}
const englishOf = (scope) => REGIONS.find((r) => regionScope(r) === regionScope(scope)) || null
export default {
// Only the regions that HAVE zones: an empty region would be a section (and a Poster Sections preset) that can
// only say "No posters". Today that drops 'Other', which no UPG-100 zone falls into.
scopes: () => REGIONS.filter((r) => upg100Zones(null, 'en').some((l) => regionOf(l.id) === r)).map(regionScope),
items(scope, locale) {
return upg100Zones(null, locale).filter((l) => regionScope(regionOf(l.id)) === regionScope(scope))
},
title(scope, t) {
const english = englishOf(scope)
return english ? regionTitle(english, t) : String(scope || '')
},
actions({ assetsBase, apiBase, lang, profileConfig }) {
const recordsFor = createPeopleGroupRecords({ apiBase, lang, profileConfig, evaluate: (selector, records) => evaluateSelector(selector, records) })
return {
records: (list) => (apiBase() ? recordsFor(list.selector) : Promise.reject(keyedError('wall.hud.noApi', 'no people-groups service is configured'))),
sheet: makeZoneSheet({ allRecords: () => recordsFor.all(), assetsBase, profileConfig }),
unsupported: () => null,
unit: 'UPG', itemCountKey: 'wall.listCount', eventName: 'doxa-upg-100-list:show-on-map', downloadPrefix: 'doxa-upg-100', cardStocks: true,
}
},
}

View file

@ -0,0 +1,61 @@
// upg100-regions.js — which REGION SECTION each UPG-100 zone belongs to, and that section's scope key. PURE (no
// imports), so the poster wall, the 'upg100-list' poster type (upg100-poster-type.js) and the gate read ONE table.
// Moved here verbatim from poster-wall.vue, which groups by it as before.
// ─── REGIONAL SECTIONS ───────────────────────────────────────────────────────
// clusters.json carries no region field, so the mapping lives here. It is keyed by
// list id rather than derived from the selector: a selector can be a wagf_block, a
// country_codes array or an explicit pg_slugs list, and none of those reliably
// implies the continent a reader expects to browse by.
//
// SE Asia + Oceania are ONE section deliberately (the island chains read as a
// single sweep), and the Americas gathers Brazil, the Andean/Caribbean list and the
// US-bearing scatter list.
export const REGION_OF = {
// Africa
'west-africa': 'Africa', 'sudan': 'Africa', 'east-africa': 'Africa',
'central-africa': 'Africa', 'southern-africa-islands': 'Africa',
// Middle East. Maghreb+Egypt is geographically North AFRICA but belongs here:
// the region is read as a cultural/linguistic sweep.
'maghreb-egypt': 'Middle East',
'middle-east-core': 'Middle East',
// CENTRAL ASIA is its own section: the 55-UPG Pakistan map belongs to Central
// Asia, not the Middle East. It had been folded into the Middle
// East as "the same sweep continuing east", but Pakistan, Afghanistan,
// Uzbekistan, Tajikistan, Kyrgyzstan and Kazakhstan are Central/South Asia —
// filing them under the Middle East misnames six countries to save a heading.
'uncovered-nas-a': 'Central Asia',
// Southeast Asia + Oceania
'laos': 'Southeast Asia + Oceania', 'oceania-indonesia': 'Southeast Asia + Oceania',
'mainland-se-asia-philippines': 'Southeast Asia + Oceania',
// Americas
'brazil': 'Americas', 'andean-caribbean': 'Americas', 'uncovered-scatter': 'Americas',
// Europe
'europe': 'Europe',
// India and China are their OWN sections, not one lump called "Asia". Between
// them they are 1,122 people groups across 12 lists — more than every other
// section combined — so collapsing them hides the two largest prayer fields in
// the dataset behind a single continental label. Matched by id prefix below.
}
/** The section a zone belongs to, by its ENGLISH grouping key (REGION_OF, then the India / China id prefixes). */
export function regionOf(id) {
if (REGION_OF[id]) return REGION_OF[id]
if (String(id).startsWith('asia-india-')) return 'India'
// "China + Mongolia": the northern zone declares Mongolia as a member country (its land is drawn and highlighted),
// so the section heading names both rather than folding a sovereign country under a China label.
if (String(id).startsWith('asia-china-')) return 'China + Mongolia'
return 'Other'
}
/**
* A section's SCOPE for <dxm-poster-section type="upg100-list" scope="…">: its English key slugified the way DX
* Life's /regions does (no diacritics, lowercase, "&"/"+" → a dash, non-alphanumerics → "-"), so "Southeast Asia +
* Oceania" is "southeast-asia-oceania" and the English name itself is accepted too.
*/
export const regionScope = (s) => String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
.replace(/['’.]/g, '').replace(/&/g, ' and ').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'other'
/** Every section's English key, in a stable order (the wall itself sorts its sections by size). */
export const REGIONS = ['India', 'China + Mongolia', 'Africa', 'Middle East', 'Central Asia', 'Southeast Asia + Oceania', 'Americas', 'Europe', 'Other']
export default { REGION_OF, regionOf, regionScope, REGIONS }

View file

@ -1,4 +1,6 @@
import { defineMapElement } from '@map/elements/defineMapElement.js'
import { registerPosterType } from '@map/components/poster/posterTypes.js'
import { registerPosterSectionElement } from '@map/elements/posterSectionElement.js'
/**
* plugin-designs/my-upg-100-list/index.js
*
@ -64,4 +66,11 @@ if (typeof customElements !== 'undefined') {
}
}
// ★ THE 'upg100-list' POSTER TYPE, registered from this plugin (no plugin code in the library): one UPG-100 region
// section as an element, <dxm-poster-section type="upg100-list" scope="africa">, drawing the same section the poster
// wall draws (data/upg100-poster-type.js). The element is registered here too (a no-op when another bundle on the
// page did it first), so a page that loads only this bundle can show it.
registerPosterType('upg100-list', () => import('./data/upg100-poster-type.js'))
registerPosterSectionElement()
export default Upg100ListMapElement

View file

@ -12,7 +12,7 @@
* its records provider.
*
* WHAT COULD NOT BE SHARED, and why each is here rather than in the component:
* REGION_OF / regionFor — the 100-list's own continental taxonomy, keyed by list id because a
* regionFor (REGION_OF in data/upg100-regions.js) — the 100-list's own continental taxonomy, keyed by list id because a
* selector can be a wagf_block, a country_codes array or an explicit
* pg_slugs list and none of those implies a continent.
* fetchListRecords — knows the people-groups API, the selector evaluator AND the pg_slugs
@ -41,7 +41,6 @@
import { computed, inject, ref } from 'vue'
import { useOptionalT, useOptionalI18n } from '@map/composables/useOptionalT.js'
// The page's people-group LIST (library/lists/): every people-group fetch below keeps only its groups.
import { resolvePeopleGroupList } from '@map/lists/peopleGroupList.js'
import PosterWall from '@map/components/poster/PosterWall.vue'
import { resolveAssetsBase } from '@map/utils/embedBase.js'
// The reader's language, for the DATA fetch below. Not for interface text — that goes through t().
@ -49,18 +48,15 @@ import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
// The list's total for the intro, from the funnel (never a number written into a string; see data/upg-total.js).
import visionFunnel from '../../data/people-groups/vision-funnel.json'
import { upgTotal, countInSentence } from './data/upg-total.js'
// Named imports: only the list arrays ship in the bundle; the file's bookkeeping fields stay out.
import { zonesForList } from './data/zones-for-list.js'
// The SAME normalize+evaluate pipeline the sibling map profile uses to turn the live
// /people-groups/list payload + a list's `selector` into the exact rows that belong to it.
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
import { makeZoneSheet } from './data/zone-sheet-print.js'
import { REGION_KEY } from './data/legend-tree.js'
import { regionOf } from './data/upg100-regions.js'
// The zones (list-aware, each named in the reader's language) and each region's heading: the 'upg100-list' poster
// type's own functions, so this wall and <dxm-poster-section type="upg100-list"> draw the same cards.
import { upg100Zones, regionTitle } from './data/upg100-poster-type.js'
import { keyedError } from '@map/i18n/keyedError.js'
// A zone's title in the reader's language (tier A: its country; others keep their label) — the same
// generator the map's legend and the packet use, so a card, its alt text and its print say one name.
import { zoneTitle } from './data/zone-title.js'
import { countryName } from '@map/utils/countryName.js'
import { evaluate as evaluateSelector } from './data/selector-evaluator.js'
// The table the zone map sheets were generated from (generate.mjs labels each country with it), so a
@ -82,41 +78,7 @@ const API_BASE = (typeof window !== 'undefined' && window.MAP_APP_API_URL)
|| import.meta.env.VITE_API_BASE_URL
|| ''
// ─── REGIONAL SECTIONS ───────────────────────────────────────────────────────
// clusters.json carries no region field, so the mapping lives here. It is keyed by
// list id rather than derived from the selector: a selector can be a wagf_block, a
// country_codes array or an explicit pg_slugs list, and none of those reliably
// implies the continent a reader expects to browse by.
//
// SE Asia + Oceania are ONE section deliberately (the island chains read as a
// single sweep), and the Americas gathers Brazil, the Andean/Caribbean list and the
// US-bearing scatter list.
const REGION_OF = {
// Africa
'west-africa': 'Africa', 'sudan': 'Africa', 'east-africa': 'Africa',
'central-africa': 'Africa', 'southern-africa-islands': 'Africa',
// Middle East. Maghreb+Egypt is geographically North AFRICA but belongs here:
// the region is read as a cultural/linguistic sweep.
'maghreb-egypt': 'Middle East',
'middle-east-core': 'Middle East',
// CENTRAL ASIA is its own section: the 55-UPG Pakistan map belongs to Central
// Asia, not the Middle East. It had been folded into the Middle
// East as "the same sweep continuing east", but Pakistan, Afghanistan,
// Uzbekistan, Tajikistan, Kyrgyzstan and Kazakhstan are Central/South Asia —
// filing them under the Middle East misnames six countries to save a heading.
'uncovered-nas-a': 'Central Asia',
// Southeast Asia + Oceania
'laos': 'Southeast Asia + Oceania', 'oceania-indonesia': 'Southeast Asia + Oceania',
'mainland-se-asia-philippines': 'Southeast Asia + Oceania',
// Americas
'brazil': 'Americas', 'andean-caribbean': 'Americas', 'uncovered-scatter': 'Americas',
// Europe
'europe': 'Europe',
// India and China are their OWN sections, not one lump called "Asia". Between
// them they are 1,122 people groups across 12 lists — more than every other
// section combined — so collapsing them hides the two largest prayer fields in
// the dataset behind a single continental label. Matched by id prefix below.
}
// ─── REGIONAL SECTIONS: data/upg100-regions.js (moved there so the 'upg100-list' poster type reads the same table) ─
// ⚠ TAKES AN ID, while the shared wall's groupBy takes the whole ITEM. The template
// adapts it — `:group-by="(item) => regionFor(item.id)"` — rather than changing this
// signature, because the id is what the taxonomy is keyed by and every prefix rule
@ -128,19 +90,11 @@ const REGION_OF = {
// grouping — and the heading a reader sees is resolved here, per locale, through REGION_KEY
// (data/legend-tree.js), the one table the legend's region rows translate through as well. Every
// heading has a poster.region_* key in all 11 locales, India and Other included.
const regionLabel = (english) => {
const key = REGION_KEY[english]
return key ? t(`poster.${key}`) : english // no key yet -> the English heading, not a raw key
}
const regionFor = (id) => {
if (REGION_OF[id]) return regionLabel(REGION_OF[id])
if (id.startsWith('asia-india-')) return regionLabel('India')
// "China + Mongolia": the northern zone declares Mongolia as a member country
// (its land is drawn and highlighted), so the section heading names both rather
// than folding a sovereign country under a China label.
if (id.startsWith('asia-china-')) return regionLabel('China + Mongolia')
return regionLabel('Other')
}
const regionFor = (id) => regionTitle(regionOf(id), t)
// ★ COLLAPSIBLE, LIKE COUNTRY POSTERS (each region section is the shared PosterSection, through the wall). Every
// section starts OPEN: a reader who knew the wall finds it as before, and can now fold away the regions they are
// not praying for. (Country Posters starts its small regions shut; UPG-100's regions are all large.)
const allOpen = () => false // no key yet -> the English heading, not a raw key
// One flat list of the zones the map paints — for WHICHEVER list is showing.
//
// ★ THROUGH zonesForList, not a static clusters.json import (pattern work, 2026-09-27). This wall already
@ -156,21 +110,15 @@ const regionFor = (id) => {
// this list IS, and the old comment was describing the wrong thing.
const { locale: wallLocale } = useOptionalI18n('')
const items = computed(() => {
const z = zonesForList(resolvePeopleGroupList(profileConfig))
// The i18n locale REF, read here so this computed re-runs on a runtime language switch: getRuntimeLocale()
// is a plain getter, and a computed that reads only it is resolved once and never again (a frozen label).
const l = wallLocale && (wallLocale.value ?? wallLocale)
const locale = (typeof l === 'string' && l) || getRuntimeLocale() || detectLocale()
return [
...(z.lists || []),
...(z.asia_lists || []),
...(z.uncovered_lists || []),
].filter(l => l && l.id)
// ★ THE CARD'S NAME IN THE READER'S LANGUAGE. item.label is the Show-on-map button's accessible
// name, every poster's alt text and tooltip, the print HUD's title and the printed documents' title
// — all five were English in every locale. zoneTitle declines (null) for a zone it cannot name, and
// that zone keeps its hand-written label, exactly as on the legend and the packet.
.map(l => { const title = zoneTitle(l, null, { countryName, locale }); return title ? { ...l, label: title } : l })
// ★ THE CARD'S NAME IN THE READER'S LANGUAGE (item.label is the Show-on-map button's accessible name, every
// poster's alt text and tooltip, the print HUD's title and the printed documents' title). The zones and their
// names come from the 'upg100-list' poster type's own function, so this wall and <dxm-poster-section
// type="upg100-list"> can never show different cards.
return upg100Zones(profileConfig, locale)
})
// ─── THE RECORDS PROVIDER — now the library's (lists/peopleGroupRecords.js) ─────────
@ -292,6 +240,8 @@ const wallTag = computed(() => t('messages.upg100WallTag'))
download-prefix="doxa-upg-100"
:columns-control="true"
:min-card-px="150"
:collapsible="true"
:collapsed-by="allOpen"
/>
<!-- min-card-px 150: "Posters per row: 2" now SHOWS two on a phone. The shared clamp (210px) forced one at
390 and silently overrode the select, which read 2 (2026-09-30: work like the country posters,

View file

@ -66,6 +66,78 @@
"scope": "oceania"
}
},
{
"id": "upg100-all",
"title": "UPG 100 posters — all regions",
"props": {
"type": "upg100-list",
"scope": "all"
}
},
{
"id": "upg100-india",
"title": "UPG 100 posters — India",
"props": {
"type": "upg100-list",
"scope": "india"
}
},
{
"id": "upg100-china-mongolia",
"title": "UPG 100 posters — China + Mongolia",
"props": {
"type": "upg100-list",
"scope": "china-mongolia"
}
},
{
"id": "upg100-africa",
"title": "UPG 100 posters — Africa",
"props": {
"type": "upg100-list",
"scope": "africa"
}
},
{
"id": "upg100-middle-east",
"title": "UPG 100 posters — Middle East",
"props": {
"type": "upg100-list",
"scope": "middle-east"
}
},
{
"id": "upg100-central-asia",
"title": "UPG 100 posters — Central Asia",
"props": {
"type": "upg100-list",
"scope": "central-asia"
}
},
{
"id": "upg100-southeast-asia-oceania",
"title": "UPG 100 posters — Southeast Asia + Oceania",
"props": {
"type": "upg100-list",
"scope": "southeast-asia-oceania"
}
},
{
"id": "upg100-americas",
"title": "UPG 100 posters — Americas",
"props": {
"type": "upg100-list",
"scope": "americas"
}
},
{
"id": "upg100-europe",
"title": "UPG 100 posters — Europe",
"props": {
"type": "upg100-list",
"scope": "europe"
}
},
{
"id": "configure",
"title": "Configure — make a new preset",

View file

@ -9,13 +9,14 @@
* making new presets. Nothing here draws a poster: the sections ARE the element (registered by index.js), and Copy
* embed shows the same attributes as text (src/embed.js, one function for both).
*/
import { computed, h, inject, ref, watch } from 'vue'
import { computed, h, inject, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useOptionalI18n } from '@map/composables/useOptionalT.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { SUPPORTED_LOCALES, RTL_LOCALES, getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
import { posterType, posterTypeNames } from '@map/components/poster/posterTypes.js'
import { posterType, posterTypeNames, TYPE_REGISTERED_EVENT } from '@map/components/poster/posterTypes.js'
import { absoluteEmbedUrl, resolveAssetsBase } from '@map/utils/embedBase.js'
import { TAG, sectionAttrs, embedSnippet } from './src/embed.js'
import { TYPE_SCRIPTS } from './src/typeScripts.js'
const profileConfig = inject('profileConfig', ref(null))
const cfg = computed(() => profileConfig.value || {})
@ -44,10 +45,12 @@ const COLUMN_CHOICES = [1, 2, 3, 4, 5]
// The poster types the registry knows, read at runtime and again whenever the picker is opened: another bundle on
// the page may register its type later (each type's own bundle registers it). Named in the reader's language when the
// catalogue has a name for it, else by its registry name.
const types = ref(posterTypeNames())
const refreshTypes = () => { const now = posterTypeNames(); if (now.join() !== types.value.join()) types.value = now }
// Plus the types that load on demand (src/typeScripts.js), so the picker offers them before their script has run.
const knownTypes = () => [...new Set([...posterTypeNames(), ...Object.keys(TYPE_SCRIPTS)])]
const types = ref(knownTypes())
const refreshTypes = () => { const now = knownTypes(); if (now.join() !== types.value.join()) types.value = now }
const typeLabel = (name) => {
const key = `posterSections.type${name.charAt(0).toUpperCase()}${name.slice(1)}`
const key = `posterSections.type${name.replace(/(^|-)(\w)/g, (_, _d, c) => c.toUpperCase())}` // upg100-list → typeUpg100List
const v = t(key)
return v && v !== key ? v : name
}
@ -56,9 +59,30 @@ const langLabel = (code) => {
try { return new Intl.DisplayNames([code], { type: 'language' }).of(code) || code } catch { return code }
}
// A type another plugin registers (src/typeScripts.js) arrives with that plugin's bundle: loaded here as a second
// <script>, once per page, when the type is not registered yet. Its registration is announced, and this page (like
// <dxm-poster-section> itself) re-resolves the type then, so "all regions" lists the type's regions as they arrive.
const typeScriptUrl = (name) => (TYPE_SCRIPTS[name] ? absoluteEmbedUrl(TYPE_SCRIPTS[name], cfg.value) : '')
function ensureTypeScript(name) {
const url = typeScriptUrl(name)
if (!url || typeof document === 'undefined' || posterTypeNames().includes(name)) return
if (document.querySelector(`script[data-dxm-poster-type="${name}"]`)) return
const s = document.createElement('script')
s.src = url
s.dataset.dxmPosterType = name
document.head.appendChild(s)
}
// The regions the chosen type offers (its adapter's scopes), each named by the type's own section title.
const adapter = ref(null)
watch(type, async (name) => { adapter.value = await posterType(name) }, { immediate: true })
const resolveType = async (name) => { adapter.value = await posterType(name) }
watch(type, (name) => { ensureTypeScript(name); resolveType(name) }, { immediate: true })
const onTypeRegistered = (e) => {
refreshTypes()
if (!e || !e.detail || e.detail.name === type.value) resolveType(type.value)
}
onMounted(() => { try { window.addEventListener(TYPE_REGISTERED_EVENT, onTypeRegistered) } catch (_) { /* no window */ } })
onBeforeUnmount(() => { try { window.removeEventListener(TYPE_REGISTERED_EVENT, onTypeRegistered) } catch (_) { /* no window */ } })
const scopes = computed(() => {
const a = adapter.value
const list = a && typeof a.scopes === 'function' ? a.scopes() : []
@ -79,7 +103,7 @@ const choices = computed(() => ({
type: type.value, scope: scope.value, scopes: scopes.value.map((s) => s.key),
locale: lang.value, collapsed: collapsed.value, columns: columns.value, assetsBase: assetsBase.value,
}))
const snippet = computed(() => embedSnippet(choices.value, scriptUrl.value))
const snippet = computed(() => embedSnippet(choices.value, scriptUrl.value, typeScriptUrl(type.value) ? [typeScriptUrl(type.value)] : []))
// The live sections: the element itself, with exactly the attributes above. Keyed by them, so a new choice
// mounts a fresh section (a language change rebuilds its catalogue rather than patching it).

View file

@ -20,6 +20,7 @@ const LOC = join(dirname(fileURLToPath(import.meta.url)), '../../../library/i18n
const SUPPORTED_LOCALES = readdirSync(LOC).filter((d) => statSync(join(LOC, d)).isDirectory()).sort()
import { posterSectionConfig } from '../../../library/elements/posterSectionConfig.js'
import * as real from '../src/embed.js'
import { TYPE_SCRIPTS } from '../src/typeScripts.js'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../..')
const PRESETS = JSON.parse(readFileSync(join(ROOT, 'plugin-designs/poster-sections/poster-sections.instances.json'), 'utf8'))
// poster type → [preset id prefix, the file that defines the type's adapter (its default export has scopes())]
@ -61,6 +62,20 @@ function presetArm(presets, scopesByType) {
return fails
}
// LOADABLE: every preset's type is the library's own ('country') or a type another plugin registers whose bundle the
// page loads (src/typeScripts.js), and that bundle is in the built tree. Else the preset page could only say "No posters".
function loadableArm(presets, scripts, treeHas) {
const fails = []
for (const i of presets.instances.filter((x) => x.props && x.props.type && x.props.type !== 'country')) {
const path = scripts[i.props.type]
if (!path) fails.push(`preset "${i.id}" shows type "${i.props.type}", which no script on its page registers (add it to src/typeScripts.js)`)
else if (treeHas && !treeHas(path)) fails.push(`preset "${i.id}" needs ${path}, which is not in the built tree`)
}
return fails
}
const treeHas = (() => { const pub = join(ROOT, 'public'); if (!existsSync(pub)) return null
const tree = readdirSync(pub).find((d) => existsSync(join(pub, d, 'manifest.json'))); return tree ? (p) => existsSync(join(pub, tree, p)) : null })()
// The built preset pages, when a tree is built: every folder under poster-sections/poster-sections/ that holds a page.
function builtPages() {
const pub = join(ROOT, 'public')
@ -113,6 +128,9 @@ if (process.argv.includes('--self-test')) {
}
let ok = true
const stale = { ...PRESETS, instances: [...PRESETS.instances, { id: 'countries-atlantis', props: { type: 'country', scope: 'atlantis' } }] }
const lf = loadableArm(PRESETS, {}, null)
console.log(` mutant "a preset of another plugin's type with no script to load it": ${lf.length || !PRESETS.instances.some((i) => i.props && i.props.type && i.props.type !== 'country') ? (lf.length ? 'FAILS (good)' : 'NOT APPLICABLE (no such preset)') : 'PASSES (bad)'}`)
if (!lf.length && PRESETS.instances.some((i) => i.props && i.props.type && i.props.type !== 'country')) ok = false
const pg = pagesArm(PRESETS, [...PRESETS.instances.map((i) => i.id), 'countries-atlantis'])
console.log(` mutant "a built page for a preset that no longer exists": ${pg.length ? 'FAILS (good)' : 'PASSES (bad)'}`)
if (!pg.length) ok = false
@ -127,7 +145,7 @@ if (process.argv.includes('--self-test')) {
console.log(ok ? 'POSTER-SECTIONS SELF-TEST: PASS — every mutant fails the check.' : 'POSTER-SECTIONS SELF-TEST: FAIL')
process.exit(ok ? 0 : 1)
}
const fails = [...arms(real), ...presetArm(PRESETS, SCOPES_BY_TYPE), ...pagesArm(PRESETS, builtPages())]
const fails = [...arms(real), ...presetArm(PRESETS, SCOPES_BY_TYPE), ...pagesArm(PRESETS, builtPages()), ...loadableArm(PRESETS, TYPE_SCRIPTS, treeHas)]
const measured = Object.entries(SCOPES_BY_TYPE).filter(([, v]) => v).map(([k, v]) => `${k} ${v.length}`).join(', ')
if (fails.length) { console.log(`POSTER-SECTIONS: FAIL (${fails.length})`); fails.forEach((f) => console.log(` - ${f}`)); process.exit(1) }
console.log(`POSTER-SECTIONS: PASS — every panel choice (${SCOPES.length + 1} region choices × ${SUPPORTED_LOCALES.length} languages × open/collapsed × 5 per row) reads back through the element unchanged; All regions = one section per region; the snippet is the script + those tags; presets = each type's scopes() + all (${measured}).`)

View file

@ -24,8 +24,12 @@ export function sectionAttrs(choices) {
const esc = (v) => String(v).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;')
/** The text a website pastes: the bundle's script once, then one tag per section. */
export function embedSnippet(choices, scriptUrl) {
/**
* The text a website pastes: the bundle's script once, then any script the poster type needs (its own plugin's bundle,
* src/typeScripts.js), then one tag per section.
*/
export function embedSnippet(choices, scriptUrl, typeScriptUrls = []) {
const tags = sectionAttrs(choices).map((a) => `<${TAG}${Object.entries(a).map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${esc(v)}"`)).join('')}></${TAG}>`)
return [`<script src="${esc(scriptUrl)}"></script>`, ...tags].join('\n')
const scripts = [scriptUrl, ...typeScriptUrls].map((u) => `<script src="${esc(u)}"></script>`)
return [...scripts, ...tags].join('\n')
}

View file

@ -0,0 +1,9 @@
/**
* typeScripts.js — poster types that ANOTHER plugin's bundle registers, and that bundle's path in the tree. A preset
* page or an embed showing such a type loads the bundle as a SECOND <script> (posterTypes.js keeps one registry per
* page, so a type registered by any script on the page reaches <dxm-poster-section>); never by import. 'country' is
* built into the library and needs nothing. check:poster-sections holds every preset's type to this list.
*/
export const TYPE_SCRIPTS = {
'upg100-list': 'my-upg-100-list/my-upg-100-list.js',
}

View file

@ -929,7 +929,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR",
"integrity": "sha384-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h",
"elements": [
"my-upg-100-list-map"
],
@ -952,7 +952,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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"provider\":\"maplibre\",\"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-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -968,8 +968,8 @@
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<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-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<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-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -985,8 +985,8 @@
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<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-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<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-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
}
]
@ -1181,7 +1181,7 @@
"bundle": "poster-sections",
"scriptUrl": "./poster-sections/poster-sections.js",
"output": "dx-maps/poster-sections/poster-sections.js",
"integrity": "sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD",
"integrity": "sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM",
"elements": [
"poster-sections"
],
@ -1200,7 +1200,7 @@
"scriptUrl": "./poster-sections/poster-sections.js",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/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=\"./poster-sections/poster-sections.js\" integrity=\"sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-sections profile-config='{\"profile\":\"poster-sections\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-sections>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-sections/poster-sections.js\" integrity=\"sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-sections profile-config='{\"profile\":\"poster-sections\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-sections>"
},
"instances": [
{
@ -1323,6 +1323,141 @@
"iframe": "<iframe src=\"./poster-sections/poster-sections/countries-oceania/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-all",
"title": "UPG 100 posters — all regions",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "all"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-all/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-all/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-india",
"title": "UPG 100 posters — India",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "india"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-india/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-india/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-china-mongolia",
"title": "UPG 100 posters — China + Mongolia",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "china-mongolia"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-china-mongolia/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-china-mongolia/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-africa",
"title": "UPG 100 posters — Africa",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "africa"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-africa/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-africa/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-middle-east",
"title": "UPG 100 posters — Middle East",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "middle-east"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-middle-east/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-middle-east/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-central-asia",
"title": "UPG 100 posters — Central Asia",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "central-asia"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-central-asia/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-central-asia/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-southeast-asia-oceania",
"title": "UPG 100 posters — Southeast Asia + Oceania",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "southeast-asia-oceania"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-southeast-asia-oceania/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-southeast-asia-oceania/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-americas",
"title": "UPG 100 posters — Americas",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "americas"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-americas/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-americas/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-europe",
"title": "UPG 100 posters — Europe",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "europe"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-europe/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-europe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "configure",
"title": "Configure — make a new preset",

View file

@ -929,7 +929,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR",
"integrity": "sha384-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h",
"elements": [
"my-upg-100-list-map"
],
@ -952,7 +952,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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"provider\":\"maplibre\",\"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-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -968,8 +968,8 @@
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<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-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<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-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -985,8 +985,8 @@
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<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-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<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-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
}
]
@ -1181,7 +1181,7 @@
"bundle": "poster-sections",
"scriptUrl": "./poster-sections/poster-sections.js",
"output": "dx-maps/poster-sections/poster-sections.js",
"integrity": "sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD",
"integrity": "sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM",
"elements": [
"poster-sections"
],
@ -1200,7 +1200,7 @@
"scriptUrl": "./poster-sections/poster-sections.js",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/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=\"./poster-sections/poster-sections.js\" integrity=\"sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-sections profile-config='{\"profile\":\"poster-sections\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-sections>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-sections/poster-sections.js\" integrity=\"sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-sections profile-config='{\"profile\":\"poster-sections\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-sections>"
},
"instances": [
{
@ -1323,6 +1323,141 @@
"iframe": "<iframe src=\"./poster-sections/poster-sections/countries-oceania/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-all",
"title": "UPG 100 posters — all regions",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "all"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-all/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-all/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-india",
"title": "UPG 100 posters — India",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "india"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-india/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-india/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-china-mongolia",
"title": "UPG 100 posters — China + Mongolia",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "china-mongolia"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-china-mongolia/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-china-mongolia/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-africa",
"title": "UPG 100 posters — Africa",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "africa"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-africa/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-africa/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-middle-east",
"title": "UPG 100 posters — Middle East",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "middle-east"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-middle-east/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-middle-east/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-central-asia",
"title": "UPG 100 posters — Central Asia",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "central-asia"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-central-asia/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-central-asia/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-southeast-asia-oceania",
"title": "UPG 100 posters — Southeast Asia + Oceania",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "southeast-asia-oceania"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-southeast-asia-oceania/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-southeast-asia-oceania/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-americas",
"title": "UPG 100 posters — Americas",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "americas"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-americas/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-americas/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "upg100-europe",
"title": "UPG 100 posters — Europe",
"apiBaseUrl": "",
"props": {
"profile": "poster-sections",
"type": "upg100-list",
"scope": "europe"
},
"element": "poster-sections",
"iframeUrl": "./poster-sections/poster-sections/upg100-europe/index.html",
"embed": {
"iframe": "<iframe src=\"./poster-sections/poster-sections/upg100-europe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "configure",
"title": "Configure — make a new preset",

File diff suppressed because one or more lines are too long

View file

@ -67,7 +67,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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -76,7 +76,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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -82,7 +82,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-NI+A8OCrOoKb53elAZUtoKYfH1+fT0CyF8htPQGnHKpZvJAckCPNoUBUAohAp9eR" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-2Vtd481vNcbOqhdLb5ywDi4grWgiAcYgt6YFH85vJtsIzEMprh47Mwy85ungmy3h" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD" crossorigin="anonymous"></script>
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD" crossorigin="anonymous"></script>
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD" crossorigin="anonymous"></script>
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD" crossorigin="anonymous"></script>
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD" crossorigin="anonymous"></script>
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD" crossorigin="anonymous"></script>
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD" crossorigin="anonymous"></script>
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD" crossorigin="anonymous"></script>
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD" crossorigin="anonymous"></script>
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -67,7 +67,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../poster-sections.js" integrity="sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD" crossorigin="anonymous"></script>
<script src="../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>UPG 100 posters — Africa · poster-sections</title>
<link rel="icon" href="data:,">
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
<style>
/* BARE full-page map — no chrome. The custom element IS the page, so the
browser URL shares the map itself.
HEIGHT IN svh, NOT inset:0. A fixed box with inset:0 follows the phone's DYNAMIC viewport, so the map
(and a legend sheet sized from it) changes height every time the address bar hides or shows. 100svh is
the viewport WITH the bar shown and never changes; when the bar hides, the page background shows below
instead of the map resizing. On a desktop svh = the viewport, so nothing moves there. The 100% before it
is the fallback for a browser without svh, which would otherwise size the box to its content.
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, poster-sections { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
padding: 24px; text-align: center; color: #9aa7b8;
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
#status.err { color: #f87171; }
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
Two things every generated leaf got wrong on paper, because no generated page has ever
carried an @media print rule (measured: 0 across the tree).
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
flooded black, and that is the reader who most wants the printout.
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
a full sheet reading "loading…" above the content that did in fact load. It is hidden
only while it is NOT an error — an error still prints, because a blank sheet with the
explanation suppressed is worse than a sheet that says what went wrong. */
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
then blank to the bottom of the sheet.
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
so height:auto with nothing else can leave the element a zero-height box and print an empty
page — a worse failure than the one being fixed, and the discriminator is not isMap
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
box for the map case while letting a taller document grow onto page two. */
#slot, poster-sections { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
}
</style>
</head>
<body>
<div id="slot"></div>
<div id="status">loading…</div>
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
// 2. ?tk= URL param — token carried in the shared link
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
// *endpoint* is resolved and fetched:
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
// means "read that global"; anything else is used as a URL — so both a global
// name and a plain URL are safe (no SyntaxError footgun).
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"poster-sections","type":"upg100-list","scope":"africa"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
var TOKEN_FREE = true;
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
// rather than this page's profile-config. With no baked token (the public copy), it must get
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
var NESTS_MAPS = false;
var TOKEN_SRC = "window.MAP_TOKEN_URL";
var BAKED_TOKEN = "";
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
var DEFAULT_TOKEN_URL = '/api/maps/token';
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
function askToken() {
if (!status) return;
status.className = 'err';
status.innerHTML = 'map needs a Mapbox token — ' +
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
var go = document.getElementById('tkGo');
if (go) go.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (!v) return;
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
mount(v);
});
}
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
mounted element does not cover shows it through, because every layer above body is
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
unpainted canvas as first supposed. Clearing it here (the one place we already know the
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
function done() {
if (status && status.parentNode) { status.parentNode.removeChild(status); }
document.documentElement.style.background = '#f3f3f1';
document.body.style.background = '#f3f3f1';
}
function mount(token) {
if (!token && !TOKEN_FREE) { return askToken(); }
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
try {
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
var el = document.createElement("poster-sections");
el.setAttribute('profile-config', JSON.stringify(cfg));
slot.innerHTML = '';
slot.appendChild(el);
done();
} catch (e) { fail(e && e.message ? e.message : String(e)); }
}
function qp(name) {
try { return new URLSearchParams(location.search).get(name) || ''; }
catch (e) { return ''; }
}
function extractToken(data) {
if (typeof data === 'string') return data.trim();
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
return '';
}
// Rungs 1-3b: an actual token → mount directly, no fetch.
// 2b = localStorage: the paste-once store shared with the staging pages
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
var stored = '';
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
var urlTk = qp('tk');
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
if (TOKEN_FREE) {
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
mount('');
// A token-free composite with no token anywhere: its content renders, but its nested maps
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
var bar = document.createElement('div');
bar.id = 'status';
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
document.body.appendChild(bar);
status = bar;
askToken();
var goBtn = document.getElementById('tkGo');
if (goBtn) goBtn.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (v) { location.reload(); }
});
}
return;
}
if (directToken) { return mount(directToken); }
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
// A single hard-coded pick used to kill the page when its one url 404'd or was
// blocked by CORS; a shared link must work wherever the tree is hosted.
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function fetchToken(i, errors) {
if (i >= candidates.length) {
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
askToken();
if (status) {
var d = document.createElement('div');
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
status.insertBefore(d, status.firstChild);
}
return;
}
var url = candidates[i];
return fetch(url)
.then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
var ct = r.headers.get('content-type') || '';
return ct.indexOf('json') !== -1 ? r.json() : r.text();
})
.then(function (data) {
var tk = extractToken(data);
if (!tk) throw new Error(url + ' → no token in response');
mount(tk);
})
.catch(function (e) {
errors.push(e && e.message ? e.message : String(e));
return fetchToken(i + 1, errors);
});
}
fetchToken(0, []);
})();
</script>
</body>
</html>

View file

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>UPG 100 posters — all regions · poster-sections</title>
<link rel="icon" href="data:,">
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
<style>
/* BARE full-page map — no chrome. The custom element IS the page, so the
browser URL shares the map itself.
HEIGHT IN svh, NOT inset:0. A fixed box with inset:0 follows the phone's DYNAMIC viewport, so the map
(and a legend sheet sized from it) changes height every time the address bar hides or shows. 100svh is
the viewport WITH the bar shown and never changes; when the bar hides, the page background shows below
instead of the map resizing. On a desktop svh = the viewport, so nothing moves there. The 100% before it
is the fallback for a browser without svh, which would otherwise size the box to its content.
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, poster-sections { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
padding: 24px; text-align: center; color: #9aa7b8;
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
#status.err { color: #f87171; }
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
Two things every generated leaf got wrong on paper, because no generated page has ever
carried an @media print rule (measured: 0 across the tree).
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
flooded black, and that is the reader who most wants the printout.
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
a full sheet reading "loading…" above the content that did in fact load. It is hidden
only while it is NOT an error — an error still prints, because a blank sheet with the
explanation suppressed is worse than a sheet that says what went wrong. */
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
then blank to the bottom of the sheet.
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
so height:auto with nothing else can leave the element a zero-height box and print an empty
page — a worse failure than the one being fixed, and the discriminator is not isMap
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
box for the map case while letting a taller document grow onto page two. */
#slot, poster-sections { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
}
</style>
</head>
<body>
<div id="slot"></div>
<div id="status">loading…</div>
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
// 2. ?tk= URL param — token carried in the shared link
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
// *endpoint* is resolved and fetched:
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
// means "read that global"; anything else is used as a URL — so both a global
// name and a plain URL are safe (no SyntaxError footgun).
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"poster-sections","type":"upg100-list","scope":"all"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
var TOKEN_FREE = true;
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
// rather than this page's profile-config. With no baked token (the public copy), it must get
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
var NESTS_MAPS = false;
var TOKEN_SRC = "window.MAP_TOKEN_URL";
var BAKED_TOKEN = "";
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
var DEFAULT_TOKEN_URL = '/api/maps/token';
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
function askToken() {
if (!status) return;
status.className = 'err';
status.innerHTML = 'map needs a Mapbox token — ' +
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
var go = document.getElementById('tkGo');
if (go) go.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (!v) return;
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
mount(v);
});
}
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
mounted element does not cover shows it through, because every layer above body is
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
unpainted canvas as first supposed. Clearing it here (the one place we already know the
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
function done() {
if (status && status.parentNode) { status.parentNode.removeChild(status); }
document.documentElement.style.background = '#f3f3f1';
document.body.style.background = '#f3f3f1';
}
function mount(token) {
if (!token && !TOKEN_FREE) { return askToken(); }
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
try {
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
var el = document.createElement("poster-sections");
el.setAttribute('profile-config', JSON.stringify(cfg));
slot.innerHTML = '';
slot.appendChild(el);
done();
} catch (e) { fail(e && e.message ? e.message : String(e)); }
}
function qp(name) {
try { return new URLSearchParams(location.search).get(name) || ''; }
catch (e) { return ''; }
}
function extractToken(data) {
if (typeof data === 'string') return data.trim();
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
return '';
}
// Rungs 1-3b: an actual token → mount directly, no fetch.
// 2b = localStorage: the paste-once store shared with the staging pages
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
var stored = '';
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
var urlTk = qp('tk');
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
if (TOKEN_FREE) {
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
mount('');
// A token-free composite with no token anywhere: its content renders, but its nested maps
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
var bar = document.createElement('div');
bar.id = 'status';
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
document.body.appendChild(bar);
status = bar;
askToken();
var goBtn = document.getElementById('tkGo');
if (goBtn) goBtn.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (v) { location.reload(); }
});
}
return;
}
if (directToken) { return mount(directToken); }
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
// A single hard-coded pick used to kill the page when its one url 404'd or was
// blocked by CORS; a shared link must work wherever the tree is hosted.
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function fetchToken(i, errors) {
if (i >= candidates.length) {
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
askToken();
if (status) {
var d = document.createElement('div');
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
status.insertBefore(d, status.firstChild);
}
return;
}
var url = candidates[i];
return fetch(url)
.then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
var ct = r.headers.get('content-type') || '';
return ct.indexOf('json') !== -1 ? r.json() : r.text();
})
.then(function (data) {
var tk = extractToken(data);
if (!tk) throw new Error(url + ' → no token in response');
mount(tk);
})
.catch(function (e) {
errors.push(e && e.message ? e.message : String(e));
return fetchToken(i + 1, errors);
});
}
fetchToken(0, []);
})();
</script>
</body>
</html>

View file

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>UPG 100 posters — Americas · poster-sections</title>
<link rel="icon" href="data:,">
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
<style>
/* BARE full-page map — no chrome. The custom element IS the page, so the
browser URL shares the map itself.
HEIGHT IN svh, NOT inset:0. A fixed box with inset:0 follows the phone's DYNAMIC viewport, so the map
(and a legend sheet sized from it) changes height every time the address bar hides or shows. 100svh is
the viewport WITH the bar shown and never changes; when the bar hides, the page background shows below
instead of the map resizing. On a desktop svh = the viewport, so nothing moves there. The 100% before it
is the fallback for a browser without svh, which would otherwise size the box to its content.
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, poster-sections { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
padding: 24px; text-align: center; color: #9aa7b8;
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
#status.err { color: #f87171; }
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
Two things every generated leaf got wrong on paper, because no generated page has ever
carried an @media print rule (measured: 0 across the tree).
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
flooded black, and that is the reader who most wants the printout.
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
a full sheet reading "loading…" above the content that did in fact load. It is hidden
only while it is NOT an error — an error still prints, because a blank sheet with the
explanation suppressed is worse than a sheet that says what went wrong. */
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
then blank to the bottom of the sheet.
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
so height:auto with nothing else can leave the element a zero-height box and print an empty
page — a worse failure than the one being fixed, and the discriminator is not isMap
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
box for the map case while letting a taller document grow onto page two. */
#slot, poster-sections { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
}
</style>
</head>
<body>
<div id="slot"></div>
<div id="status">loading…</div>
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
// 2. ?tk= URL param — token carried in the shared link
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
// *endpoint* is resolved and fetched:
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
// means "read that global"; anything else is used as a URL — so both a global
// name and a plain URL are safe (no SyntaxError footgun).
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"poster-sections","type":"upg100-list","scope":"americas"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
var TOKEN_FREE = true;
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
// rather than this page's profile-config. With no baked token (the public copy), it must get
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
var NESTS_MAPS = false;
var TOKEN_SRC = "window.MAP_TOKEN_URL";
var BAKED_TOKEN = "";
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
var DEFAULT_TOKEN_URL = '/api/maps/token';
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
function askToken() {
if (!status) return;
status.className = 'err';
status.innerHTML = 'map needs a Mapbox token — ' +
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
var go = document.getElementById('tkGo');
if (go) go.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (!v) return;
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
mount(v);
});
}
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
mounted element does not cover shows it through, because every layer above body is
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
unpainted canvas as first supposed. Clearing it here (the one place we already know the
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
function done() {
if (status && status.parentNode) { status.parentNode.removeChild(status); }
document.documentElement.style.background = '#f3f3f1';
document.body.style.background = '#f3f3f1';
}
function mount(token) {
if (!token && !TOKEN_FREE) { return askToken(); }
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
try {
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
var el = document.createElement("poster-sections");
el.setAttribute('profile-config', JSON.stringify(cfg));
slot.innerHTML = '';
slot.appendChild(el);
done();
} catch (e) { fail(e && e.message ? e.message : String(e)); }
}
function qp(name) {
try { return new URLSearchParams(location.search).get(name) || ''; }
catch (e) { return ''; }
}
function extractToken(data) {
if (typeof data === 'string') return data.trim();
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
return '';
}
// Rungs 1-3b: an actual token → mount directly, no fetch.
// 2b = localStorage: the paste-once store shared with the staging pages
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
var stored = '';
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
var urlTk = qp('tk');
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
if (TOKEN_FREE) {
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
mount('');
// A token-free composite with no token anywhere: its content renders, but its nested maps
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
var bar = document.createElement('div');
bar.id = 'status';
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
document.body.appendChild(bar);
status = bar;
askToken();
var goBtn = document.getElementById('tkGo');
if (goBtn) goBtn.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (v) { location.reload(); }
});
}
return;
}
if (directToken) { return mount(directToken); }
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
// A single hard-coded pick used to kill the page when its one url 404'd or was
// blocked by CORS; a shared link must work wherever the tree is hosted.
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function fetchToken(i, errors) {
if (i >= candidates.length) {
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
askToken();
if (status) {
var d = document.createElement('div');
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
status.insertBefore(d, status.firstChild);
}
return;
}
var url = candidates[i];
return fetch(url)
.then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
var ct = r.headers.get('content-type') || '';
return ct.indexOf('json') !== -1 ? r.json() : r.text();
})
.then(function (data) {
var tk = extractToken(data);
if (!tk) throw new Error(url + ' → no token in response');
mount(tk);
})
.catch(function (e) {
errors.push(e && e.message ? e.message : String(e));
return fetchToken(i + 1, errors);
});
}
fetchToken(0, []);
})();
</script>
</body>
</html>

View file

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>UPG 100 posters — Central Asia · poster-sections</title>
<link rel="icon" href="data:,">
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
<style>
/* BARE full-page map — no chrome. The custom element IS the page, so the
browser URL shares the map itself.
HEIGHT IN svh, NOT inset:0. A fixed box with inset:0 follows the phone's DYNAMIC viewport, so the map
(and a legend sheet sized from it) changes height every time the address bar hides or shows. 100svh is
the viewport WITH the bar shown and never changes; when the bar hides, the page background shows below
instead of the map resizing. On a desktop svh = the viewport, so nothing moves there. The 100% before it
is the fallback for a browser without svh, which would otherwise size the box to its content.
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, poster-sections { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
padding: 24px; text-align: center; color: #9aa7b8;
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
#status.err { color: #f87171; }
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
Two things every generated leaf got wrong on paper, because no generated page has ever
carried an @media print rule (measured: 0 across the tree).
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
flooded black, and that is the reader who most wants the printout.
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
a full sheet reading "loading…" above the content that did in fact load. It is hidden
only while it is NOT an error — an error still prints, because a blank sheet with the
explanation suppressed is worse than a sheet that says what went wrong. */
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
then blank to the bottom of the sheet.
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
so height:auto with nothing else can leave the element a zero-height box and print an empty
page — a worse failure than the one being fixed, and the discriminator is not isMap
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
box for the map case while letting a taller document grow onto page two. */
#slot, poster-sections { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
}
</style>
</head>
<body>
<div id="slot"></div>
<div id="status">loading…</div>
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
// 2. ?tk= URL param — token carried in the shared link
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
// *endpoint* is resolved and fetched:
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
// means "read that global"; anything else is used as a URL — so both a global
// name and a plain URL are safe (no SyntaxError footgun).
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"poster-sections","type":"upg100-list","scope":"central-asia"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
var TOKEN_FREE = true;
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
// rather than this page's profile-config. With no baked token (the public copy), it must get
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
var NESTS_MAPS = false;
var TOKEN_SRC = "window.MAP_TOKEN_URL";
var BAKED_TOKEN = "";
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
var DEFAULT_TOKEN_URL = '/api/maps/token';
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
function askToken() {
if (!status) return;
status.className = 'err';
status.innerHTML = 'map needs a Mapbox token — ' +
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
var go = document.getElementById('tkGo');
if (go) go.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (!v) return;
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
mount(v);
});
}
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
mounted element does not cover shows it through, because every layer above body is
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
unpainted canvas as first supposed. Clearing it here (the one place we already know the
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
function done() {
if (status && status.parentNode) { status.parentNode.removeChild(status); }
document.documentElement.style.background = '#f3f3f1';
document.body.style.background = '#f3f3f1';
}
function mount(token) {
if (!token && !TOKEN_FREE) { return askToken(); }
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
try {
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
var el = document.createElement("poster-sections");
el.setAttribute('profile-config', JSON.stringify(cfg));
slot.innerHTML = '';
slot.appendChild(el);
done();
} catch (e) { fail(e && e.message ? e.message : String(e)); }
}
function qp(name) {
try { return new URLSearchParams(location.search).get(name) || ''; }
catch (e) { return ''; }
}
function extractToken(data) {
if (typeof data === 'string') return data.trim();
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
return '';
}
// Rungs 1-3b: an actual token → mount directly, no fetch.
// 2b = localStorage: the paste-once store shared with the staging pages
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
var stored = '';
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
var urlTk = qp('tk');
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
if (TOKEN_FREE) {
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
mount('');
// A token-free composite with no token anywhere: its content renders, but its nested maps
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
var bar = document.createElement('div');
bar.id = 'status';
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
document.body.appendChild(bar);
status = bar;
askToken();
var goBtn = document.getElementById('tkGo');
if (goBtn) goBtn.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (v) { location.reload(); }
});
}
return;
}
if (directToken) { return mount(directToken); }
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
// A single hard-coded pick used to kill the page when its one url 404'd or was
// blocked by CORS; a shared link must work wherever the tree is hosted.
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function fetchToken(i, errors) {
if (i >= candidates.length) {
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
askToken();
if (status) {
var d = document.createElement('div');
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
status.insertBefore(d, status.firstChild);
}
return;
}
var url = candidates[i];
return fetch(url)
.then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
var ct = r.headers.get('content-type') || '';
return ct.indexOf('json') !== -1 ? r.json() : r.text();
})
.then(function (data) {
var tk = extractToken(data);
if (!tk) throw new Error(url + ' → no token in response');
mount(tk);
})
.catch(function (e) {
errors.push(e && e.message ? e.message : String(e));
return fetchToken(i + 1, errors);
});
}
fetchToken(0, []);
})();
</script>
</body>
</html>

View file

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>UPG 100 posters — China + Mongolia · poster-sections</title>
<link rel="icon" href="data:,">
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
<style>
/* BARE full-page map — no chrome. The custom element IS the page, so the
browser URL shares the map itself.
HEIGHT IN svh, NOT inset:0. A fixed box with inset:0 follows the phone's DYNAMIC viewport, so the map
(and a legend sheet sized from it) changes height every time the address bar hides or shows. 100svh is
the viewport WITH the bar shown and never changes; when the bar hides, the page background shows below
instead of the map resizing. On a desktop svh = the viewport, so nothing moves there. The 100% before it
is the fallback for a browser without svh, which would otherwise size the box to its content.
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, poster-sections { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
padding: 24px; text-align: center; color: #9aa7b8;
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
#status.err { color: #f87171; }
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
Two things every generated leaf got wrong on paper, because no generated page has ever
carried an @media print rule (measured: 0 across the tree).
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
flooded black, and that is the reader who most wants the printout.
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
a full sheet reading "loading…" above the content that did in fact load. It is hidden
only while it is NOT an error — an error still prints, because a blank sheet with the
explanation suppressed is worse than a sheet that says what went wrong. */
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
then blank to the bottom of the sheet.
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
so height:auto with nothing else can leave the element a zero-height box and print an empty
page — a worse failure than the one being fixed, and the discriminator is not isMap
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
box for the map case while letting a taller document grow onto page two. */
#slot, poster-sections { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
}
</style>
</head>
<body>
<div id="slot"></div>
<div id="status">loading…</div>
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
// 2. ?tk= URL param — token carried in the shared link
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
// *endpoint* is resolved and fetched:
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
// means "read that global"; anything else is used as a URL — so both a global
// name and a plain URL are safe (no SyntaxError footgun).
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"poster-sections","type":"upg100-list","scope":"china-mongolia"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
var TOKEN_FREE = true;
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
// rather than this page's profile-config. With no baked token (the public copy), it must get
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
var NESTS_MAPS = false;
var TOKEN_SRC = "window.MAP_TOKEN_URL";
var BAKED_TOKEN = "";
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
var DEFAULT_TOKEN_URL = '/api/maps/token';
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
function askToken() {
if (!status) return;
status.className = 'err';
status.innerHTML = 'map needs a Mapbox token — ' +
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
var go = document.getElementById('tkGo');
if (go) go.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (!v) return;
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
mount(v);
});
}
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
mounted element does not cover shows it through, because every layer above body is
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
unpainted canvas as first supposed. Clearing it here (the one place we already know the
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
function done() {
if (status && status.parentNode) { status.parentNode.removeChild(status); }
document.documentElement.style.background = '#f3f3f1';
document.body.style.background = '#f3f3f1';
}
function mount(token) {
if (!token && !TOKEN_FREE) { return askToken(); }
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
try {
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
var el = document.createElement("poster-sections");
el.setAttribute('profile-config', JSON.stringify(cfg));
slot.innerHTML = '';
slot.appendChild(el);
done();
} catch (e) { fail(e && e.message ? e.message : String(e)); }
}
function qp(name) {
try { return new URLSearchParams(location.search).get(name) || ''; }
catch (e) { return ''; }
}
function extractToken(data) {
if (typeof data === 'string') return data.trim();
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
return '';
}
// Rungs 1-3b: an actual token → mount directly, no fetch.
// 2b = localStorage: the paste-once store shared with the staging pages
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
var stored = '';
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
var urlTk = qp('tk');
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
if (TOKEN_FREE) {
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
mount('');
// A token-free composite with no token anywhere: its content renders, but its nested maps
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
var bar = document.createElement('div');
bar.id = 'status';
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
document.body.appendChild(bar);
status = bar;
askToken();
var goBtn = document.getElementById('tkGo');
if (goBtn) goBtn.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (v) { location.reload(); }
});
}
return;
}
if (directToken) { return mount(directToken); }
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
// A single hard-coded pick used to kill the page when its one url 404'd or was
// blocked by CORS; a shared link must work wherever the tree is hosted.
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function fetchToken(i, errors) {
if (i >= candidates.length) {
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
askToken();
if (status) {
var d = document.createElement('div');
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
status.insertBefore(d, status.firstChild);
}
return;
}
var url = candidates[i];
return fetch(url)
.then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
var ct = r.headers.get('content-type') || '';
return ct.indexOf('json') !== -1 ? r.json() : r.text();
})
.then(function (data) {
var tk = extractToken(data);
if (!tk) throw new Error(url + ' → no token in response');
mount(tk);
})
.catch(function (e) {
errors.push(e && e.message ? e.message : String(e));
return fetchToken(i + 1, errors);
});
}
fetchToken(0, []);
})();
</script>
</body>
</html>

View file

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>UPG 100 posters — Europe · poster-sections</title>
<link rel="icon" href="data:,">
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
<style>
/* BARE full-page map — no chrome. The custom element IS the page, so the
browser URL shares the map itself.
HEIGHT IN svh, NOT inset:0. A fixed box with inset:0 follows the phone's DYNAMIC viewport, so the map
(and a legend sheet sized from it) changes height every time the address bar hides or shows. 100svh is
the viewport WITH the bar shown and never changes; when the bar hides, the page background shows below
instead of the map resizing. On a desktop svh = the viewport, so nothing moves there. The 100% before it
is the fallback for a browser without svh, which would otherwise size the box to its content.
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, poster-sections { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
padding: 24px; text-align: center; color: #9aa7b8;
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
#status.err { color: #f87171; }
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
Two things every generated leaf got wrong on paper, because no generated page has ever
carried an @media print rule (measured: 0 across the tree).
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
flooded black, and that is the reader who most wants the printout.
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
a full sheet reading "loading…" above the content that did in fact load. It is hidden
only while it is NOT an error — an error still prints, because a blank sheet with the
explanation suppressed is worse than a sheet that says what went wrong. */
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
then blank to the bottom of the sheet.
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
so height:auto with nothing else can leave the element a zero-height box and print an empty
page — a worse failure than the one being fixed, and the discriminator is not isMap
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
box for the map case while letting a taller document grow onto page two. */
#slot, poster-sections { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
}
</style>
</head>
<body>
<div id="slot"></div>
<div id="status">loading…</div>
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
// 2. ?tk= URL param — token carried in the shared link
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
// *endpoint* is resolved and fetched:
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
// means "read that global"; anything else is used as a URL — so both a global
// name and a plain URL are safe (no SyntaxError footgun).
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"poster-sections","type":"upg100-list","scope":"europe"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
var TOKEN_FREE = true;
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
// rather than this page's profile-config. With no baked token (the public copy), it must get
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
var NESTS_MAPS = false;
var TOKEN_SRC = "window.MAP_TOKEN_URL";
var BAKED_TOKEN = "";
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
var DEFAULT_TOKEN_URL = '/api/maps/token';
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
function askToken() {
if (!status) return;
status.className = 'err';
status.innerHTML = 'map needs a Mapbox token — ' +
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
var go = document.getElementById('tkGo');
if (go) go.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (!v) return;
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
mount(v);
});
}
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
mounted element does not cover shows it through, because every layer above body is
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
unpainted canvas as first supposed. Clearing it here (the one place we already know the
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
function done() {
if (status && status.parentNode) { status.parentNode.removeChild(status); }
document.documentElement.style.background = '#f3f3f1';
document.body.style.background = '#f3f3f1';
}
function mount(token) {
if (!token && !TOKEN_FREE) { return askToken(); }
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
try {
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
var el = document.createElement("poster-sections");
el.setAttribute('profile-config', JSON.stringify(cfg));
slot.innerHTML = '';
slot.appendChild(el);
done();
} catch (e) { fail(e && e.message ? e.message : String(e)); }
}
function qp(name) {
try { return new URLSearchParams(location.search).get(name) || ''; }
catch (e) { return ''; }
}
function extractToken(data) {
if (typeof data === 'string') return data.trim();
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
return '';
}
// Rungs 1-3b: an actual token → mount directly, no fetch.
// 2b = localStorage: the paste-once store shared with the staging pages
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
var stored = '';
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
var urlTk = qp('tk');
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
if (TOKEN_FREE) {
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
mount('');
// A token-free composite with no token anywhere: its content renders, but its nested maps
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
var bar = document.createElement('div');
bar.id = 'status';
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
document.body.appendChild(bar);
status = bar;
askToken();
var goBtn = document.getElementById('tkGo');
if (goBtn) goBtn.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (v) { location.reload(); }
});
}
return;
}
if (directToken) { return mount(directToken); }
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
// A single hard-coded pick used to kill the page when its one url 404'd or was
// blocked by CORS; a shared link must work wherever the tree is hosted.
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function fetchToken(i, errors) {
if (i >= candidates.length) {
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
askToken();
if (status) {
var d = document.createElement('div');
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
status.insertBefore(d, status.firstChild);
}
return;
}
var url = candidates[i];
return fetch(url)
.then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
var ct = r.headers.get('content-type') || '';
return ct.indexOf('json') !== -1 ? r.json() : r.text();
})
.then(function (data) {
var tk = extractToken(data);
if (!tk) throw new Error(url + ' → no token in response');
mount(tk);
})
.catch(function (e) {
errors.push(e && e.message ? e.message : String(e));
return fetchToken(i + 1, errors);
});
}
fetchToken(0, []);
})();
</script>
</body>
</html>

View file

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>UPG 100 posters — India · poster-sections</title>
<link rel="icon" href="data:,">
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
<style>
/* BARE full-page map — no chrome. The custom element IS the page, so the
browser URL shares the map itself.
HEIGHT IN svh, NOT inset:0. A fixed box with inset:0 follows the phone's DYNAMIC viewport, so the map
(and a legend sheet sized from it) changes height every time the address bar hides or shows. 100svh is
the viewport WITH the bar shown and never changes; when the bar hides, the page background shows below
instead of the map resizing. On a desktop svh = the viewport, so nothing moves there. The 100% before it
is the fallback for a browser without svh, which would otherwise size the box to its content.
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, poster-sections { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
padding: 24px; text-align: center; color: #9aa7b8;
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
#status.err { color: #f87171; }
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
Two things every generated leaf got wrong on paper, because no generated page has ever
carried an @media print rule (measured: 0 across the tree).
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
flooded black, and that is the reader who most wants the printout.
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
a full sheet reading "loading…" above the content that did in fact load. It is hidden
only while it is NOT an error — an error still prints, because a blank sheet with the
explanation suppressed is worse than a sheet that says what went wrong. */
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
then blank to the bottom of the sheet.
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
so height:auto with nothing else can leave the element a zero-height box and print an empty
page — a worse failure than the one being fixed, and the discriminator is not isMap
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
box for the map case while letting a taller document grow onto page two. */
#slot, poster-sections { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
}
</style>
</head>
<body>
<div id="slot"></div>
<div id="status">loading…</div>
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
// 2. ?tk= URL param — token carried in the shared link
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
// *endpoint* is resolved and fetched:
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
// means "read that global"; anything else is used as a URL — so both a global
// name and a plain URL are safe (no SyntaxError footgun).
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"poster-sections","type":"upg100-list","scope":"india"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
var TOKEN_FREE = true;
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
// rather than this page's profile-config. With no baked token (the public copy), it must get
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
var NESTS_MAPS = false;
var TOKEN_SRC = "window.MAP_TOKEN_URL";
var BAKED_TOKEN = "";
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
var DEFAULT_TOKEN_URL = '/api/maps/token';
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
function askToken() {
if (!status) return;
status.className = 'err';
status.innerHTML = 'map needs a Mapbox token — ' +
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
var go = document.getElementById('tkGo');
if (go) go.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (!v) return;
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
mount(v);
});
}
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
mounted element does not cover shows it through, because every layer above body is
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
unpainted canvas as first supposed. Clearing it here (the one place we already know the
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
function done() {
if (status && status.parentNode) { status.parentNode.removeChild(status); }
document.documentElement.style.background = '#f3f3f1';
document.body.style.background = '#f3f3f1';
}
function mount(token) {
if (!token && !TOKEN_FREE) { return askToken(); }
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
try {
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
var el = document.createElement("poster-sections");
el.setAttribute('profile-config', JSON.stringify(cfg));
slot.innerHTML = '';
slot.appendChild(el);
done();
} catch (e) { fail(e && e.message ? e.message : String(e)); }
}
function qp(name) {
try { return new URLSearchParams(location.search).get(name) || ''; }
catch (e) { return ''; }
}
function extractToken(data) {
if (typeof data === 'string') return data.trim();
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
return '';
}
// Rungs 1-3b: an actual token → mount directly, no fetch.
// 2b = localStorage: the paste-once store shared with the staging pages
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
var stored = '';
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
var urlTk = qp('tk');
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
if (TOKEN_FREE) {
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
mount('');
// A token-free composite with no token anywhere: its content renders, but its nested maps
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
var bar = document.createElement('div');
bar.id = 'status';
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
document.body.appendChild(bar);
status = bar;
askToken();
var goBtn = document.getElementById('tkGo');
if (goBtn) goBtn.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (v) { location.reload(); }
});
}
return;
}
if (directToken) { return mount(directToken); }
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
// A single hard-coded pick used to kill the page when its one url 404'd or was
// blocked by CORS; a shared link must work wherever the tree is hosted.
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function fetchToken(i, errors) {
if (i >= candidates.length) {
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
askToken();
if (status) {
var d = document.createElement('div');
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
status.insertBefore(d, status.firstChild);
}
return;
}
var url = candidates[i];
return fetch(url)
.then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
var ct = r.headers.get('content-type') || '';
return ct.indexOf('json') !== -1 ? r.json() : r.text();
})
.then(function (data) {
var tk = extractToken(data);
if (!tk) throw new Error(url + ' → no token in response');
mount(tk);
})
.catch(function (e) {
errors.push(e && e.message ? e.message : String(e));
return fetchToken(i + 1, errors);
});
}
fetchToken(0, []);
})();
</script>
</body>
</html>

View file

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>UPG 100 posters — Middle East · poster-sections</title>
<link rel="icon" href="data:,">
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
<style>
/* BARE full-page map — no chrome. The custom element IS the page, so the
browser URL shares the map itself.
HEIGHT IN svh, NOT inset:0. A fixed box with inset:0 follows the phone's DYNAMIC viewport, so the map
(and a legend sheet sized from it) changes height every time the address bar hides or shows. 100svh is
the viewport WITH the bar shown and never changes; when the bar hides, the page background shows below
instead of the map resizing. On a desktop svh = the viewport, so nothing moves there. The 100% before it
is the fallback for a browser without svh, which would otherwise size the box to its content.
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, poster-sections { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
padding: 24px; text-align: center; color: #9aa7b8;
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
#status.err { color: #f87171; }
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
Two things every generated leaf got wrong on paper, because no generated page has ever
carried an @media print rule (measured: 0 across the tree).
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
flooded black, and that is the reader who most wants the printout.
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
a full sheet reading "loading…" above the content that did in fact load. It is hidden
only while it is NOT an error — an error still prints, because a blank sheet with the
explanation suppressed is worse than a sheet that says what went wrong. */
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
then blank to the bottom of the sheet.
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
so height:auto with nothing else can leave the element a zero-height box and print an empty
page — a worse failure than the one being fixed, and the discriminator is not isMap
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
box for the map case while letting a taller document grow onto page two. */
#slot, poster-sections { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
}
</style>
</head>
<body>
<div id="slot"></div>
<div id="status">loading…</div>
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
// 2. ?tk= URL param — token carried in the shared link
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
// *endpoint* is resolved and fetched:
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
// means "read that global"; anything else is used as a URL — so both a global
// name and a plain URL are safe (no SyntaxError footgun).
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"poster-sections","type":"upg100-list","scope":"middle-east"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
var TOKEN_FREE = true;
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
// rather than this page's profile-config. With no baked token (the public copy), it must get
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
var NESTS_MAPS = false;
var TOKEN_SRC = "window.MAP_TOKEN_URL";
var BAKED_TOKEN = "";
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
var DEFAULT_TOKEN_URL = '/api/maps/token';
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
function askToken() {
if (!status) return;
status.className = 'err';
status.innerHTML = 'map needs a Mapbox token — ' +
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
var go = document.getElementById('tkGo');
if (go) go.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (!v) return;
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
mount(v);
});
}
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
mounted element does not cover shows it through, because every layer above body is
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
unpainted canvas as first supposed. Clearing it here (the one place we already know the
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
function done() {
if (status && status.parentNode) { status.parentNode.removeChild(status); }
document.documentElement.style.background = '#f3f3f1';
document.body.style.background = '#f3f3f1';
}
function mount(token) {
if (!token && !TOKEN_FREE) { return askToken(); }
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
try {
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
var el = document.createElement("poster-sections");
el.setAttribute('profile-config', JSON.stringify(cfg));
slot.innerHTML = '';
slot.appendChild(el);
done();
} catch (e) { fail(e && e.message ? e.message : String(e)); }
}
function qp(name) {
try { return new URLSearchParams(location.search).get(name) || ''; }
catch (e) { return ''; }
}
function extractToken(data) {
if (typeof data === 'string') return data.trim();
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
return '';
}
// Rungs 1-3b: an actual token → mount directly, no fetch.
// 2b = localStorage: the paste-once store shared with the staging pages
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
var stored = '';
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
var urlTk = qp('tk');
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
if (TOKEN_FREE) {
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
mount('');
// A token-free composite with no token anywhere: its content renders, but its nested maps
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
var bar = document.createElement('div');
bar.id = 'status';
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
document.body.appendChild(bar);
status = bar;
askToken();
var goBtn = document.getElementById('tkGo');
if (goBtn) goBtn.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (v) { location.reload(); }
});
}
return;
}
if (directToken) { return mount(directToken); }
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
// A single hard-coded pick used to kill the page when its one url 404'd or was
// blocked by CORS; a shared link must work wherever the tree is hosted.
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function fetchToken(i, errors) {
if (i >= candidates.length) {
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
askToken();
if (status) {
var d = document.createElement('div');
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
status.insertBefore(d, status.firstChild);
}
return;
}
var url = candidates[i];
return fetch(url)
.then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
var ct = r.headers.get('content-type') || '';
return ct.indexOf('json') !== -1 ? r.json() : r.text();
})
.then(function (data) {
var tk = extractToken(data);
if (!tk) throw new Error(url + ' → no token in response');
mount(tk);
})
.catch(function (e) {
errors.push(e && e.message ? e.message : String(e));
return fetchToken(i + 1, errors);
});
}
fetchToken(0, []);
})();
</script>
</body>
</html>

View file

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>UPG 100 posters — Southeast Asia + Oceania · poster-sections</title>
<link rel="icon" href="data:,">
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
<style>
/* BARE full-page map — no chrome. The custom element IS the page, so the
browser URL shares the map itself.
HEIGHT IN svh, NOT inset:0. A fixed box with inset:0 follows the phone's DYNAMIC viewport, so the map
(and a legend sheet sized from it) changes height every time the address bar hides or shows. 100svh is
the viewport WITH the bar shown and never changes; when the bar hides, the page background shows below
instead of the map resizing. On a desktop svh = the viewport, so nothing moves there. The 100% before it
is the fallback for a browser without svh, which would otherwise size the box to its content.
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, poster-sections { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
padding: 24px; text-align: center; color: #9aa7b8;
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
#status.err { color: #f87171; }
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
Two things every generated leaf got wrong on paper, because no generated page has ever
carried an @media print rule (measured: 0 across the tree).
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
flooded black, and that is the reader who most wants the printout.
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
a full sheet reading "loading…" above the content that did in fact load. It is hidden
only while it is NOT an error — an error still prints, because a blank sheet with the
explanation suppressed is worse than a sheet that says what went wrong. */
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
then blank to the bottom of the sheet.
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
so height:auto with nothing else can leave the element a zero-height box and print an empty
page — a worse failure than the one being fixed, and the discriminator is not isMap
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
box for the map case while letting a taller document grow onto page two. */
#slot, poster-sections { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
}
</style>
</head>
<body>
<div id="slot"></div>
<div id="status">loading…</div>
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
<script src="../../poster-sections.js" integrity="sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
// 2. ?tk= URL param — token carried in the shared link
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
// *endpoint* is resolved and fetched:
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
// means "read that global"; anything else is used as a URL — so both a global
// name and a plain URL are safe (no SyntaxError footgun).
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"poster-sections","type":"upg100-list","scope":"southeast-asia-oceania"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
var TOKEN_FREE = true;
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
// rather than this page's profile-config. With no baked token (the public copy), it must get
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
var NESTS_MAPS = false;
var TOKEN_SRC = "window.MAP_TOKEN_URL";
var BAKED_TOKEN = "";
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
var DEFAULT_TOKEN_URL = '/api/maps/token';
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
function askToken() {
if (!status) return;
status.className = 'err';
status.innerHTML = 'map needs a Mapbox token — ' +
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
var go = document.getElementById('tkGo');
if (go) go.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (!v) return;
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
mount(v);
});
}
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
mounted element does not cover shows it through, because every layer above body is
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
unpainted canvas as first supposed. Clearing it here (the one place we already know the
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
function done() {
if (status && status.parentNode) { status.parentNode.removeChild(status); }
document.documentElement.style.background = '#f3f3f1';
document.body.style.background = '#f3f3f1';
}
function mount(token) {
if (!token && !TOKEN_FREE) { return askToken(); }
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
try {
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
var el = document.createElement("poster-sections");
el.setAttribute('profile-config', JSON.stringify(cfg));
slot.innerHTML = '';
slot.appendChild(el);
done();
} catch (e) { fail(e && e.message ? e.message : String(e)); }
}
function qp(name) {
try { return new URLSearchParams(location.search).get(name) || ''; }
catch (e) { return ''; }
}
function extractToken(data) {
if (typeof data === 'string') return data.trim();
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
return '';
}
// Rungs 1-3b: an actual token → mount directly, no fetch.
// 2b = localStorage: the paste-once store shared with the staging pages
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
var stored = '';
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
var urlTk = qp('tk');
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
if (TOKEN_FREE) {
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
mount('');
// A token-free composite with no token anywhere: its content renders, but its nested maps
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
var bar = document.createElement('div');
bar.id = 'status';
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
document.body.appendChild(bar);
status = bar;
askToken();
var goBtn = document.getElementById('tkGo');
if (goBtn) goBtn.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (v) { location.reload(); }
});
}
return;
}
if (directToken) { return mount(directToken); }
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
// A single hard-coded pick used to kill the page when its one url 404'd or was
// blocked by CORS; a shared link must work wherever the tree is hosted.
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function fetchToken(i, errors) {
if (i >= candidates.length) {
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
askToken();
if (status) {
var d = document.createElement('div');
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
status.insertBefore(d, status.firstChild);
}
return;
}
var url = candidates[i];
return fetch(url)
.then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
var ct = r.headers.get('content-type') || '';
return ct.indexOf('json') !== -1 ? r.json() : r.text();
})
.then(function (data) {
var tk = extractToken(data);
if (!tk) throw new Error(url + ' → no token in response');
mount(tk);
})
.catch(function (e) {
errors.push(e && e.message ? e.message : String(e));
return fetchToken(i + 1, errors);
});
}
fetchToken(0, []);
})();
</script>
</body>
</html>