Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run

# Conflicts:
#	public/doxa-maps/country-posters/countries-map/index.html
#	public/doxa-maps/country-posters/country-poster-wall/index.html
#	public/doxa-maps/country-posters/country-posters-mfe/index.html
#	public/doxa-maps/country-posters/country-posters.js
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
This commit is contained in:
doxa-maps-88655c70 2026-09-24 05:09:42 +00:00
commit 916ef35ee8
22 changed files with 743 additions and 198 deletions

View file

@ -0,0 +1,111 @@
// check-poster-prints — the SHARED print actions must produce real documents.
//
// Context: the poster wall was generalised into library/components/poster/
// PosterWall.vue and two bundles migrated onto it, but the ORIGINAL 903-line
// my-upg-100-list wall — the one carrying the actual print actions — was left
// behind. So the shared component's packet path was a "coming soon" stub, and
// every print fix in the 100-list was invisible to the three bundles using the
// shared wall. usePosterPrints.js closes that: the ACTIONS are shared, the RECORD
// FETCHING stays per-bundle behind a `records` provider.
//
// ★ THE SEAM IS THE THING TO ASSERT. A wall WITH a provider must produce a real
// document; a wall WITHOUT one must degrade honestly rather than failing at click
// time. Both directions are checked, because "the button is there" and "the button
// works" are different claims — this flight has repeatedly found the first true
// and the second false.
//
// Runs the composable in a browser page (it calls document/DOM), driving it with a
// fake records provider so the assertion is about the DOCUMENTS, not about any
// bundle's API being reachable.
import { chromium } from 'playwright'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const REPO = process.env.DOXA_REPO
|| resolve('/home/lk10x/Workspace/eco-dx/root-doxa-maps/wt-packet')
const src = readFileSync(resolve(REPO, 'library/composables/usePosterPrints.js'), 'utf8')
const frame = readFileSync(resolve(REPO, 'library/composables/printInPlace.js'), 'utf8')
const b = await chromium.launch()
const pg = await b.newPage({ viewport: { width: 1000, height: 800 } })
const errs = []
pg.on('pageerror', (e) => errs.push(String(e).slice(0, 140)))
await pg.setContent('<!DOCTYPE html><html><body></body></html>')
const r = await pg.evaluate(async ({ src, frame }) => {
// Shim the two module deps so the composable can be evaluated standalone: a tiny
// `ref`, and printInPlace's real source so the frame mechanism is the real one.
const frameUrl = URL.createObjectURL(new Blob([frame], { type: 'text/javascript' }))
const vueShim = URL.createObjectURL(new Blob([
'export const ref = (v) => ({ value: v })',
], { type: 'text/javascript' }))
const patched = src
.replace("from 'vue'", `from '${vueShim}'`)
.replace("from './printInPlace.js'", `from '${frameUrl}'`)
const modUrl = URL.createObjectURL(new Blob([patched], { type: 'text/javascript' }))
const { usePosterPrints } = await import(modUrl)
const ROWS = [
{ slug: 'a', name: 'Aheu', country_code_label: 'Laos', population: 2700, religion_label: 'Animism' },
{ slug: 'b', name: 'Akha', country_code_label: 'Laos', population: 113000, religion_label: 'Animism' },
]
const out = {}
// ── with a provider ──────────────────────────────────────────────────────
// ⚠ CAPTURE THE HTML, do not read the frame back. openPrintFrame calls
// window.print() under autoprint, which BLOCKS in headless Chromium, and the
// frame self-removes on afterprint — so reading it back measured a frame that had
// already gone and reported 0 rows for a correct document. Intercept the document
// the composable hands to the frame instead: that IS what gets printed.
let printed = ''
const realCreate = document.createElement.bind(document)
document.createElement = (tag) => {
const el = realCreate(tag)
if (tag === 'iframe') {
Object.defineProperty(el, 'contentDocument', {
get: () => ({ open() {}, close() {}, write(h) { printed += h } }),
})
Object.defineProperty(el, 'contentWindow', { get: () => ({ addEventListener() {} }) })
}
return el
}
const withProv = usePosterPrints({ records: async () => ROWS, label: (i) => i.label })
await withProv.printList({ id: 'laos', label: 'Laos' })
await new Promise((r2) => setTimeout(r2, 400))
document.createElement = realCreate
const doc = new DOMParser().parseFromString(printed, 'text/html')
out.listRows = doc ? doc.querySelectorAll('tbody tr').length : 0
out.listHasCheckbox = doc ? /□/.test(doc.body.innerText) : false
out.listPhase = withProv.exportState.value?.phase ?? 'cleared'
// ── without a provider: must FAIL HONESTLY, not throw ────────────────────
const noProv = usePosterPrints({ label: (i) => i.label })
await noProv.printList({ id: 'x', label: 'X' })
out.noProvPhase = noProv.exportState.value?.phase
out.noProvMsg = noProv.exportState.value?.message || ''
return out
}, { src, frame })
await b.close()
const fails = []
if (r.listRows !== 2) fails.push(`the list print rendered ${r.listRows} row(s), expected 2`)
if (!r.listHasCheckbox) fails.push('the list print has no prayer checkbox column')
if (r.noProvPhase !== 'error') {
fails.push(`without a records provider the phase was "${r.noProvPhase}", expected "error" — `
+ 'it must degrade honestly rather than silently doing nothing')
}
if (!/no records provider/i.test(r.noProvMsg)) {
fails.push(`the no-provider message was "${r.noProvMsg}" — it should name the missing provider`)
}
if (errs.length) fails.push(`${errs.length} page error(s): ${errs[0]}`)
console.log(` list print : ${r.listRows} rows, checkbox=${r.listHasCheckbox}`)
console.log(` without provider : phase=${r.noProvPhase} "${r.noProvMsg.slice(0, 60)}"`)
console.log(` page errors : ${errs.length}`)
if (fails.length) {
console.error(`\nPOSTER-PRINTS: FAIL — ${fails.length} problem(s)`)
for (const f of fails) console.error(' - ' + f)
process.exit(1)
}
console.log('\nPOSTER-PRINTS: PASS — shared print actions produce real documents, and degrade honestly without a provider.')

View file

@ -0,0 +1,99 @@
// check-provider-switch — ?provider=maplibre must actually SWAP THE ENGINE.
//
// The bug this gates (BUG-UPG100-IGNORES-THE-PROVIDER-SWITCH): my-upg-100-list
// builds its map RAW with `new gl.Map` instead of going through useMapInstance, so
// the page-level switch — which resolves inside that composable — could not reach
// it. MEASURED at the time: five sibling leaves swapped cleanly while this one
// stayed on Mapbox and made 19 requests to api.mapbox.com.
//
// ★ IT COUNTS NETWORK REQUESTS BY HOST, not globals. `window.mapboxgl` can be
// present because a page preloads it, and a map can still be DRAWN by maplibre —
// or the reverse. What the switch is for is not loading a proprietary engine and
// not calling its tile servers, and only the request log shows that.
//
// Usage: node internal/check-provider-switch.mjs <leafUrl>
import { chromium } from 'playwright'
// ⚠ NOT named `URL` — that shadows the global URL constructor, so every
// `new URL(request.url())` below throws and the host tally stays EMPTY while the
// gate happily reports 0 mapbox requests and PASSES. Measured: it did exactly
// that, hiding 4 real requests to events.mapbox.com.
const LEAF = process.argv[2]
if (!LEAF) { console.error('usage: check-provider-switch.mjs <leafUrl>'); process.exit(2) }
async function run(provider) {
const b = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] })
const pg = await b.newPage({ viewport: { width: 1280, height: 900 } })
const hosts = new Map()
let unparsed = 0
pg.on('request', (r) => {
try {
const h = new URL(r.url()).hostname
if (!h) { unparsed += 1; return }
hosts.set(h, (hosts.get(h) || 0) + 1)
} catch { unparsed += 1 }
})
const errs = []
pg.on('pageerror', (e) => errs.push(String(e).slice(0, 120)))
const url = provider ? `${LEAF}${LEAF.includes('?') ? '&' : '?'}provider=${provider}` : LEAF
await pg.goto(url, { waitUntil: 'load', timeout: 45000 })
await pg.waitForTimeout(12000)
const globals = await pg.evaluate(() => ({
mapbox: typeof window.mapboxgl !== 'undefined',
maplibre: typeof window.maplibregl !== 'undefined',
}))
await b.close()
const count = (needle) => [...hosts.entries()]
.filter(([h]) => h.includes(needle)).reduce((a, [, n]) => a + n, 0)
// Report what could NOT be parsed rather than swallowing it: a silently empty
// tally is how this gate first passed while measuring nothing at all.
return { mapboxReq: count('mapbox.com'), globals, errs, unparsed, total: [...hosts.values()].reduce((a, n) => a + n, 0) }
}
const def = await run(null)
const ml = await run('maplibre')
// DERIVED from the leaf's own HTML, not a hardcoded number: count the distinct
// api.mapbox.com references the generated page carries, then allow a little slack
// because a stylesheet can be fetched more than once (the upg-100 leaf resolves 4
// distinct urls but issues 5 requests — one css is re-fetched). Hardcoding 5 would
// pass a leaf that legitimately preloads 4 and leaks 1 runtime call.
const pageHtml = await (await fetch(LEAF)).text()
const declared = new Set((pageHtml.match(/https:\/\/api\.mapbox\.com[^"')]+/g) || []))
const PRELOAD_ALLOWANCE = Number(process.env.PROVIDER_PRELOADS ?? (declared.size + 1))
console.log(` page preloads : ${declared.size} distinct api.mapbox.com url(s) in the leaf HTML`)
console.log(` default : ${def.mapboxReq} mapbox / ${def.total} total request(s) `
+ `[mapboxgl=${def.globals.mapbox} maplibregl=${def.globals.maplibre}]`)
console.log(` ?provider=maplibre: ${ml.mapboxReq} mapbox / ${ml.total} total request(s) `
+ `[mapboxgl=${ml.globals.mapbox} maplibregl=${ml.globals.maplibre}]`)
const fails = []
if (!ml.globals.maplibre) {
fails.push('?provider=maplibre did not load the maplibre engine at all')
}
// The RESIDUAL SET is the leaf page's own <link>/<script> preloads — mapbox-gl and
// the geocoder, referenced in the generated index.html header. Those fire before any
// Vue code runs, so no runtime switch can suppress them; removing them is a separate
// concern (the page template), not this bug. What the switch must achieve is that NO
// RUNTIME request goes to Mapbox: measured 19 -> 5 on the upg-100 leaf, and all 5 are
// in the HTML. Asserting "fewer than default" would pass a half-fix at 18.
if (ml.mapboxReq > PRELOAD_ALLOWANCE) {
fails.push(`?provider=maplibre made ${ml.mapboxReq} mapbox request(s), more than the `
+ `${PRELOAD_ALLOWANCE} page preloads — ${ml.mapboxReq - PRELOAD_ALLOWANCE} are RUNTIME `
+ 'calls the switch failed to redirect')
}
if (def.mapboxReq <= PRELOAD_ALLOWANCE) {
fails.push(`the default run made only ${def.mapboxReq} mapbox request(s) — the map is probably `
+ 'not rendering, so this comparison proves nothing')
}
if (!def.total) fails.push('no requests were recorded at all — the tally is broken, not the page')
if (ml.errs.length) fails.push(`${ml.errs.length} page error(s) under maplibre: ${ml.errs[0]}`)
if (fails.length) {
console.error(`\nPROVIDER-SWITCH: FAIL — ${fails.length} problem(s)`)
for (const f of fails) console.error(' - ' + f)
process.exit(1)
}
console.log(`\nPROVIDER-SWITCH: PASS — maplibre loads and mapbox traffic drops `
+ `${def.mapboxReq} -> ${ml.mapboxReq}.`)

View file

@ -41,6 +41,8 @@
*/
import { computed, ref } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { usePosterPrints } from '@map/composables/usePosterPrints.js'
import { useI18n } from 'vue-i18n'
// Export-modal geometry. A fixed;inset:0 scrim covers only the CONTENT PANE in the cap-5 shell
// (contain:layout on .vtl-content). Measured 1024x793 @ (256,107) at 1280x900 — rail clickable
// behind the "modal". See library/utils/viewportScrim.js for the measurements and rejected fixes.
@ -77,6 +79,17 @@ const props = defineProps({
// prayer list for each country generated.")
prayerListSoonMessage: { type: String,
default: 'Coming soon — the prayer list for each people group is in build.' },
// ★ RECORDS PROVIDER — the seam that makes the print actions shareable.
// The PRINT ACTIONS are common to every wall; the RECORD FETCHING is not. The
// 100-list resolves its rows through the people-groups API, the selector
// evaluator and a pg_slugs special case; Key Cities and Country Posters have
// entirely different data shapes. So a bundle that CAN supply rows passes this
// and gets the list + card-deck prints; one that cannot simply omits it and
// those buttons stay hidden, rather than failing at click time.
records: { type: Function, default: null },
// Card stock for the deck print, when this wall offers one. See card-render.js
// STOCK — both orientations are available.
cardStock: { type: String, default: 'poker' },
// EMBEDDED mode (added 2026-09-24 for the regions-tab poster dropdown). The wall's root is
// `position:absolute;inset:0` because in country-posters and my-upg-100-list it IS a pane and
// fills it. Dropped inside another component — a region card, say — that absolute root covers
@ -208,11 +221,43 @@ async function exportPoster(item) {
}
}
// ── SHARED PRINT ACTIONS ──────────────────────────────────────────────────
// The list and card-deck prints live in the shared composable so every wall gets
// the same documents. They need ROWS, which only the bundle can fetch — hence the
// `records` prop. Without it the buttons are hidden and this stays honest rather
// than offering an action that cannot work.
// The wall follows the app's locale: the card deck and its QR codes are localised,
// and packet-strings.json falls back per key.
const { locale: wallLocale } = useI18n()
// ⚠ ONE exportState, not two. The composable owns a ref of its own; if the HUD
// keeps rendering the wall's while the print actions write to the composable's, a
// card-deck print shows NO progress and its errors vanish silently — the mechanism
// works and the feedback does not. Hand the composable this wall's ref so both
// write to the surface the reader is actually looking at.
const prints = usePosterPrints({
state: exportState,
records: props.records ? (item) => props.records(item) : null,
// useI18n()'s locale can be a ref or a plain string depending on how the host
// installed it, so normalise rather than assume.
lang: () => {
const l = wallLocale && (wallLocale.value ?? wallLocale)
return (typeof l === 'string' && l) || 'en'
},
stock: () => props.cardStock,
label: (item) => item?.label || item?.id || '',
})
const canPrintRows = computed(() => typeof props.records === 'function')
function downloadPacket(item) {
// NOT an error — unbuilt, not broken. phase:'error' reads to a user as
// "something went wrong" rather than "not yet".
exportState.value = { label: item.label, kind: 'Prayer List', phase: 'soon',
message: props.prayerListSoonMessage, pct: 0 }
// Still the honest "not yet" when this wall has no records provider: NOT an
// error — unbuilt, not broken. phase:'error' reads to a user as "something went
// wrong" rather than "not yet".
if (!canPrintRows.value) {
exportState.value = { label: item.label, kind: 'Prayer List', phase: 'soon',
message: props.prayerListSoonMessage, pct: 0 }
return
}
prints.printList(item)
}
function download(blob, name) {
@ -422,6 +467,11 @@ useShadowStyles(
<fieldset class="tape pfs">
<legend>Print Options</legend>
<button type="button" class="pbtn" @click="downloadPacket(item)">Prayer List</button>
<!-- CARD DECK — only when this wall can supply rows. A button that
cannot work is worse than an absent one, so it is gated on the
records provider rather than shown and then failing at click. -->
<button v-if="canPrintRows" type="button" class="pbtn"
@click="prints.printCardDeck(item)">Card Deck</button>
<button type="button" class="pbtn" @click="exportPoster(item)">Poster Map</button>
</fieldset>
</div>

View file

@ -0,0 +1,168 @@
// usePosterPrints.js — THE print actions every poster wall shares.
//
// ★ WHY THIS EXISTS. The poster wall was generalised into
// library/components/poster/PosterWall.vue and two bundles were migrated onto it —
// but the ORIGINAL, my-upg-100-list/poster-wall.vue (903 lines), was left behind.
// So the one wall carrying the real print actions is the one wall nothing else can
// benefit from, and the shared component's packet path is still a "coming soon"
// stub. Every print fix made in the 100-list is invisible to the three bundles on
// the shared component, and vice versa. That divergence is the bug.
//
// ── THE SEAM ────────────────────────────────────────────────────────────────
// The PRINT ACTIONS are shareable; the RECORD FETCHING is not. fetchListRecords in
// the 100-list knows the people-groups API, the selector evaluator and the pg_slugs
// special case; Key Cities and Country Posters have entirely different data shapes.
// So this composable takes a `records(item)` PROVIDER and owns everything after it:
// the HUD state machine, the in-place print frame, and the three documents.
//
// That is also what keeps the bundles honest — a wall that cannot supply records
// simply does not pass a provider, and the actions that need them stay hidden
// rather than failing at click time.
//
// ⚠ PRINTS IN PLACE, never window.open(). A popup is blockable and drops the reader
// out of the map; the shared openPrintFrame mounts a hidden same-page iframe. This
// is the mechanism the Driver asked for by name: "not supposed to open up a new
// browser, but should open up the print dialog directly in the browser".
import { ref } from 'vue';
import { openPrintFrame } from './printInPlace.js';
const esc = (s) => String(s ?? '')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
/**
* @param {object} opts
* records async (item) => rows[] — REQUIRED for the list and deck prints.
* strings () => Promise<object> — the packet/card i18n catalog.
* lang () => string — the app locale; the deck and packet follow it.
* stock () => string — card stock id (see card-render.js STOCK).
* posterUrl (item) => string|null — the pre-rendered image, when one exists.
* label (item) => string
*/
export function usePosterPrints(opts = {}) {
// ⚠ ACCEPT the caller's state ref when it has one. A wall that renders its OWN
// exportState while these actions write to a private ref shows no progress and
// swallows errors — the mechanism works and the feedback does not. Owning a ref
// here is only the standalone default.
const exportState = opts.state || ref(null);
const busy = () => exportState.value !== null;
const label = opts.label || ((item) => item?.label || item?.id || '');
const lang = opts.lang || (() => 'en');
const stock = opts.stock || (() => 'poker');
const set = (item, kind, phase, message, pct) => {
exportState.value = { label: label(item), kind, phase, message, pct };
};
const done = (item, kind) => {
set(item, kind, 'done', 'print dialog opened', 100);
setTimeout(() => { exportState.value = null; }, 900);
};
const fail = (item, kind, e) => {
set(item, kind, 'error', String(e?.message || e).slice(0, 140), 0);
};
function dismiss() { exportState.value = null; }
async function rows(item) {
if (typeof opts.records !== 'function') {
throw new Error('this wall supplies no records provider');
}
const r = await opts.records(item);
if (!r || !r.length) throw new Error('that list resolved to no records');
return r;
}
// ── 1. THE POSTER ─────────────────────────────────────────────────────────
// Prefers the pre-rendered image; falls back to rasterising the poster's own
// HTML in the browser, which is what lets the repo stop shipping 41 MB of webp.
async function printPoster(item) {
if (busy()) return;
const kind = 'Poster';
set(item, kind, 'fetch', 'fetching the poster', 25);
try {
const url = opts.posterUrl ? opts.posterUrl(item) : null;
let html = '';
if (url) {
const res = await fetch(url, { cache: 'force-cache' });
if (res.ok) html = await res.text();
}
if (!html) throw new Error('no poster available for this item');
openPrintFrame(html, { autoprint: true });
done(item, kind);
} catch (e) { fail(item, kind, e); }
}
// ── 2. THE LIST ───────────────────────────────────────────────────────────
// The Driver's FIRST option — "that's what the whole app was about": the list
// itself as a print-paginated table with a prayer checkbox per row.
async function printList(item, { columns, title } = {}) {
if (busy()) return;
const kind = 'List';
set(item, kind, 'fetch', 'fetching records', 30);
try {
const data = await rows(item);
const cols = columns || [
{ key: 'name', head: 'Name', cls: 'c-name' },
{ key: 'country_code_label', head: 'Country' },
{ key: 'population', head: 'People', cls: 'c-num', num: true },
{ key: 'religion_label', head: 'Religion' },
];
const head = cols.map((c) => `<th>${esc(c.head)}</th>`).join('') + '<th class="c-pray">✓</th>';
const body = data.map((r) => '<tr>' + cols.map((c) => {
const raw = r[c.key] ?? r[`${c.key}_label`] ?? '—';
const v = c.num && Number.isFinite(Number(raw))
? Number(raw).toLocaleString(lang()) : raw;
return `<td${c.cls ? ` class="${c.cls}"` : ''}>${esc(v)}</td>`;
}).join('') + '<td class="c-pray">□</td></tr>').join('');
set(item, kind, 'render', `laying out ${data.length} rows`, 70);
const docTitle = title || `${label(item)} — ${data.length}`;
openPrintFrame(
`<!DOCTYPE html><html><head><meta charset="utf-8"><title>${esc(docTitle)}</title>`
+ '<style>'
+ '@page{size:letter portrait;margin:14mm 12mm}'
+ 'html,body{margin:0;background:#fff;color:#0f1419;'
+ "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}"
+ '.hd{margin-bottom:14px}.hd h1{margin:0 0 3px;font-size:18px}'
+ '.hd p{margin:0;font-size:11px;color:#5b6672}'
+ 'table{width:100%;border-collapse:collapse;font-size:11px}'
+ 'thead th{text-align:left;border-bottom:2px solid #0f1419;padding:5px 6px;'
+ 'font-size:9.5px;text-transform:uppercase;letter-spacing:.05em}'
+ 'tbody td{padding:5px 6px;border-bottom:1px solid #e3e6ea}'
+ '.c-name{font-weight:600}.c-num{font-variant-numeric:tabular-nums}'
+ '.c-pray{width:26px;text-align:center;font-size:13px}'
+ 'tbody tr{break-inside:avoid}'
+ '</style></head><body>'
+ `<div class="hd"><h1>${esc(docTitle)}</h1>`
+ `<p>${data.length} entries · pray for them by name</p></div>`
+ `<table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`
+ '</body></html>',
{ autoprint: true },
);
done(item, kind);
} catch (e) { fail(item, kind, e); }
}
// ── 3. THE CARD DECK ──────────────────────────────────────────────────────
// One card per PAGE at the chosen stock, so the print dialog shows CARDS rather
// than letter paper. The geometry lives in the shared card renderer.
async function printCardDeck(item) {
if (busy()) return;
const kind = 'Card Deck';
set(item, kind, 'fetch', 'fetching records', 30);
try {
const data = await rows(item);
const [{ createCardRenderer }, strings] = await Promise.all([
import('../packet/card-render.js'),
opts.strings ? opts.strings() : import('../scripts/posters/packet-strings.json').then((m) => m.default || m),
]);
set(item, kind, 'render', `laying out ${data.length} cards`, 70);
const R = createCardRenderer({ strings, lang: lang(), stock: stock() });
openPrintFrame(R.buildDeck(item, data), { autoprint: true });
done(item, kind);
} catch (e) { fail(item, kind, e); }
}
return { exportState, dismiss, printPoster, printList, printCardDeck };
}

View file

@ -57,6 +57,8 @@
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery",
"check:card-deck": "node internal/check-card-deck.mjs",
"check:poster-raster": "node internal/check-poster-raster.mjs",
"check:poster-prints": "node internal/check-poster-prints.mjs",
"check:provider-switch": "node internal/check-provider-switch.mjs",
"check:mfe-two-tab": "node internal/check-mfe-two-tab.mjs",
"check:share-policy": "node internal/check-share-policy.mjs",
"check:zone-census": "node internal/check-zone-census.mjs",

View file

@ -260,9 +260,36 @@ const NOT_EXPERIMENTAL = new Set([
'template-bundle',
'people-groups-grid',
'research-dashboard',
'big-picture'
'big-picture',
// roundup ruling (EXPERIMENTAL-MAPS-ROUNDUP.md, 2026-09-24): scaffolding and authoring
// tooling, not maps -- the studio still surfaces them for developers.
'demo-map',
'golden-path-check',
// doxa-research-map is the PART already composed into the Simple Research tab
// (research-map-plus-grid) -- same redundancy rule as research-dashboard/big-picture.
'doxa-research-map',
// stack-demo is broken and Driver-DEFERRED; a broken tab in the giveaway shelf reads as
// product damage. It returns when someone fixes it (a tracked issue).
'stack-demo'
])
// ── EXPERIMENTAL ORDER = KINGDOM IMPACT, not the alphabet ────────────────────────────────
// The roundup charter orders the folder by "potential to get people TO the unreached groups
// and help them meet and understand Jesus", finalized 2026-09-24 AFTER reading every map's
// rendered output (EXPERIMENTAL-MAPS-ROUNDUP.md has the per-map reasoning). Rank is
// EDITORIAL, the same class of decision as the curated TAB_DEF above -- which is why it
// lives here and not in per-bundle meta: the manifest deliberately strips meta config, and
// an order is a statement about the whole shelf, not about one bundle. Unranked bundles
// still appear (discovery is never gated by this) -- they sort after the ranked, A-Z.
const EXPERIMENTAL_RANK = {
'prayer-walk-planner': 1, // feet on streets: zones -> printed one-page walking maps
'upg-neighbor': 2, // the rescued legacy set; entry = send-map ("who goes, to whom")
'key-cities': 3, // where the unreached physically live
'all-people-groups': 4, // the census view that feeds everything else
'country-posters': 5, // mobilization print artifacts
'doxa-vision-card': 6, // vision framing
}
// The tab manifest. Each entry CURATES one tab: which bundle+profile it shows, in what order, under
// what human label. Every tab nests its plugin's REAL custom element — there is no iframe mode.
//
@ -570,8 +597,12 @@ function experimentalTabsFromManifest(manifest) {
discovered: true // marks an auto-discovered tab (used by the rail + tests)
})
}
// Stable, predictable order so the folder does not reshuffle between builds.
out.sort((a, z) => a.label.localeCompare(z.label))
// Ranked first (Kingdom impact, see EXPERIMENTAL_RANK), then A-Z -- stable between builds.
out.sort((a, z) => {
const ra = EXPERIMENTAL_RANK[a.id] || 999
const rz = EXPERIMENTAL_RANK[z.id] || 999
return ra !== rz ? ra - rz : a.label.localeCompare(z.label)
})
return out
}

View file

@ -29,7 +29,7 @@
* element (same double-emit pattern as people-groups-grid.vue's `groups-filtered`), so a plain
* host page hears it standalone and a Vue-nesting shell hears it as a prop-less DOM event.
*/
import { inject, ref, computed, onMounted, getCurrentInstance } from 'vue'
import { inject, ref, computed, onMounted, watch, getCurrentInstance } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { splitLastWord } from './src/useLastWordHighlight.js'
import { useRegionsI18n } from './src/useRegionsI18n.js'
@ -102,7 +102,7 @@ const isRTL = computed(() => RTL_LOCALES.has(i18nLocale.value))
// We rendered only the region cards, so the standalone leaf page had no title and no strapline —
// invisible inside the composite, where the tab strip names the view, and plainly missing on
// /doxa-maps/regions-dashboard/regions-list/index.html, which is a page in its own right.
const { t } = useRegionsI18n()
const { t, tf } = useRegionsI18n()
const headTitle = computed(() => splitLastWord(t('Regions')))
// ★ REGISTER BEBAS NEUE AT DOCUMENT LEVEL, not in the shadow root: @font-face is document-scoped
@ -135,7 +135,18 @@ function formatNumber(n) {
}
function summaryLine(region) {
return `${formatNumber(region.count)} people groups in ${formatNumber(region.countryCount)} countries`
// THEIRS, pages/regions/index.vue:29-31:
// t('%count% people groups in %countries% countries')
// .replace('%count%', formatNumber(region.count))
// .replace('%countries%', formatNumber(region.countryCount))
// This was a hardcoded English template, so /ru/regions rendered "315 people groups in 41
// countries" while theirs reads "315 групп людей в 41 странах". The ru string was ALREADY in
// library/i18n/locales/ru/regions.json — copied, never wired. Caught by reading the Russian
// screenshot, not by any assertion.
return tf('%count% people groups in %countries% countries', {
count: formatNumber(region.count),
countries: formatNumber(region.countryCount),
})
}
// ── emit (Vue + native composed CustomEvent, same pattern as people-groups-grid) ────────────
@ -171,7 +182,10 @@ function selectCountry(country, region) {
// "See Africa dashboard" / "See Asia dashboard" — same wording pattern as DX-life's
// dashboardLink() in app/pages/regions/index.vue (t('See %region% dashboard')).
function dashboardLink(region) {
return `See ${region.label || 'Other'} dashboard`
// THEIRS: t('See %region% dashboard') — ru renders "Открыть панель: %region%", which is not
// even the same word order, so a hardcoded English template could never be patched into it.
// The translation was already catalogued; only the wiring was missing.
return tf('See %region% dashboard', { region: region.label || t('Other') })
}
// ── data fetch (same live endpoint + fields as people-groups-grid.vue) ──────────────────────
@ -259,6 +273,14 @@ function onPosterShowOnMap(ev) {
}
}
// A LOCALE CHANGE MUST REFETCH, not just re-render. The region and country LABELS come from
// the API's ?lang=, not from the i18n catalogue — MEASURED on their live site, which serves
// "Азия"/"Африка"/"Азербайджан (3)" on /ru/regions against "Africa"/"Angola" on /regions. With
// no watcher this tab kept whatever labels the first fetch returned, so switching to Russian
// translated the chrome and left every region and country name in English. region-dashboards.vue
// already had this watcher (line 541); this tab was simply missed.
watch(languageCode, () => { loadRegions() })
onMounted(() => { if (typeof window !== 'undefined') window.addEventListener(POSTER_EVENT, onPosterShowOnMap) })
onBeforeUnmount(() => { if (typeof window !== 'undefined') window.removeEventListener(POSTER_EVENT, onPosterShowOnMap) })
@ -322,9 +344,17 @@ useShadowStyles(
// 7vw, --font-size-5xl) and the subtitle is --font-size-md in --color-brand-lighter.
// cqw not vw, for the same composite-pane reason as every other clamp here.
'.rl-head{ display:flex; flex-direction:column; gap:10px; text-align:center; }' +
// NO text-transform. MEASURED on their live h1, both locales: text-transform is `none`
// (/regions and /ru/regions alike). This rule used to force uppercase, which is INVISIBLE in
// English because Bebas Neue is an all-caps face — and wrong the moment the text is not Latin:
// Bebas carries no Cyrillic, so /ru falls back to a normal system face and their heading reads
// "Регионы" while ours read "РЕГИОНЫ". The same conclusion is already written out for
// .region-card__title below ("DX-life applies NO transform on top of the font"); this rule was
// simply missed, and no check caught it because the vs-live property diff covers the DASHBOARD
// tab's .rd-title, never this one. _probe-list-vs-live.mjs now covers it.
'.rl-title{ margin:0; font-size:clamp(47.6837px, 7cqw, 74.5058px); line-height:1;' +
' font-family:"Bebas Neue","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
' font-weight:400; text-transform:uppercase; }' +
' font-weight:400; }' +
'.rl-title__last{ color:#92b195; }' +
'.rl-summary{ margin:0; color:var(--dx-muted); font-size:19.5312px; }' +
'.rl-status{ padding:28px; text-align:center; color:var(--dx-muted); font-size:19.5312px; }' +
@ -419,7 +449,10 @@ useShadowStyles(
<h1 class="rl-title">{{ headTitle.head ? headTitle.head + ' ' : '' }}<span class="rl-title__last">{{ headTitle.last }}</span></h1>
<p class="rl-summary">{{ t('Find unreached people groups by region and country.') }}</p>
</div>
<div v-if="loading" class="rl-status">Loading regions…</div>
<!-- THEIR string, not a new one: 'Loading results...' is in their common.json and so is
already translated in all eight locales. Their /regions is server-rendered and has no
loading state of its own, but reusing their wording beats inventing English here. -->
<div v-if="loading" class="rl-status">{{ t('Loading results...') }}</div>
<div v-else-if="loadFailed" class="rl-status rl-failed" role="status">
Region data could not be loaded. This is a connection problem, not a filter.
</div>

View file

@ -2,8 +2,8 @@
/**
* upg-neighbor.vue — LEGACY-WRAPPER bundle (experimental roundup).
*
* @description Rescues the three pre-bundler experimental maps that live inside the legacy
* upg-neighbor IIFE — send-map, flower-mesh and lang-cluster — without rebuilding them and
* @description Rescues the five pre-bundler experimental maps that live inside the legacy
* upg-neighbor IIFE — send-map, the upg-neighbors/bloom cockpit, lang-cluster and flower-mesh — without rebuilding them and
* without letting their old engine touch the modern bundles: each profile mounts its element
* inside a sandboxed IFRAME whose host page (assets/legacy/upg-neighbor/index.html) loads the
* prebuilt script, shims its relative /api fetches to the live API, and passes the token.

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-pWCBnzi9A8axrT5aW4DjigzZ8utiQeU+OVsOIKVKA9vC61ArtxaRxjqQ8l5xNxzI" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-KCb+2VIX0x0RZlPudE1oeY64Wp7UPNmJo8Zx7EkADp5+BYHfB2KsRlzS6ssYZMIF" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-pWCBnzi9A8axrT5aW4DjigzZ8utiQeU+OVsOIKVKA9vC61ArtxaRxjqQ8l5xNxzI" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-KCb+2VIX0x0RZlPudE1oeY64Wp7UPNmJo8Zx7EkADp5+BYHfB2KsRlzS6ssYZMIF" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-pWCBnzi9A8axrT5aW4DjigzZ8utiQeU+OVsOIKVKA9vC61ArtxaRxjqQ8l5xNxzI" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-KCb+2VIX0x0RZlPudE1oeY64Wp7UPNmJo8Zx7EkADp5+BYHfB2KsRlzS6ssYZMIF" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
<script src="../../doxa-maps-spa.js" integrity="sha384-Ce+DKsLgwfej+rgoWeaWFuMMbR3Y4H+8io/lVL+lQfX0O3iGKwDLf4remDkHGCrP" crossorigin="anonymous"></script>
<script src="../../doxa-maps-spa.js" integrity="sha384-1f+/rBSGo/1jyUmRmGg34CIU/PcGkB7M3jah1DO0ocb5O0zj81wpUemXcz4WAFz8" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
<script src="../doxa-maps-spa.js" integrity="sha384-Ce+DKsLgwfej+rgoWeaWFuMMbR3Y4H+8io/lVL+lQfX0O3iGKwDLf4remDkHGCrP" crossorigin="anonymous"></script>
<script src="../doxa-maps-spa.js" integrity="sha384-1f+/rBSGo/1jyUmRmGg34CIU/PcGkB7M3jah1DO0ocb5O0zj81wpUemXcz4WAFz8" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -154,7 +154,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "doxa-maps/country-posters/country-posters.js",
"integrity": "sha384-pWCBnzi9A8axrT5aW4DjigzZ8utiQeU+OVsOIKVKA9vC61ArtxaRxjqQ8l5xNxzI",
"integrity": "sha384-KCb+2VIX0x0RZlPudE1oeY64Wp7UPNmJo8Zx7EkADp5+BYHfB2KsRlzS6ssYZMIF",
"elements": [
"country-posters-map"
],
@ -174,7 +174,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/countries-map/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=\"./country-posters/country-posters.js\" integrity=\"sha384-pWCBnzi9A8axrT5aW4DjigzZ8utiQeU+OVsOIKVKA9vC61ArtxaRxjqQ8l5xNxzI\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-KCb+2VIX0x0RZlPudE1oeY64Wp7UPNmJo8Zx7EkADp5+BYHfB2KsRlzS6ssYZMIF\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -188,7 +188,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-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=\"./country-posters/country-posters.js\" integrity=\"sha384-pWCBnzi9A8axrT5aW4DjigzZ8utiQeU+OVsOIKVKA9vC61ArtxaRxjqQ8l5xNxzI\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-KCb+2VIX0x0RZlPudE1oeY64Wp7UPNmJo8Zx7EkADp5+BYHfB2KsRlzS6ssYZMIF\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -202,7 +202,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-pWCBnzi9A8axrT5aW4DjigzZ8utiQeU+OVsOIKVKA9vC61ArtxaRxjqQ8l5xNxzI\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-KCb+2VIX0x0RZlPudE1oeY64Wp7UPNmJo8Zx7EkADp5+BYHfB2KsRlzS6ssYZMIF\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -298,7 +298,7 @@
"bundle": "doxa-maps-spa",
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
"output": "doxa-maps/doxa-maps-spa/doxa-maps-spa.js",
"integrity": "sha384-Ce+DKsLgwfej+rgoWeaWFuMMbR3Y4H+8io/lVL+lQfX0O3iGKwDLf4remDkHGCrP",
"integrity": "sha384-1f+/rBSGo/1jyUmRmGg34CIU/PcGkB7M3jah1DO0ocb5O0zj81wpUemXcz4WAFz8",
"elements": [
"doxa-maps-spa"
],
@ -322,7 +322,7 @@
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
"embed": {
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/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=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-Ce+DKsLgwfej+rgoWeaWFuMMbR3Y4H+8io/lVL+lQfX0O3iGKwDLf4remDkHGCrP\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-1f+/rBSGo/1jyUmRmGg34CIU/PcGkB7M3jah1DO0ocb5O0zj81wpUemXcz4WAFz8\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
},
"instances": [
{
@ -856,7 +856,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-BHIpCnisN9qu6gdJpslS3Q59xknjMZfO4AL3tNe6jbYfdg3CShiTaVWQYv5KyidS",
"integrity": "sha384-tj7QLhUCvAluRlp9Bp2ddjbXLcknRx2v1nMotvvY8XKImLplx6WkSdHh5MQC1Rix",
"elements": [
"regions-dashboard"
],
@ -876,7 +876,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-BHIpCnisN9qu6gdJpslS3Q59xknjMZfO4AL3tNe6jbYfdg3CShiTaVWQYv5KyidS\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-tj7QLhUCvAluRlp9Bp2ddjbXLcknRx2v1nMotvvY8XKImLplx6WkSdHh5MQC1Rix\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -890,7 +890,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-BHIpCnisN9qu6gdJpslS3Q59xknjMZfO4AL3tNe6jbYfdg3CShiTaVWQYv5KyidS\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-tj7QLhUCvAluRlp9Bp2ddjbXLcknRx2v1nMotvvY8XKImLplx6WkSdHh5MQC1Rix\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -904,7 +904,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-BHIpCnisN9qu6gdJpslS3Q59xknjMZfO4AL3tNe6jbYfdg3CShiTaVWQYv5KyidS\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-tj7QLhUCvAluRlp9Bp2ddjbXLcknRx2v1nMotvvY8XKImLplx6WkSdHh5MQC1Rix\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]
@ -1076,7 +1076,7 @@
{
"profile": "upg-neighbor",
"file": "upg-neighbor.vue",
"description": "Rescues the three pre-bundler experimental maps that live inside the legacy",
"description": "Rescues the five pre-bundler experimental maps that live inside the legacy",
"element": "upg-neighbor",
"type": "parameterized",
"isMap": false,

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-BHIpCnisN9qu6gdJpslS3Q59xknjMZfO4AL3tNe6jbYfdg3CShiTaVWQYv5KyidS" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-tj7QLhUCvAluRlp9Bp2ddjbXLcknRx2v1nMotvvY8XKImLplx6WkSdHh5MQC1Rix" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-BHIpCnisN9qu6gdJpslS3Q59xknjMZfO4AL3tNe6jbYfdg3CShiTaVWQYv5KyidS" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-tj7QLhUCvAluRlp9Bp2ddjbXLcknRx2v1nMotvvY8XKImLplx6WkSdHh5MQC1Rix" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-BHIpCnisN9qu6gdJpslS3Q59xknjMZfO4AL3tNe6jbYfdg3CShiTaVWQYv5KyidS" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-tj7QLhUCvAluRlp9Bp2ddjbXLcknRx2v1nMotvvY8XKImLplx6WkSdHh5MQC1Rix" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: