Compare commits
9 commits
main
...
poster-des
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bad61918ff | ||
|
|
e96b6fb4de | ||
|
|
7138bf61cf | ||
|
|
09cf034214 | ||
|
|
519e43d58b | ||
|
|
9793f8bd21 | ||
|
|
32d669deea | ||
|
|
1faf1c9d49 | ||
|
|
07b28e405e |
49 changed files with 3241 additions and 400 deletions
|
|
@ -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).
|
// directly (legend-tree's regionFor returns 'India' / 'China + Mongolia' / 'Other' without a helper call).
|
||||||
const TABLES = [
|
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',
|
produces: 'REGION_OF',
|
||||||
mapsFile: 'plugin-designs/my-upg-100-list/data/legend-tree.js',
|
mapsFile: 'plugin-designs/my-upg-100-list/data/legend-tree.js',
|
||||||
maps: 'REGION_KEY',
|
maps: 'REGION_KEY',
|
||||||
via: 'regionLabel',
|
via: 'regionTitle',
|
||||||
|
producerReturns: 'regionOf',
|
||||||
area: 'poster',
|
area: 'poster',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -87,6 +90,13 @@ function producerLabels(src, fn) {
|
||||||
const m = new RegExp(`const ${fn}\\s*=\\s*\\([^)]*\\)\\s*=>([\\s\\S]*?);\\n`).exec(src)
|
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
|
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. */
|
/** 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]))
|
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. */
|
/** 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 }
|
if (!got || !got.length) { ok(false, `${t.file}: no \`const ${t.producerFn} = (…) =>\` returning labels — this gate has gone blind`); continue }
|
||||||
fromFn = got
|
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 produced = new Set([...fromTable, ...fromCalls, ...fromFn])
|
||||||
const covered = keysOf(maps)
|
const covered = keysOf(maps)
|
||||||
|
|
||||||
|
|
|
||||||
84
internal/check-upg100-poster-type.mjs
Normal file
84
internal/check-upg100-poster-type.mjs
Normal 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">.')
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"intro": "اختر الملصقات وشاهدها أدناه، ثم انسخ الرمز لوضعها على أي موقع.",
|
"intro": "اختر الملصقات وشاهدها أدناه، ثم انسخ الرمز لوضعها على أي موقع.",
|
||||||
"type": "نوع الملصق",
|
"type": "نوع الملصق",
|
||||||
"typeCountry": "الدول",
|
"typeCountry": "الدول",
|
||||||
|
"typeUpg100List": "مناطق UPG-100",
|
||||||
"region": "المنطقة",
|
"region": "المنطقة",
|
||||||
"allRegions": "كل المناطق",
|
"allRegions": "كل المناطق",
|
||||||
"language": "اللغة",
|
"language": "اللغة",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"intro": "Wähle die Plakate, sieh sie dir unten an und kopiere dann den Code für eine beliebige Website.",
|
"intro": "Wähle die Plakate, sieh sie dir unten an und kopiere dann den Code für eine beliebige Website.",
|
||||||
"type": "Plakattyp",
|
"type": "Plakattyp",
|
||||||
"typeCountry": "Länder",
|
"typeCountry": "Länder",
|
||||||
|
"typeUpg100List": "UPG-100-Gebiete",
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
"allRegions": "Alle Regionen",
|
"allRegions": "Alle Regionen",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"intro": "Pick the posters, see them below, then copy the tag to put them on any website.",
|
"intro": "Pick the posters, see them below, then copy the tag to put them on any website.",
|
||||||
"type": "Poster type",
|
"type": "Poster type",
|
||||||
"typeCountry": "Countries",
|
"typeCountry": "Countries",
|
||||||
|
"typeUpg100List": "UPG-100 territories",
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
"allRegions": "All regions",
|
"allRegions": "All regions",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"intro": "Elige los carteles, míralos abajo y copia la etiqueta para ponerlos en cualquier sitio web.",
|
"intro": "Elige los carteles, míralos abajo y copia la etiqueta para ponerlos en cualquier sitio web.",
|
||||||
"type": "Tipo de cartel",
|
"type": "Tipo de cartel",
|
||||||
"typeCountry": "Países",
|
"typeCountry": "Países",
|
||||||
|
"typeUpg100List": "Territorios UPG-100",
|
||||||
"region": "Región",
|
"region": "Región",
|
||||||
"allRegions": "Todas las regiones",
|
"allRegions": "Todas las regiones",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"intro": "Choisissez les affiches, regardez-les ci-dessous, puis copiez la balise pour les placer sur n’importe quel site.",
|
"intro": "Choisissez les affiches, regardez-les ci-dessous, puis copiez la balise pour les placer sur n’importe quel site.",
|
||||||
"type": "Type d’affiche",
|
"type": "Type d’affiche",
|
||||||
"typeCountry": "Pays",
|
"typeCountry": "Pays",
|
||||||
|
"typeUpg100List": "Territoires UPG-100",
|
||||||
"region": "Région",
|
"region": "Région",
|
||||||
"allRegions": "Toutes les régions",
|
"allRegions": "Toutes les régions",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"intro": "पोस्टर चुनें, नीचे देखें, फिर किसी भी वेबसाइट पर लगाने के लिए टैग कॉपी करें।",
|
"intro": "पोस्टर चुनें, नीचे देखें, फिर किसी भी वेबसाइट पर लगाने के लिए टैग कॉपी करें।",
|
||||||
"type": "पोस्टर का प्रकार",
|
"type": "पोस्टर का प्रकार",
|
||||||
"typeCountry": "देश",
|
"typeCountry": "देश",
|
||||||
|
"typeUpg100List": "UPG-100 क्षेत्र",
|
||||||
"region": "क्षेत्र",
|
"region": "क्षेत्र",
|
||||||
"allRegions": "सभी क्षेत्र",
|
"allRegions": "सभी क्षेत्र",
|
||||||
"language": "भाषा",
|
"language": "भाषा",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"intro": "Scegli i poster, guardali qui sotto, poi copia il tag per metterli su qualsiasi sito.",
|
"intro": "Scegli i poster, guardali qui sotto, poi copia il tag per metterli su qualsiasi sito.",
|
||||||
"type": "Tipo di poster",
|
"type": "Tipo di poster",
|
||||||
"typeCountry": "Paesi",
|
"typeCountry": "Paesi",
|
||||||
|
"typeUpg100List": "Territori UPG-100",
|
||||||
"region": "Regione",
|
"region": "Regione",
|
||||||
"allRegions": "Tutte le regioni",
|
"allRegions": "Tutte le regioni",
|
||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"intro": "Escolha os cartazes, veja-os abaixo e copie a etiqueta para colocá-los em qualquer site.",
|
"intro": "Escolha os cartazes, veja-os abaixo e copie a etiqueta para colocá-los em qualquer site.",
|
||||||
"type": "Tipo de cartaz",
|
"type": "Tipo de cartaz",
|
||||||
"typeCountry": "Países",
|
"typeCountry": "Países",
|
||||||
|
"typeUpg100List": "Territórios UPG-100",
|
||||||
"region": "Região",
|
"region": "Região",
|
||||||
"allRegions": "Todas as regiões",
|
"allRegions": "Todas as regiões",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"intro": "Alege posterele, vezi-le mai jos, apoi copiază eticheta ca să le pui pe orice site.",
|
"intro": "Alege posterele, vezi-le mai jos, apoi copiază eticheta ca să le pui pe orice site.",
|
||||||
"type": "Tipul posterului",
|
"type": "Tipul posterului",
|
||||||
"typeCountry": "Țări",
|
"typeCountry": "Țări",
|
||||||
|
"typeUpg100List": "Teritorii UPG-100",
|
||||||
"region": "Regiune",
|
"region": "Regiune",
|
||||||
"allRegions": "Toate regiunile",
|
"allRegions": "Toate regiunile",
|
||||||
"language": "Limbă",
|
"language": "Limbă",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"intro": "Выберите плакаты, посмотрите их ниже, затем скопируйте тег, чтобы разместить их на любом сайте.",
|
"intro": "Выберите плакаты, посмотрите их ниже, затем скопируйте тег, чтобы разместить их на любом сайте.",
|
||||||
"type": "Тип плаката",
|
"type": "Тип плаката",
|
||||||
"typeCountry": "Страны",
|
"typeCountry": "Страны",
|
||||||
|
"typeUpg100List": "Территории UPG-100",
|
||||||
"region": "Регион",
|
"region": "Регион",
|
||||||
"allRegions": "Все регионы",
|
"allRegions": "Все регионы",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"intro": "选择海报,在下方查看,然后复制标签,放到任何网站上。",
|
"intro": "选择海报,在下方查看,然后复制标签,放到任何网站上。",
|
||||||
"type": "海报类型",
|
"type": "海报类型",
|
||||||
"typeCountry": "国家",
|
"typeCountry": "国家",
|
||||||
|
"typeUpg100List": "UPG-100 区域",
|
||||||
"region": "地区",
|
"region": "地区",
|
||||||
"allRegions": "所有地区",
|
"allRegions": "所有地区",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,7 @@
|
||||||
"check:list-pattern-doc": "node internal/check-list-pattern-doc.mjs",
|
"check:list-pattern-doc": "node internal/check-list-pattern-doc.mjs",
|
||||||
"check:pg-name-gate": "node internal/check-pg-name-gate.mjs",
|
"check:pg-name-gate": "node internal/check-pg-name-gate.mjs",
|
||||||
"check:pin-selection": "node internal/check-pin-selection.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:poster-action-print": "node internal/check-poster-action-print.mjs",
|
||||||
"check:popup-href-escaped": "node internal/check-popup-href-escaped.mjs",
|
"check:popup-href-escaped": "node internal/check-popup-href-escaped.mjs",
|
||||||
"check:popup-pan-controls": "node internal/check-popup-pan-clears-controls.mjs",
|
"check:popup-pan-controls": "node internal/check-popup-pan-clears-controls.mjs",
|
||||||
|
|
@ -355,7 +356,7 @@
|
||||||
"fix:sri": "node internal/fix-sri.mjs",
|
"fix:sri": "node internal/fix-sri.mjs",
|
||||||
"gallery": "node internal/generate-gallery.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: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",
|
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||||
"library-index": "node internal/generate-library-index.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: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: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: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: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: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.",
|
"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.",
|
||||||
|
|
|
||||||
62
plugin-designs/my-upg-100-list/data/upg100-poster-type.js
vendored
Normal file
62
plugin-designs/my-upg-100-list/data/upg100-poster-type.js
vendored
Normal 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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
61
plugin-designs/my-upg-100-list/data/upg100-regions.js
vendored
Normal file
61
plugin-designs/my-upg-100-list/data/upg100-regions.js
vendored
Normal 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 }
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { defineMapElement } from '@map/elements/defineMapElement.js'
|
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
|
* 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
|
export default Upg100ListMapElement
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
* its records provider.
|
* its records provider.
|
||||||
*
|
*
|
||||||
* WHAT COULD NOT BE SHARED, and why each is here rather than in the component:
|
* 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
|
* selector can be a wagf_block, a country_codes array or an explicit
|
||||||
* pg_slugs list and none of those implies a continent.
|
* pg_slugs list and none of those implies a continent.
|
||||||
* fetchListRecords — knows the people-groups API, the selector evaluator AND the pg_slugs
|
* fetchListRecords — knows the people-groups API, the selector evaluator AND the pg_slugs
|
||||||
|
|
@ -41,7 +41,6 @@
|
||||||
import { computed, inject, ref } from 'vue'
|
import { computed, inject, ref } from 'vue'
|
||||||
import { useOptionalT, useOptionalI18n } from '@map/composables/useOptionalT.js'
|
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.
|
// 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 PosterWall from '@map/components/poster/PosterWall.vue'
|
||||||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||||
// The reader's language, for the DATA fetch below. Not for interface text — that goes through t().
|
// 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).
|
// 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 visionFunnel from '../../data/people-groups/vision-funnel.json'
|
||||||
import { upgTotal, countInSentence } from './data/upg-total.js'
|
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
|
// 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.
|
// /people-groups/list payload + a list's `selector` into the exact rows that belong to it.
|
||||||
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
|
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
|
||||||
import { makeZoneSheet } from './data/zone-sheet-print.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'
|
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'
|
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
|
// 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
|
|| import.meta.env.VITE_API_BASE_URL
|
||||||
|| ''
|
|| ''
|
||||||
|
|
||||||
// ─── REGIONAL SECTIONS ───────────────────────────────────────────────────────
|
// ─── REGIONAL SECTIONS: data/upg100-regions.js (moved there so the 'upg100-list' poster type reads the same table) ─
|
||||||
// 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.
|
|
||||||
}
|
|
||||||
// ⚠ TAKES AN ID, while the shared wall's groupBy takes the whole ITEM. The template
|
// ⚠ 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
|
// 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
|
// 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
|
// 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
|
// (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.
|
// heading has a poster.region_* key in all 11 locales, India and Other included.
|
||||||
const regionLabel = (english) => {
|
const regionFor = (id) => regionTitle(regionOf(id), t)
|
||||||
const key = REGION_KEY[english]
|
// ★ COLLAPSIBLE, LIKE COUNTRY POSTERS (each region section is the shared PosterSection, through the wall). Every
|
||||||
return key ? t(`poster.${key}`) : english // no key yet -> the English heading, not a raw key
|
// 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 regionFor = (id) => {
|
const allOpen = () => false // no key yet -> the English heading, not a raw key
|
||||||
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')
|
|
||||||
}
|
|
||||||
// One flat list of the zones the map paints — for WHICHEVER list is showing.
|
// 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
|
// ★ 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.
|
// this list IS, and the old comment was describing the wrong thing.
|
||||||
const { locale: wallLocale } = useOptionalI18n('')
|
const { locale: wallLocale } = useOptionalI18n('')
|
||||||
const items = computed(() => {
|
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()
|
// 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).
|
// 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 l = wallLocale && (wallLocale.value ?? wallLocale)
|
||||||
const locale = (typeof l === 'string' && l) || getRuntimeLocale() || detectLocale()
|
const locale = (typeof l === 'string' && l) || getRuntimeLocale() || detectLocale()
|
||||||
return [
|
// ★ THE CARD'S NAME IN THE READER'S LANGUAGE (item.label is the Show-on-map button's accessible name, every
|
||||||
...(z.lists || []),
|
// poster's alt text and tooltip, the print HUD's title and the printed documents' title). The zones and their
|
||||||
...(z.asia_lists || []),
|
// names come from the 'upg100-list' poster type's own function, so this wall and <dxm-poster-section
|
||||||
...(z.uncovered_lists || []),
|
// type="upg100-list"> can never show different cards.
|
||||||
].filter(l => l && l.id)
|
return upg100Zones(profileConfig, locale)
|
||||||
// ★ 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 RECORDS PROVIDER — now the library's (lists/peopleGroupRecords.js) ─────────
|
// ─── 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"
|
download-prefix="doxa-upg-100"
|
||||||
:columns-control="true"
|
:columns-control="true"
|
||||||
:min-card-px="150"
|
: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
|
<!-- 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,
|
390 and silently overrode the select, which read 2 (2026-09-30: work like the country posters,
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,78 @@
|
||||||
"scope": "oceania"
|
"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",
|
"id": "configure",
|
||||||
"title": "Configure — make a new preset",
|
"title": "Configure — make a new preset",
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,14 @@
|
||||||
* making new presets. Nothing here draws a poster: the sections ARE the element (registered by index.js), and Copy
|
* 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).
|
* 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 { useOptionalI18n } from '@map/composables/useOptionalT.js'
|
||||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||||
import { SUPPORTED_LOCALES, RTL_LOCALES, getRuntimeLocale, detectLocale } from '@map/i18n/index.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 { absoluteEmbedUrl, resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||||
import { TAG, sectionAttrs, embedSnippet } from './src/embed.js'
|
import { TAG, sectionAttrs, embedSnippet } from './src/embed.js'
|
||||||
|
import { TYPE_SCRIPTS } from './src/typeScripts.js'
|
||||||
|
|
||||||
const profileConfig = inject('profileConfig', ref(null))
|
const profileConfig = inject('profileConfig', ref(null))
|
||||||
const cfg = computed(() => profileConfig.value || {})
|
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 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
|
// 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.
|
// catalogue has a name for it, else by its registry name.
|
||||||
const types = ref(posterTypeNames())
|
// Plus the types that load on demand (src/typeScripts.js), so the picker offers them before their script has run.
|
||||||
const refreshTypes = () => { const now = posterTypeNames(); if (now.join() !== types.value.join()) types.value = now }
|
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 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)
|
const v = t(key)
|
||||||
return v && v !== key ? v : name
|
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 }
|
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.
|
// The regions the chosen type offers (its adapter's scopes), each named by the type's own section title.
|
||||||
const adapter = ref(null)
|
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 scopes = computed(() => {
|
||||||
const a = adapter.value
|
const a = adapter.value
|
||||||
const list = a && typeof a.scopes === 'function' ? a.scopes() : []
|
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),
|
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,
|
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
|
// 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).
|
// mounts a fresh section (a language change rebuilds its catalogue rather than patching it).
|
||||||
|
|
|
||||||
|
|
@ -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()
|
const SUPPORTED_LOCALES = readdirSync(LOC).filter((d) => statSync(join(LOC, d)).isDirectory()).sort()
|
||||||
import { posterSectionConfig } from '../../../library/elements/posterSectionConfig.js'
|
import { posterSectionConfig } from '../../../library/elements/posterSectionConfig.js'
|
||||||
import * as real from '../src/embed.js'
|
import * as real from '../src/embed.js'
|
||||||
|
import { TYPE_SCRIPTS } from '../src/typeScripts.js'
|
||||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../..')
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../..')
|
||||||
const PRESETS = JSON.parse(readFileSync(join(ROOT, 'plugin-designs/poster-sections/poster-sections.instances.json'), 'utf8'))
|
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())]
|
// 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
|
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.
|
// The built preset pages, when a tree is built: every folder under poster-sections/poster-sections/ that holds a page.
|
||||||
function builtPages() {
|
function builtPages() {
|
||||||
const pub = join(ROOT, 'public')
|
const pub = join(ROOT, 'public')
|
||||||
|
|
@ -113,6 +128,9 @@ if (process.argv.includes('--self-test')) {
|
||||||
}
|
}
|
||||||
let ok = true
|
let ok = true
|
||||||
const stale = { ...PRESETS, instances: [...PRESETS.instances, { id: 'countries-atlantis', props: { type: 'country', scope: 'atlantis' } }] }
|
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'])
|
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)'}`)
|
console.log(` mutant "a built page for a preset that no longer exists": ${pg.length ? 'FAILS (good)' : 'PASSES (bad)'}`)
|
||||||
if (!pg.length) ok = false
|
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')
|
console.log(ok ? 'POSTER-SECTIONS SELF-TEST: PASS — every mutant fails the check.' : 'POSTER-SECTIONS SELF-TEST: FAIL')
|
||||||
process.exit(ok ? 0 : 1)
|
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(', ')
|
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) }
|
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}).`)
|
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}).`)
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,12 @@ export function sectionAttrs(choices) {
|
||||||
|
|
||||||
const esc = (v) => String(v).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<')
|
const esc = (v) => String(v).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<')
|
||||||
|
|
||||||
/** 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}>`)
|
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')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
plugin-designs/poster-sections/src/typeScripts.js
Normal file
9
plugin-designs/poster-sections/src/typeScripts.js
Normal 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',
|
||||||
|
}
|
||||||
|
|
@ -929,7 +929,7 @@
|
||||||
"bundle": "my-upg-100-list",
|
"bundle": "my-upg-100-list",
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"output": "dx-maps/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": [
|
"elements": [
|
||||||
"my-upg-100-list-map"
|
"my-upg-100-list-map"
|
||||||
],
|
],
|
||||||
|
|
@ -952,7 +952,7 @@
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"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>",
|
"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",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"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>",
|
"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>",
|
"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-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>"
|
"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",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"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>",
|
"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>",
|
"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-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>"
|
"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",
|
"bundle": "poster-sections",
|
||||||
"scriptUrl": "./poster-sections/poster-sections.js",
|
"scriptUrl": "./poster-sections/poster-sections.js",
|
||||||
"output": "dx-maps/poster-sections/poster-sections.js",
|
"output": "dx-maps/poster-sections/poster-sections.js",
|
||||||
"integrity": "sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD",
|
"integrity": "sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM",
|
||||||
"elements": [
|
"elements": [
|
||||||
"poster-sections"
|
"poster-sections"
|
||||||
],
|
],
|
||||||
|
|
@ -1200,7 +1200,7 @@
|
||||||
"scriptUrl": "./poster-sections/poster-sections.js",
|
"scriptUrl": "./poster-sections/poster-sections.js",
|
||||||
"embed": {
|
"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>",
|
"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": [
|
"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>"
|
"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",
|
"id": "configure",
|
||||||
"title": "Configure — make a new preset",
|
"title": "Configure — make a new preset",
|
||||||
|
|
|
||||||
|
|
@ -929,7 +929,7 @@
|
||||||
"bundle": "my-upg-100-list",
|
"bundle": "my-upg-100-list",
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"output": "dx-maps/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": [
|
"elements": [
|
||||||
"my-upg-100-list-map"
|
"my-upg-100-list-map"
|
||||||
],
|
],
|
||||||
|
|
@ -952,7 +952,7 @@
|
||||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"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>",
|
"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",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"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>",
|
"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>",
|
"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-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>"
|
"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",
|
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||||
"embed": {
|
"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>",
|
"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>",
|
"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-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>"
|
"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",
|
"bundle": "poster-sections",
|
||||||
"scriptUrl": "./poster-sections/poster-sections.js",
|
"scriptUrl": "./poster-sections/poster-sections.js",
|
||||||
"output": "dx-maps/poster-sections/poster-sections.js",
|
"output": "dx-maps/poster-sections/poster-sections.js",
|
||||||
"integrity": "sha384-pOuNSxS6NxmK0yMui0WNKjdjeW/3X2mKfcYo8KRfL2NRnDxOohwJc7BqJx5t9eDD",
|
"integrity": "sha384-BuMNn28GhZpbVsoqnpIIObw/1JGVU0l8BOO6244oX0IaxeneyNC48jQ2ht63M5aM",
|
||||||
"elements": [
|
"elements": [
|
||||||
"poster-sections"
|
"poster-sections"
|
||||||
],
|
],
|
||||||
|
|
@ -1200,7 +1200,7 @@
|
||||||
"scriptUrl": "./poster-sections/poster-sections.js",
|
"scriptUrl": "./poster-sections/poster-sections.js",
|
||||||
"embed": {
|
"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>",
|
"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": [
|
"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>"
|
"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",
|
"id": "configure",
|
||||||
"title": "Configure — make a new preset",
|
"title": "Configure — make a new preset",
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||||
<!-- Bundle IIFE — registers the <poster-sections> custom element. -->
|
<!-- 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>
|
<script>
|
||||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in a new issue