Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run

# Conflicts:
#	public/doxa-maps/gospel-resources/charts/index.html
#	public/doxa-maps/gospel-resources/data-tables/index.html
#	public/doxa-maps/gospel-resources/gospel-dashboard/index.html
#	public/doxa-maps/gospel-resources/gospel-resources.js
#	public/doxa-maps/gospel-resources/resources-list/index.html
#	public/doxa-maps/gospel-resources/resources-map/index.html
#	public/doxa-maps/index.html
#	public/doxa-maps/manifest.json
This commit is contained in:
doxa-maps-88655c70 2026-09-24 20:52:29 +00:00
commit 795b357ab8
98 changed files with 1881 additions and 964 deletions

View file

@ -49,7 +49,7 @@ The scripts these commands are grounded in (from this branch's `package.json`):
"build": "bash internal/build-lock.sh",
"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",
"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:wp-embed check:doc-scripts",
"gates:post": "node internal/run-parallel.mjs --label=post -- 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:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status 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:legend-source check:backlog-refs",
"gates:post": "node internal/run-parallel.mjs --label=post -- 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:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status 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:legend-source check:backlog-refs check:composite-embed-knobs",
"build:bundles": "node internal/run-parallel.mjs --bundles", // the 22 vite builds, 4 at a time
"preview": "bun x vite preview",
"serve": "node internal/serve-strict.mjs", // 404s a directory, like production

View file

@ -63,7 +63,6 @@ Generated from 22 bundle(s).
| `doxa-simple-map` | `doxa-simple-map` | `parameterized` |
| `doxa-vision-card` | `doxa-vision-card` | `parameterized` |
| `people-groups-grid` | `people-groups-grid` | `parameterized` |
| `upg-neighbor` | `upg-neighbor` | `parameterized` |
## Capability 3 — multi-view-element (monolithic)
@ -71,6 +70,7 @@ Generated from 22 bundle(s).
|---|---|---|
| `doxa-research-map` | `research-map` | `monolithic` |
| `research-dashboard` | `research-dashboard` | `monolithic` |
| `upg-neighbor` | `upg-neighbor` | `monolithic` |
## Capability 4 — composite-element (nested-element SPA)

View file

@ -0,0 +1,229 @@
#!/usr/bin/env node
/**
* check-composite-embed-knobs.mjs — a composite must pass the host's TOKEN and PROVIDER down to the
* map it nests, on somebody else's page.
*
* THE DEFECT THIS EXISTS FOR, MEASURED 2026-09-24. Two shipped composites declared NO PROPS at all,
* so the `profile-config` a partner writes was read by nobody:
*
* <research-map-plus-grid profile-config='{"tk":"pk...."}'> -> "No Mapbox access token
* <dashboard-plus-grid profile-config='{"tk":"pk...."}'> provided", 0 tiles, no canvas
*
* and after the token was threaded, the SAME hole was still open one knob over:
*
* <research-map-plus-grid profile-config='{"provider":"maplibre"}'> -> child still on mapbox,
* so it demanded a token
*
* Every existing gate missed all of it, and not by accident: they load a composite on its OWN
* generated leaf, and that leaf BAKES a token and assigns window.MAP_TOKEN, which the shared-token
* bridge hands to the nested map. The leaf supplies the very thing under test. The failure exists
* only where there is no leaf — which is exactly the partner page the published embed snippet is
* written for, and exactly the case nobody was testing.
*
* WHY THESE TWO KNOBS AND NOT A LONGER LIST. They are the two the snippet documents and the two the
* product promises: `tk` is how a partner supplies their own key, and `provider` is the one prop that
* is supposed to swap the engine. A keyless MapLibre embed is the whole reason MapLibre support was
* built — partners at volume who do not want a Mapbox key — so a composite that drops `provider`
* does not degrade the feature, it cancels it, and reports the loss as a token error.
*
* HOW. Serves the built tree from a FREE port (ACAO:* via serve-strict, production parity) and the
* partner page from `localhost` — a genuinely different origin, so CORS is exercised rather than
* assumed. Free ports asked of the OS, never fixed ones: a stray server on a fixed port answers in
* place of the tree under test and the gate then passes a build it never loaded.
*
* ASSERTIONS, per composite that nests a map:
* tk — given ONLY {"tk": <the token the tree bakes>}, a canvas appears and at least one
* api.mapbox.com style/tile request succeeds. No token error in the console.
* provider — given ONLY {"provider":"maplibre"} and NO token, a canvas appears and there are
* ZERO requests to api.mapbox.com. Zero is the assertion: "it rendered" would pass on
* a build that quietly fell back to mapbox with a baked key.
*
* NEGATIVE-TESTED: reverting either fix in research-map-plus-grid.vue makes the matching case FAIL
* naming the composite and the knob; both restored, both pass. Removing the zero-request assertion
* from the provider case makes it pass on a build that ignores the prop, which is what it is for.
*
* Exit: 0 pass or SKIP (no built tree, no browser, no baked token to test with) · 1 fail.
*/
import { createServer } from 'node:http'
import { spawn } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
if (!existsSync(MANIFEST)) {
console.log('COMPOSITE-EMBED-KNOBS: SKIP — no built manifest (run `bun run tree` first).')
process.exit(0)
}
let chromium
try { ({ chromium } = await import('playwright')) } catch {
console.log('COMPOSITE-EMBED-KNOBS: SKIP — playwright is not installed.')
process.exit(0)
}
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
// Which bundles nest a MAP? A composite declares `composes`; a bundle is a map if any of its
// profiles is isMap. Asking the manifest keeps this list correct when a composite is added.
const mapBundles = new Set()
for (const b of manifest.bundles || []) {
if ((b.profiles || []).some((p) => p.isMap)) mapBundles.add(b.bundle)
}
const targets = []
for (const b of manifest.bundles || []) {
// TWO WAYS A COMPOSITE DECLARES ITSELF, and taking only the first under-covered this gate.
// `composes` is written when a composite mounts children BY TAG; a composite that BAKES its
// children in (an imported .vue) lists nothing there, and country-posters-mfe — one of the three
// composites this defect was measured on — was silently not a target. The profile TYPE says so in
// both cases, so ask for either.
const composes = b.composes || []
const byCompose = composes.some((c) => mapBundles.has(c))
const profile = (b.profiles || []).find((p) => p.element && (byCompose || p.type === 'nested-elements-mfe'))
if (!profile || !b.scriptUrl) continue
targets.push({ bundle: b.bundle, element: profile.element, script: `${b.bundle}/${b.bundle}.js` })
}
if (!targets.length) {
console.log('COMPOSITE-EMBED-KNOBS: SKIP — the manifest declares no composite that nests a map.')
process.exit(0)
}
// The token to hand the partner page is the one the tree already bakes into its own leaves: this
// gate must not invent a key, and a token that is not this tree's would 401 and look like our bug.
const bakedToken = (() => {
for (const b of manifest.bundles || []) {
for (const p of b.profiles || []) {
if (!p.isMap || !p.iframeUrl) continue
const leaf = join(ROOT, 'public', 'doxa-maps', String(p.iframeUrl).replace(/^\.\//, ''))
if (!existsSync(leaf)) continue
const m = readFileSync(leaf, 'utf8').match(/BAKED_TOKEN\s*=\s*"(pk\.[^"]+)"/)
if (m) return m[1]
}
}
return ''
})()
const freePort = () => new Promise((ok) => {
const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) })
})
const TREE_PORT = await freePort()
const HOST_PORT = await freePort()
const TREE = `http://127.0.0.1:${TREE_PORT}/doxa-maps`
let pageHtml = '<!doctype html><html><body>nothing yet</body></html>'
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' })
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html' }); r.end(pageHtml) }).listen(HOST_PORT, 'localhost')
const cleanup = () => { try { tree.kill() } catch { /* already gone */ } try { host.close() } catch { /* already closed */ } }
await new Promise((r) => setTimeout(r, 900))
const reachable = await fetch(`${TREE}/manifest.json`, { signal: AbortSignal.timeout(4000) }).then((r) => r.ok).catch(() => false)
if (!reachable) {
cleanup()
console.log('COMPOSITE-EMBED-KNOBS: SKIP — could not serve the built tree locally.')
process.exit(0)
}
const CASES = [
{ knob: 'tk', config: (tok) => ({ tk: tok }), needsToken: true },
{ knob: 'provider', config: () => ({ provider: 'maplibre' }), needsToken: false },
]
const browser = await chromium.launch({ args: ['--enable-unsafe-swiftshader', '--use-gl=angle', '--use-angle=swiftshader'] })
const fails = []
const lines = []
// Elements whose tk case actually drew a map — the only ones whose provider case can mean anything.
const renders = new Set()
try {
for (const t of targets) {
for (const c of CASES) {
// Do not pay for a page load we already know cannot mean anything: if this element's tk case
// drew no map, its provider case has nothing to compare against. Short-circuiting here is what
// keeps the whole gate near two minutes instead of five on a nine-composite tree.
if (c.knob === 'provider' && !renders.has(t.element)) {
lines.push(` · ${t.element} [provider] UNMEASURED — its tk case never drew a map, so there is nothing to compare against.`)
continue
}
if (c.needsToken && !bakedToken) {
lines.push(` · ${t.element} [${c.knob}] SKIPPED — no baked token in this tree to hand the host`)
continue
}
const cfg = JSON.stringify(c.config(bakedToken)).replace(/'/g, '&#39;')
pageHtml = `<!doctype html><html><head><meta charset=utf-8><title>partner</title></head><body style="margin:0">
<header style="height:48px;background:#eee">HOST HEADER</header>
<script>window.MAP_APP_API_URL="https://pray.doxa.life";</script>
<script src="${TREE}/${t.script}" crossorigin="anonymous"></script>
<${t.element} profile-config='${cfg}'></${t.element}>
</body></html>`
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
let mapboxOk = 0, mapboxBad = 0
const errs = []
page.on('response', (r) => {
if (!/api\.mapbox\.com|tiles\.mapbox\.com/.test(r.url())) return
if (r.status() >= 400) mapboxBad++; else mapboxOk++
})
page.on('console', (m) => { if (m.type() === 'error') errs.push(m.text().slice(0, 120)) })
await page.goto(`http://localhost:${HOST_PORT}/`, { waitUntil: 'load' }).catch(() => {})
// WAIT FOR THE THING, NOT FOR A CLOCK. A flat 14s per case made this gate cost four minutes
// of wall time on a nine-composite tree, most of it spent watching a map that had already
// drawn. Poll for a canvas instead, then settle briefly so late tile requests are counted —
// the provider case asserts ZERO requests to api.mapbox.com, so it must not stop measuring
// the moment something paints. An element that never draws still costs the full budget, which
// is correct: that is the case whose absence is the finding.
const CANVAS_BUDGET_MS = 16000
const deadline = Date.now() + CANVAS_BUDGET_MS
let canvas = 0
const countCanvas = `(() => {
let n = 0
const visit = (r) => { n += r.querySelectorAll('canvas').length; for (const el of r.querySelectorAll('*')) if (el.shadowRoot) visit(el.shadowRoot) }
visit(document)
return n
})()`
while (Date.now() < deadline) {
canvas = await page.evaluate(countCanvas).catch(() => 0)
if (canvas) break
await page.waitForTimeout(500)
}
await page.waitForTimeout(2500)
canvas = await page.evaluate(countCanvas).catch(() => canvas)
const tokenErr = errs.find((e) => /access token/i.test(e))
const label = `${t.element} [${c.knob}]`
const mapboxAll = mapboxOk + mapboxBad
// ★ FAIL ONLY WHERE THE EVIDENCE POINTS AT THE KNOB. An element that draws no map under a
// MINIMAL config is not thereby broken: several composites need a `profile` to select which
// of their profiles to show, and this gate deliberately passes only the knob under test. The
// first draft failed nine of them for that, which would have red-gated other people's plugins
// over a claim it had not measured. The distinguishing evidence is the console: a token WAS
// supplied, so "No Mapbox access token provided" means the knob did not cross the boundary —
// that is this defect. No canvas and no such complaint is simply not measurable here, and is
// reported as UNMEASURED rather than dressed up as a finding.
if (c.knob === 'tk') {
if (canvas && mapboxOk > 0) { renders.add(t.element); lines.push(` ok ${label} — canvas ${canvas}, api.mapbox.com requests ${mapboxAll}`) }
else if (canvas && mapboxOk === 0) fails.push(`${label}: canvas drew but NOTHING loaded from api.mapbox.com — the host's token never reached the map`)
else if (tokenErr) fails.push(`${label}: a token was supplied and the element still says "${tokenErr}" — it never read the host's profile-config`)
else lines.push(` · ${label} UNMEASURED — no map canvas under a minimal config, and no token complaint. This element likely needs a \`profile\` to show its map; not a knob failure.`)
} else {
if (canvas && mapboxAll === 0) lines.push(` ok ${label} — canvas ${canvas}, api.mapbox.com requests 0 (keyless, as asked)`)
else if (mapboxAll > 0) fails.push(`${label}: asked for maplibre and still made ${mapboxAll} request(s) to api.mapbox.com — the prop did not cross the composite boundary`)
else fails.push(`${label}: asked for maplibre with no token and drew nothing${tokenErr ? ` — ${tokenErr}` : ''}`)
}
await page.close()
}
}
} finally {
await browser.close()
cleanup()
}
console.log(`COMPOSITE-EMBED-KNOBS: ${targets.length} composite(s) that nest a map, on a foreign origin`)
for (const l of lines) console.log(l)
if (fails.length) {
console.error(`\nCOMPOSITE-EMBED-KNOBS: FAIL — ${fails.length} case(s)`)
for (const f of fails) console.error(' - ' + f)
console.error('\n A composite reads the host\'s profile-config and hands `tk` and `provider` to the map it')
console.error(' nests. Our own leaves bake a token and assign window.MAP_TOKEN, so this only ever breaks')
console.error(' on a partner page — the case the published embed snippet is written for.')
process.exit(1)
}
console.log(`\nCOMPOSITE-EMBED-KNOBS: PASS — the host's token and provider reach every nested map.`)

View file

@ -55,6 +55,7 @@ async function measure(width) {
const cards = all.filter((e) => (e.className||'').toString().split(' ').includes('card')).filter(vis)
const boxes = cards.map((c) => ({ c, b: c.getBoundingClientRect() }))
let intrusions = 0, overflowing = 0, worst = 0, coveringPreview = 0, worstY = 0
let previewsPainted = 0, previewsBroken = 0, noPreview = 0
for (const { c, b } of boxes) {
const row = c.querySelector('.pfs')
if (!row) continue
@ -68,7 +69,24 @@ async function measure(width) {
// 35px -> 78px, and because it sits at bottom:-14px the extra height pushed its TOP from
// 216px up to 173px in a 238px card: over the preview image and the card's outline.
// Horizontal clean, card visibly broken, gate green.
// COUNT THE PREVIEW, do not merely guard on it. An if-guard protects this loop from a
// placeholder card — and also means a card with NO preview passes the over-preview assertion
// perfectly. The driver reported BOTH a missing-previews symptom AND the overlap;
// measuring only the overlap leaves the first unasserted.
// NOTE: no backticks in this block — it lives inside a JS template literal.
const img = c.querySelector('img')
// ONLY JUDGE WHAT IS IN VIEW. Every preview carries loading=lazy, so a card below the fold
// legitimately has naturalWidth 0 — measured at 390px: 8 of 27 painted, the first unpainted
// one 3926px down an 844px viewport, and ZERO failing HTTP responses. A first version of this
// assertion called that 18 broken images. Deferred is not broken; only an in-viewport image
// that never painted is.
if (!img) noPreview++
else {
const ib2 = img.getBoundingClientRect()
const inView = ib2.top < window.innerHeight && ib2.bottom > 0
if (img.complete && img.naturalWidth > 0) previewsPainted++
else if (inView) previewsBroken++
}
if (img) {
const ib = img.getBoundingClientRect()
if (ib.height > 2 && rb.top < ib.bottom - 1) {
@ -87,6 +105,7 @@ async function measure(width) {
const tiny = ctrls.filter((e) => { const b = e.getBoundingClientRect(); return b.width < 18 || b.height < 10 })
return { cards: cards.length, rows: cards.filter((c) => c.querySelector('.pfs')).length,
overflowing, worst, intrusions, coveringPreview, worstY,
previewsPainted, previewsBroken, noPreview,
controls: ctrls.length, tiny: tiny.length }
})()`)
await page.close()
@ -107,6 +126,8 @@ for (const r of results) {
+ ` · overflow ${r.overflowing}${r.worst ? ` (${r.worst}px)` : ''}`
+ ` · neighbour ${r.intrusions}`
+ ` · over-preview ${r.coveringPreview}${r.worstY ? ` (${r.worstY}px)` : ''}`
+ ` · previews ${r.previewsPainted} painted`
+ `${r.previewsBroken ? `/${r.previewsBroken} BROKEN` : ''}${r.noPreview ? `/${r.noPreview} none` : ''}`
+ ` · ${r.controls} control(s), ${r.tiny} too small`)
}
@ -125,6 +146,27 @@ for (const r of results) {
}
// The row belongs BELOW the preview. Driver-reported 2026-09-24: "the download print options are
// not showing up under the blue outline, they are overlapping where the blue outline would be".
// THE PREVIEW MUST ACTUALLY BE THERE. A card with no image passes every geometry assertion
// above trivially — nothing can overlap what does not render. Driver 2026-09-24: "it is supposed
// to show the previews".
if (r.previewsBroken) {
fails.push(`${at}: ${r.previewsBroken} preview image(s) failed to load (naturalWidth 0) — `
+ 'a broken preview is not something the geometry checks can see')
}
// EVERY card falling back to the placeholder means the previews are gone. MEASURED by aborting
// every previews/** request: the product degrades WELL — all 27 cards swap to a .ph placeholder
// reading "poster not generated yet", with no broken-image icons and no <img> left at all. That
// is correct behaviour and exactly why it is invisible to an image-health check: previewsBroken
// stays 0 because there is nothing left to be broken. Assert the FALLBACK instead.
if (r.cards && r.noPreview === r.cards) {
fails.push(`${at}: all ${r.cards} card(s) fell back to the "poster not generated yet" `
+ 'placeholder — no preview rendered anywhere, and the geometry assertions above are vacuous '
+ 'without one (they cannot overlap an image that is not there)')
}
if (r.previewsPainted === 0 && r.cards && r.noPreview !== r.cards) {
fails.push(`${at}: NOT ONE preview image painted across ${r.cards} card(s) — the geometry `
+ 'assertions above are vacuous without them')
}
if (r.coveringPreview) {
fails.push(`${at}: ${r.coveringPreview} print row(s) COVER the preview image `
+ `(top sits ${r.worstY}px above the image's bottom edge) — growing the row taller to fit it `

View file

@ -28,7 +28,7 @@
* party tile/glyph hiccups are reported, not failed — they are not this repo's to fix).
*
* PRECONDITION: a built tree in public/doxa-maps (this starts its own servers; no dev server).
* Usage: node internal/check-research-gallery-embed.mjs [--mode js|iframe] [--only id,id]
* Usage: node internal/check-research-gallery-embed.mjs [--mode js|iframe] [--only id,id] [--provider maplibre]
* Exit: 0 pass · 1 fail · 2 SKIP (no built tree / no browser)
*/
import { createServer } from 'node:http'
@ -42,6 +42,26 @@ const argv = process.argv.slice(2)
const argOf = (k, d) => { const i = argv.indexOf(k); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
const MODES = argOf('--mode', '') ? [argOf('--mode', '')] : ['js', 'iframe']
const ONLY = argOf('--only', '') ? argOf('--only', '').split(',') : null
// --provider maplibre: the KEYLESS embed. The host asks for maplibre (attribute in js mode, ?provider=
// in iframe mode) and every tab must still paint — with ZERO requests to api.mapbox.com. That is the
// promise a keyless partner embed depends on, and the shell once broke it by not forwarding the engine.
const PROVIDER = argOf('--provider', '')
// KNOWN, ROUTED exemptions from the zero-Mapbox rule — each names its owner. Printed on every run so
// it cannot become invisible; DELETE the entry when the owner lands the fix (the gate then enforces it).
const MAPBOX_KNOWN = {
// EMPTY ON PURPOSE. The one entry here — my-upg-100-list/upg-100-list.vue:1464 pinning
// mapbox-gl.css v3.3.0 regardless of engine — was fixed and the exemption deleted in the SAME
// COMMIT as the fix, so main never had a window where the gate red-flagged its own known bug and
// never had a window where a stale exemption hid a live one. Proven before deletion: this gate
// reported "provider=maplibre made 0 request(s) to api.mapbox.com" in BOTH js and iframe modes,
// with no KNOWN line printed at all.
//
// ⚠ NOT COVERED BY THIS MAP, and deliberately so: the generated leaf HEAD still preloads
// mapbox-gl.css v3.24.0 and mapbox-gl-geocoder v5.0.0. Different versions, different owner —
// that is generate-tree (bundler-1's lane), tracked as
// BUG-LEAF-PAGES-PRELOAD-MAPBOX-EVEN-UNDER-MAPLIBRE. This gate embeds the GALLERY, which does not
// load those leaves, which is why it reads zero today.
}
// ★ FREE PORTS, ASKED OF THE OS — never fixed ones. A stray server left on a fixed port by an
// earlier crashed run answered in place of the tree under test, and this gate PASSED a known-broken
// tree (caught by its own negative control, 2026-09-24): spawn failed with EADDRINUSE silently and
@ -63,8 +83,8 @@ try { ({ chromium } = await import('playwright')) } catch {
let mode = 'js'
const hostPage = () => {
const embed = mode === 'iframe'
? `<iframe src="${TREE}/doxa-maps-spa/doxa-maps-spa/index.html" style="width:100%;height:760px;border:0"></iframe>`
: `<script src="${TREE}/doxa-maps-spa/doxa-maps-spa.js"></script><doxa-maps-spa></doxa-maps-spa>`
? `<iframe src="${TREE}/doxa-maps-spa/doxa-maps-spa/index.html${PROVIDER ? '?provider=' + PROVIDER : ''}" style="width:100%;height:760px;border:0"></iframe>`
: `<script src="${TREE}/doxa-maps-spa/doxa-maps-spa.js"></script><doxa-maps-spa${PROVIDER ? ` provider="${PROVIDER}"` : ''}></doxa-maps-spa>`
return `<!doctype html><html><head><meta charset=utf-8><title>host</title>
<style>body{margin:0} header,footer{height:60px;background:#eee}</style></head>
<body><header id=hdr>HOST HEADER</header><main style="max-width:1200px;margin:0 auto">${embed}</main><footer>HOST FOOTER</footer></body></html>`
@ -80,7 +100,14 @@ const checked = {} // mode -> tabs actually driven; printed, and 0 is a FAIL (
try {
for (mode of MODES) {
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } })
const own = new Set(), foreign = new Set()
const own = new Set(), foreign = new Set(), mapbox = new Set()
let current = '(page load)'
const mapboxBy = {} // tab id (the one open when the request fired) -> distinct mapbox URLs
page.on('request', q => {
if (!/^https:\/\/api\.mapbox\.com\//.test(q.url())) return
const u = q.url().split('?')[0]
mapbox.add(u); (mapboxBy[current] = mapboxBy[current] || new Set()).add(u)
})
const note = (url, why) => (url.startsWith(`http://127.0.0.1:${TREE_PORT}/`) ? own : foreign).add(why + ' ' + url.split('?')[0])
page.on('response', r => { if (r.status() >= 400) note(r.url(), String(r.status())) })
page.on('requestfailed', q => { const e = (q.failure() && q.failure().errorText) || ''; if (!/ABORTED/.test(e)) note(q.url(), e) })
@ -102,6 +129,7 @@ try {
const tabs = await frame.evaluate(() => document.querySelector('doxa-maps-spa')._instance.proxy.tabs.map(t => ({ id: t.id, discovered: !!t.discovered })))
for (const { id, discovered } of tabs) {
if (ONLY && !ONLY.includes(id)) continue
current = id
await frame.evaluate(id => document.querySelector('doxa-maps-spa')._instance.proxy.onSelect(id), id)
let v
for (let k = 0; k < 50; k++) {
@ -125,6 +153,17 @@ try {
if (!ok) fails.push(`[${mode}] ${id}: ${why}`)
}
for (const f of own) { console.log(`FAIL [${mode}] tree request failed: ${f}`); fails.push(`[${mode}] ${f}`) }
if (PROVIDER === 'maplibre') {
for (const u of [...mapbox]) if (MAPBOX_KNOWN[u]) { console.log(`KNOWN [${mode}] ${u} — owner: ${MAPBOX_KNOWN[u]}`); mapbox.delete(u); for (const k in mapboxBy) { mapboxBy[k].delete(u); if (!mapboxBy[k].size) delete mapboxBy[k] } }
const ok = mapbox.size === 0
console.log(`${ok ? 'PASS' : 'FAIL'} [${mode}] provider=maplibre made ${mapbox.size} request(s) to api.mapbox.com`)
if (!ok) {
for (const [tab, urls] of Object.entries(mapboxBy)) {
console.log(` while ${tab} was open: ${urls.size} — ${[...urls].slice(0, 3).join(' , ')}`)
fails.push(`[${mode}] provider=maplibre: ${tab} hit api.mapbox.com (${urls.size} distinct URL(s))`)
}
}
}
for (const f of foreign) console.log(`note [${mode}] third-party request failed (not gated): ${f}`)
await page.close()
}

View file

@ -1,6 +1,32 @@
/**
* WAGF region color strategy — colors pins by the region they belong to.
*
* ★ THE VALUES ARE THE RESEARCH MAP'S, ON THE DRIVER'S INSTRUCTION (2026-09-24). He compared the
* two surfaces himself: "when i look on teh color ffor th regiosn tab of the simpel research map i
* don tsee the you using htes colors on the reigosn dashbarod hwen you shoudl". Two surfaces
* disagreeing about what Africa's colour means is a product defect whichever palette is prettier,
* and he chose which one wins. Source of truth:
* plugin-designs/doxa-research-map/src/colors/doxa-region.js, which the research dashboard and
* three other consumers already paint from.
*
* WHAT THE CHANGE COST, measured rather than glossed (dataviz validate_palette.js, light mode,
* surface #fcfcfb — the same tool and surface the previous palette was stepped against):
* Lightness band FAIL #1ee05f 0.793 · #FFEB3B 0.928 · #00e0bd 0.810 sit above the 0.43–0.77 band
* Chroma floor PASS all 7 >= 0.1
* CVD separation PASS worst adjacent #FFEB3B<->#1ee05f dE 8.7 protan (>= 8), tritan 14.7
* Normal vision PASS worst adjacent dE 21.0
* Contrast WARN under 3:1 vs the surface: #1ee05f 1.72 · #FFEB3B 1.19 · #00e0bd 1.65 · #ff7f11 2.47
* The palette this replaced passed all five. So the trade is real and it is recorded here rather
* than in a commit message nobody will read next year.
*
* WHY THE CONTRAST WARN IS SURVIVABLE HERE, and it is a reason, not a dismissal: that check
* obligates relief through a secondary encoding, and both surfaces already carry one — the chooser
* renders a LEGEND of labelled chips in these exact colours, and the dashboard renders the same
* seven regions as a TABLE with names and figures. Identity never rests on hue alone on either.
* The LIGHTNESS FAIL is the part with no relief: three of these read faintly as PINS on a light
* basemap. Darkening those three by ~10% L would enter the band while keeping the hue
* recognisable, and that is the Driver's call to make, not mine to make quietly — asked, not done.
*
* WHY THIS EXISTS. The regions element's no-selection state is a CHOOSER: the Driver's brief was
* "some type of map that would show the regions and countries and let you select one". For the
* seven regions to read AS regions the moment the map loads, the pins have to be colored by
@ -37,7 +63,13 @@
export const PROPERTY_KEY = 'region'
const DEFAULT_COLOR = '#6d766e' // DX-life --color-brand-lighter: an unknown region reads as chrome
// DX-life --color-brand-lighter: an unknown region reads as chrome, not as an eighth region.
// ★ DELIBERATELY NOT THEIRS. doxa-region.js uses '#0098FF' for null/undefined and keys
// 'No WAGF Region/Bloc' to '#1a1a2e'. Neither is a REGION colour — they are what an absent value
// looks like — and the Driver's instruction was about the seven regions. A bright blue for
// "unknown" would also read as a region on a map whose every other pin is a region. Every row in
// this feed carries a wagf_region, so this value is almost never painted; it is chrome on purpose.
const DEFAULT_COLOR = '#6d766e'
/** Region value → display label, as the prayer API labels them. Used by the legend. */
export const WAGF_REGIONS = {
@ -50,15 +82,22 @@ export const WAGF_REGIONS = {
'oceania': 'Oceania',
}
/** Region value → color, in the validated order. Do not reorder without re-running the validator. */
/**
* Region value → color. THE RESEARCH MAP'S VALUES, so the two surfaces agree — see the header.
* Mapped from its label-keyed table: Africa #ff2e2e, Asia #1a8cff, Europe #1ee05f,
* Latin America & Caribbean #FFEB3B, Middle East #a83dff,
* North America & Non-Spanish Caribbean #00e0bd, Oceania #ff7f11.
* The order is still the fixed alphabetical one (colour follows the entity, never its rank), which
* is also the order the adjacent-pair CVD result in the header was measured in.
*/
export const PALETTE = {
'africa': '#2563eb',
'asia': '#d97706',
'europe': '#7c3aed',
'latin_america_&_caribbean': '#15803d',
'middle_east': '#dc2626',
'north_america_&_non-spanish_caribbean': '#0891b2',
'oceania': '#be185d',
'africa': '#ff2e2e',
'asia': '#1a8cff',
'europe': '#1ee05f',
'latin_america_&_caribbean': '#FFEB3B',
'middle_east': '#a83dff',
'north_america_&_non-spanish_caribbean': '#00e0bd',
'oceania': '#ff7f11',
}
/** The fixed render order — the same order the palette was validated in. */

View file

@ -61,7 +61,13 @@ const visible = ref(false)
const hintEl = ref(null)
let watchTimer = null
let movedTo = null // the element we re-parented into, so we can put ourselves back
let attribRO = null // observes the attribution control (it resizes on its own, see below)
let attribRO = null
// The root this pill was mounted in, kept so a later clamp can look for obstacles (the phone
// legend sheet) that did not exist at mount time.
let hintRoot = null
let sheetRO = null
let sheetWatched = null
let settleTimers = [] // observes the attribution control (it resizes on its own, see below)
let hostRO = null // observes the pill's layout parent, so a LATE layout re-runs the clamp
let attribEl = null
@ -122,6 +128,45 @@ function findMapBox(canvas, root) {
* clipped, and leaves a correctly-placed pill untouched — so every surface that already worked is
* byte-identical in behaviour.
*/
/**
* The phone legend sheet is an obstacle too, not just the fold.
*
* MEASURED 2026-09-24 at 390x844 on doxa-research-map: the pill sat at y=700 and the open sheet
* began at y=603, so "Tap a coloured dot — someone still needs prayer" printed straight across the
* Progress rows — 1,418 Needs Prayer and the Full Prayer Coverage line were unreadable underneath
* it. It is the phone screenshot the README showcases. Nothing was clipped, so the existing
* below-the-fold clamp had nothing to say: the pill was fully visible and fully in the way.
*
* Same lever as the attribution strip, which this component already treats as a floor rather than
* something to cover: find the sheet, and if the pill would cross its top edge, lift by exactly the
* overlap. Only when the two actually intersect horizontally — a sheet that does not sit under the
* pill is not an obstacle — and only ever upward, so every surface with no sheet is unchanged.
*/
function sheetFloor(pill) {
const sheet = hintRoot?.querySelector?.('.legend-mobile-sheet')
if (!sheet) return Infinity
// ★ ATTACH THE OBSERVER HERE, NOT AT MOUNT. Measured: the sheet does not exist yet when the pill
// mounts — LegendMobile renders after the map's first canvas — so a mount-time querySelector
// found nothing, no observer was attached, and the clamp never ran again. The pill stayed exactly
// where it was and the "fix" moved it by 0px. Binding lazily, the first clamp that SEES a sheet
// is the one that starts watching it.
if (sheetRO && sheetWatched !== sheet) { try { sheetRO.disconnect() } catch (_) {} sheetRO = null }
if (!sheetRO && typeof ResizeObserver !== 'undefined') {
try {
sheetRO = new ResizeObserver(() => clampIntoView())
sheetRO.observe(sheet)
sheetWatched = sheet
} catch (_) { sheetRO = null }
}
try {
const sr = sheet.getBoundingClientRect()
// A collapsed sheet is a thin strip; still an obstacle, but only if it is really there.
if (!(sr.height > 16) || !(sr.width > 16)) return Infinity
const apart = pill.right <= sr.left || sr.right <= pill.left
return apart ? Infinity : sr.top
} catch (_) { return Infinity }
}
function clampIntoView() {
const el = hintEl.value
if (!el) return
@ -138,7 +183,10 @@ function clampIntoView() {
el.style.removeProperty('--pin-hint-lift')
const r = el.getBoundingClientRect()
if (!r.height) return
const overflow = Math.round(r.bottom - window.innerHeight)
// The lowest thing the pill may touch: the fold, or the top of the phone legend sheet when one
// is open beneath it. Whichever is higher wins.
const floor = Math.min(window.innerHeight, sheetFloor(r))
const overflow = Math.round(r.bottom - floor)
if (overflow > 0) el.style.setProperty('--pin-hint-lift', (overflow + 8) + 'px')
} catch (_) { /* detached — nothing to clamp */ }
}
@ -196,6 +244,7 @@ onMounted(() => {
const inst = getCurrentInstance()
const root = inst?.proxy?.$el?.getRootNode?.()
if (!root || typeof root.querySelector !== 'function') return
hintRoot = root
let tries = 0
watchTimer = setInterval(() => {
tries++
@ -215,6 +264,11 @@ onMounted(() => {
// The attribution ResizeObserver only fires when THAT control resizes, which need not
// coincide with the host settling. Watch the pill's own offsetParent as well, so a late
// layout (a composite sizing its nested child after mount) triggers a re-measure.
// RE-CLAMP AS THE PAGE SETTLES. The phone legend sheet mounts AFTER the map's first canvas,
// so the clamp above runs while the only floor is the fold. These three re-measures catch
// the sheet arriving; once one of them sees it, sheetFloor() binds a ResizeObserver and the
// sheet's own drags and collapses keep the pill clear from then on.
settleTimers.push(setTimeout(clampIntoView, 1200), setTimeout(clampIntoView, 3000), setTimeout(clampIntoView, 6000))
try {
const watch = (hintEl.value && hintEl.value.offsetParent) || null
if (watch && typeof ResizeObserver !== 'undefined') {
@ -233,6 +287,10 @@ onBeforeUnmount(() => {
if (watchTimer) clearInterval(watchTimer)
if (attribRO) { try { attribRO.disconnect() } catch (_) {} attribRO = null } attribEl = null
if (hostRO) { try { hostRO.disconnect() } catch (_) {} hostRO = null }
if (sheetRO) { try { sheetRO.disconnect() } catch (_) {} sheetRO = null } sheetWatched = null
for (const t of settleTimers) clearTimeout(t)
settleTimers = []
hintRoot = null
// We appended our own element into a node Vue does not own. Vue removes the element
// it created, but only from where IT thinks it is — so detach explicitly to be certain
// no orphan pill is left behind in the map container on profile teardown.

View file

@ -47,6 +47,7 @@ import { useI18n } from 'vue-i18n'
// (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.
import { useViewportScrim } from '@map/utils/viewportScrim.js'
import { bundleTreeRoot } from '@map/utils/embedBase.js'
const props = defineProps({
items: { type: Array, required: true },
@ -149,7 +150,29 @@ const props = defineProps({
const plural = computed(() => props.itemNounPlural || `${props.itemNoun}s`)
const base = computed(() => String(props.assetsBase || './assets').replace(/\/+$/, ''))
const previewUrl = (id) => `${base.value}/previews/${id}.webp`
// ★ A SECOND PLACE TO LOOK, because "poster not generated yet" was being shown for posters that
// ARE generated. The DRIVER reported it on Ethiopia, whose country-eth.webp exists (174 of the 175
// records have their .webp; India is the one that legitimately does not and carries its own
// reason). MEASURED here — static build AND dev server, every request 200, zero cards in that
// branch — so the file is not missing; a HOST LAYOUT is. resolveAssetsBase points at
// <treeRoot>/../assets, the shared pool, which is correct for the layout generate-tree documents
// and wrong on a host that publishes only the tree folder. generate-tree ALSO stages a
// self-contained copy INSIDE the tree ("staged 421 runtime asset(s) → doxa-maps/assets"), and
// that copy is reachable on both layouts. So: try the pool, and if the bytes do not arrive, try
// the copy before telling the user anything.
const fallbackBase = computed(() => {
const root = String(bundleTreeRoot() || '').replace(/\/+$/, '')
return root ? `${root}/assets` : ''
})
// 0/absent = first attempt, 1 = the fallback is being tried.
const attempt = ref({})
const previewUrl = (id) => `${attempt.value[id] ? fallbackBase.value : base.value}/previews/${id}.webp`
function onPreviewError(id) {
const fb = fallbackBase.value
if (!attempt.value[id] && fb && fb !== base.value) { attempt.value = { ...attempt.value, [id]: 1 }; return }
missing.value = { ...missing.value, [id]: true }
}
// Sections ordered by TOTAL count (largest first), items within a section by their
// own count — the densest prayer need leads at BOTH levels.
@ -630,11 +653,18 @@ useShadowStyles(
@click="previewIsControl && exportPoster(item)"
@keydown.enter.prevent="previewIsControl && exportPoster(item)"
@keydown.space.prevent="previewIsControl && exportPoster(item)"
@error="missing[item.id] = true" />
@error="onPreviewError(item.id)" />
<div v-else class="ph">
<div class="ph-label">{{ item.label }}</div>
<div v-if="item.count" class="ph-count">{{ item.count.toLocaleString() }} people groups</div>
<div class="ph-note">poster not generated yet</div>
<!-- NOT "not generated yet": reaching this branch means the preview IMAGE did
not arrive after both bases were tried, which says nothing about whether a
poster exists. A record with genuinely no poster carries its own
unsupportedReason and renders the branch above. Saying "not generated" of a
poster that is sitting on disk is how the Driver came to report this.
i18n DEBT: this literal predates t() here and is still untranslated, like
the two beside it. -->
<div class="ph-note">preview unavailable</div>
</div>
</div>

View file

@ -61,5 +61,9 @@
"WAGF Block": "كتلة WAGF",
"WAGF Region": "منطقة WAGF",
"people groups adopted": "الشعوب المنتمية",
"people groups engaged": "مجموعات بشرية مُنخرِطة"
"people groups engaged": "مجموعات بشرية مُنخرِطة",
"Outside this region": "خارج هذه المنطقة",
"Pray": "صلّي",
"Loading region dashboards…": "جارٍ تحميل لوحات المناطق…",
"No regions found.": "لم يتم العثور على مناطق."
}

View file

@ -61,5 +61,9 @@
"WAGF Block": "WAGF-Block",
"WAGF Region": "WAGF-Region",
"people groups adopted": "adoptierte Volksgruppen",
"people groups engaged": "engagierte Volksgruppen"
"people groups engaged": "engagierte Volksgruppen",
"Outside this region": "Außerhalb dieser Region",
"Pray": "Ich bete",
"Loading region dashboards…": "Regions-Dashboards werden geladen…",
"No regions found.": "Keine Regionen gefunden."
}

View file

@ -61,5 +61,9 @@
"WAGF Block": "WAGF Block",
"WAGF Region": "WAGF Region",
"people groups adopted": "people groups adopted",
"people groups engaged": "people groups engaged"
"people groups engaged": "people groups engaged",
"Outside this region": "Outside this region",
"Pray": "Pray",
"Loading region dashboards…": "Loading region dashboards…",
"No regions found.": "No regions found."
}

View file

@ -61,5 +61,9 @@
"WAGF Block": "Bloque WAGF",
"WAGF Region": "Región de la WAGF",
"people groups adopted": "grupos de personas adoptados",
"people groups engaged": "grupos étnicos comprometidos"
"people groups engaged": "grupos étnicos comprometidos",
"Outside this region": "Fuera de esta región",
"Pray": "Oración",
"Loading region dashboards…": "Cargando los paneles de regiones…",
"No regions found.": "No se encontraron regiones."
}

View file

@ -61,5 +61,9 @@
"WAGF Block": "Bloc WAGF",
"WAGF Region": "Région WAGF",
"people groups adopted": "peuples adoptés",
"people groups engaged": "peuples engagés"
"people groups engaged": "peuples engagés",
"Outside this region": "Hors de cette région",
"Pray": "Prier",
"Loading region dashboards…": "Chargement des tableaux de bord des régions…",
"No regions found.": "Aucune région trouvée."
}

View file

@ -61,5 +61,9 @@
"WAGF Block": "WAGF ब्लॉक",
"WAGF Region": "डब्ल्यूएजीएफ क्षेत्र",
"people groups adopted": "लोगों के समूहों ने अपनाया",
"people groups engaged": "प्रवेशित लोगों के समूह"
"people groups engaged": "प्रवेशित लोगों के समूह",
"Outside this region": "इस क्षेत्र के बाहर",
"Pray": "प्रार्थना करें",
"Loading region dashboards…": "क्षेत्र डैशबोर्ड लोड हो रहे हैं…",
"No regions found.": "कोई क्षेत्र नहीं मिला।"
}

View file

@ -61,5 +61,9 @@
"WAGF Block": "Blocco WAGF",
"WAGF Region": "Regione WAGF",
"people groups adopted": "popoli adottati",
"people groups engaged": "popoli coinvolti"
"people groups engaged": "popoli coinvolti",
"Outside this region": "Fuori da questa regione",
"Pray": "Prega",
"Loading region dashboards…": "Caricamento delle dashboard delle regioni…",
"No regions found.": "Nessuna regione trovata."
}

View file

@ -61,5 +61,9 @@
"WAGF Block": "Bloco WAGF",
"WAGF Region": "Região da WAGF",
"people groups adopted": "grupos étnicos adotados",
"people groups engaged": "grupos étnicos engajados"
"people groups engaged": "grupos étnicos engajados",
"Outside this region": "Fora desta região",
"Pray": "Ore",
"Loading region dashboards…": "Carregando os painéis das regiões…",
"No regions found.": "Nenhuma região encontrada."
}

View file

@ -61,5 +61,9 @@
"WAGF Block": "Bloc WAGF",
"WAGF Region": "Regiune WAGF",
"people groups adopted": "popoare adoptate",
"people groups engaged": "popoare implicate"
"people groups engaged": "popoare implicate",
"Outside this region": "În afara acestei regiuni",
"Pray": "Roagă-te",
"Loading region dashboards…": "Se încarcă panourile regiunilor…",
"No regions found.": "Nu au fost găsite regiuni."
}

View file

@ -61,5 +61,9 @@
"WAGF Block": "Блок WAGF",
"WAGF Region": "Регион WAGF",
"people groups adopted": "принятые группы людей",
"people groups engaged": "вовлечённые группы людей"
"people groups engaged": "вовлечённые группы людей",
"Outside this region": "За пределами этого региона",
"Pray": "Молиться",
"Loading region dashboards…": "Загрузка панелей по регионам…",
"No regions found.": "Регионы не найдены."
}

View file

@ -61,5 +61,9 @@
"WAGF Block": "WAGF 板块",
"WAGF Region": "WAGF 区域",
"people groups adopted": "已认领的民族",
"people groups engaged": "已参与的民族"
"people groups engaged": "已参与的民族",
"Outside this region": "在此区域之外",
"Pray": "祷告",
"Loading region dashboards…": "正在加载区域面板…",
"No regions found.": "未找到区域。"
}

View file

@ -29,6 +29,9 @@
selects : the `key` of the canonical row this one stands for. An ALIAS row has its own unique
`key` (for v-for) but clicking it selects `selects`, it highlights whenever that key is
active, and it is skipped by keyboard nav so arrow keys visit each canonical row once.
branch : an id that makes this row FOLDABLE — a chevron beside it shows/hides the rows below.
Closed by default; picking the row also opens it.
ancestors : the `branch` ids above this row; it shows only while ALL of them are open.
Selection is controlled via v-model:active (the active item's `key`). The host owns pane content
and mount logic — this component only paints the SHELL (top bar + rail + a content frame) and
exposes slots so each app keeps its unique chrome + its own mount machinery:
@ -161,14 +164,24 @@
</div>
<div class="vtl-rows" v-show="!grp.folder || !isFolderClosed(grp)">
<div class="vtl-rowwrap" role="presentation" v-for="row in grp.rows" :key="row.key"
v-show="rowShown(row)">
<!-- BRANCH toggle: folds this row's sub-rows (see `branch` / `ancestors`). A sibling of the
tab, never inside it — a button may not contain a button. -->
<button v-if="row.branch" type="button" class="vtl-twisty"
:aria-expanded="String(isBranchOpen(row.branch))"
:aria-label="(isBranchOpen(row.branch) ? 'Hide' : 'Show') + ' what ' + row.label + ' is made of'"
@click="toggleBranch(row.branch)">
<span class="vtl-chev" :class="{ open: isBranchOpen(row.branch) }" aria-hidden="true" v-html="iconSvg('chevron')"></span>
</button>
<button
v-for="row in grp.rows" :key="row.key" type="button" role="tab"
class="vtl-tab" :class="{ active: target(row) === active, alias: !!row.selects }"
type="button" role="tab"
class="vtl-tab" :class="{ active: target(row) === active, alias: !!row.selects, 'has-branch': !!row.branch }"
:data-depth="row.depth || null" :style="row.depth ? { '--vtl-depth': row.depth } : null"
:aria-selected="String(target(row) === active)"
:tabindex="!row.selects && row.key === active ? 0 : -1"
:ref="el => { if (!row.selects) registerTab(row.key, el) }"
@click="pick(target(row))" @keydown="onKey($event)">
@click="pick(target(row), row)" @keydown="onKey($event)">
<span class="vtl-tab-ic" aria-hidden="true" v-if="row.icon" v-html="iconSvg(row.icon)"></span>
<span class="vtl-tab-emoji" aria-hidden="true" v-else-if="row.emoji">{{ row.emoji }}</span>
<!-- Slim-rail fallback (desktop collapsed only): a row with NEITHER icon NOR emoji would
@ -180,6 +193,7 @@
<span class="vtl-tab-sub" v-if="row.sub"><span v-html="row.sub"></span></span>
</span>
</button>
</div>
</div>
</div>
</template>
@ -368,6 +382,11 @@ const VTL_SHADOW_CSS = `
.vtl-tab { display:flex; align-items:center; gap:11px; width:100%; min-height:48px; text-align:left; border:0; background:none; color:var(--vtl-muted); padding:8px 12px; cursor:pointer; border-radius:var(--vtl-radius,8px); font:inherit; margin:0; transition:background .13s ease,color .13s ease; }
.vtl-tab[data-depth] { padding-left:calc(12px + var(--vtl-depth) * 16px); }
.vtl-tab[data-depth]::before { content:''; flex:0 0 auto; align-self:stretch; width:0; margin-right:-3px; border-left:2px solid var(--vtl-accent); opacity:.45; }
.vtl-rowwrap { position:relative; }
.vtl-twisty { position:absolute; z-index:1; right:4px; top:50%; transform:translateY(-50%); width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center; padding:0; border:0; border-radius:8px; background:none; color:var(--vtl-muted); cursor:pointer; }
.vtl-twisty:hover { background:var(--vtl-accented); color:#3b463d; }
.vtl-twisty:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:-2px; }
.vtl-tab.has-branch { padding-right:36px; }
.vtl-tab:hover { background:var(--vtl-accented); color:#3b463d; }
.vtl-tab:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:-2px; }
.vtl-tab.active { background:var(--vtl-accented); color:#3b463d; font-weight:500; }
@ -463,6 +482,7 @@ const VTL_SHADOW_CSS = `
.vtl.rail-collapsed .vtl-tab-emoji { font-size:19px; }
.vtl.rail-collapsed .vtl-tab-fallback { display:inline-flex; }
.vtl.rail-collapsed .vtl-tab[data-depth]::before { display:none; }
.vtl.rail-collapsed .vtl-twisty { display:none; }
.vtl.rail-collapsed .vtl-tier { padding-left:196px; }
.vtl.rail-collapsed .vtl-tl, .vtl.rail-collapsed .vtl-td, .vtl.rail-collapsed .vtl-cap, .vtl.rail-collapsed .vtl-chev { display:none; }
.vtl.rail-collapsed .vtl-grp + .vtl-grp { margin-top:6px; padding-top:6px; }
@ -489,6 +509,7 @@ const VTL_SHADOW_CSS = `
.vtl.rail-collapsed .vtl-tab { padding-left:10px; gap:11px; }
.vtl.rail-collapsed .vtl-tab[data-depth] { padding-left:calc(12px + var(--vtl-depth) * 16px); }
.vtl.rail-collapsed .vtl-tab[data-depth]::before { display:block; }
.vtl.rail-collapsed .vtl-twisty { display:inline-flex; }
.vtl.rail-collapsed .vtl-tab-text { display:flex; }
.vtl.rail-collapsed .vtl-tab-fallback { display:none; }
.vtl.rail-collapsed .vtl-tier { padding-left:8px; }
@ -529,7 +550,7 @@ export default defineComponent({
// tab (so their pane is rendered + element mounted); hostState/hostError track each pane's load.
// A row not in activatedKeys is never rendered — the lazy contract (no visit → no cost).
return {
navOpen: this.initialNavOpen, railCollapsed: this.initialRailCollapsed, _tabEls: {}, folderOverride: {},
navOpen: this.initialNavOpen, railCollapsed: this.initialRailCollapsed, _tabEls: {}, folderOverride: {}, branchOpen: {},
// `narrow` mirrors the (max-width:760px) media query as REACTIVE state, kept current by a resize
// listener. It lets us reconcile the two boolean state machines (navOpen = mobile drawer,
// railCollapsed = desktop slim rail) at the breakpoint and drive rail visibility correctly.
@ -690,7 +711,15 @@ export default defineComponent({
},
// The key a row selects: its own, or — for an ALIAS row — the canonical row it points at.
target(row) { return row.selects || row.key },
pick(key) {
// BRANCHES — a row with `branch` folds the rows whose `ancestors` include it. Closed by default,
// so a deep composition tree costs one line until someone asks to see inside it.
isBranchOpen(id) { return !!this.branchOpen[id] },
toggleBranch(id) { this.branchOpen = { ...this.branchOpen, [id]: !this.branchOpen[id] } },
rowShown(row) { return !row.ancestors || row.ancestors.every(id => this.branchOpen[id]) },
pick(key, row) {
// Choosing a row that owns a branch also OPENS it (never closes it): you asked for that plugin,
// so show what it is made of. Only the chevron closes a branch.
if (row && row.branch && !this.branchOpen[row.branch]) this.toggleBranch(row.branch)
this.$emit('update:active', key)
// On mobile, picking a tab closes the off-canvas drawer. On desktop navOpen must stay true (it gates
// the desktop rail's visibility via railShown), so we only touch it when narrow.
@ -820,6 +849,11 @@ export default defineComponent({
.vtl-tab { display:flex; align-items:center; gap:11px; width:100%; min-height:48px; text-align:left; border:0; background:none; color:var(--vtl-muted); padding:8px 12px; cursor:pointer; border-radius:var(--vtl-radius,8px); font:inherit; margin:0; transition:background .13s ease,color .13s ease; }
.vtl-tab[data-depth] { padding-left:calc(12px + var(--vtl-depth) * 16px); }
.vtl-tab[data-depth]::before { content:''; flex:0 0 auto; align-self:stretch; width:0; margin-right:-3px; border-left:2px solid var(--vtl-accent); opacity:.45; }
.vtl-rowwrap { position:relative; }
.vtl-twisty { position:absolute; z-index:1; right:4px; top:50%; transform:translateY(-50%); width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center; padding:0; border:0; border-radius:8px; background:none; color:var(--vtl-muted); cursor:pointer; }
.vtl-twisty:hover { background:var(--vtl-accented); color:#3b463d; }
.vtl-twisty:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:-2px; }
.vtl-tab.has-branch { padding-right:36px; }
.vtl-tab:hover { background:var(--vtl-accented); color:#3b463d; }
.vtl-tab:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:-2px; }
.vtl-tab.active { background:var(--vtl-accented); color:#3b463d; font-weight:500; }
@ -915,6 +949,7 @@ export default defineComponent({
.vtl.rail-collapsed .vtl-tab-emoji { font-size:19px; }
.vtl.rail-collapsed .vtl-tab-fallback { display:inline-flex; }
.vtl.rail-collapsed .vtl-tab[data-depth]::before { display:none; }
.vtl.rail-collapsed .vtl-twisty { display:none; }
.vtl.rail-collapsed .vtl-tier { padding-left:196px; }
.vtl.rail-collapsed .vtl-tl, .vtl.rail-collapsed .vtl-td, .vtl.rail-collapsed .vtl-cap, .vtl.rail-collapsed .vtl-chev { display:none; }
.vtl.rail-collapsed .vtl-grp + .vtl-grp { margin-top:6px; padding-top:6px; }
@ -941,6 +976,7 @@ export default defineComponent({
.vtl.rail-collapsed .vtl-tab { padding-left:10px; gap:11px; }
.vtl.rail-collapsed .vtl-tab[data-depth] { padding-left:calc(12px + var(--vtl-depth) * 16px); }
.vtl.rail-collapsed .vtl-tab[data-depth]::before { display:block; }
.vtl.rail-collapsed .vtl-twisty { display:inline-flex; }
.vtl.rail-collapsed .vtl-tab-text { display:flex; }
.vtl.rail-collapsed .vtl-tab-fallback { display:none; }
.vtl.rail-collapsed .vtl-tier { padding-left:8px; }

View file

@ -150,6 +150,7 @@
"check:gallery-embed": "node internal/check-research-gallery-embed.mjs # the cap-5 Research Gallery must embed on a FOREIGN ORIGIN (a stand-in host page on localhost, the built tree on 127.0.0.1) in BOTH modes, bare <script>+<doxa-maps-spa> and <iframe>, with the layout inside the host box and every curated tab painting a map. The summary line prints the tab count PER MODE, and a mode that checked zero tabs is a FAIL. One mode only: check:gallery-embed:js or check:gallery-embed:iframe. Starts its own servers on OS-assigned ports; needs only a built tree. Exit 2 = SKIP (no built tree / no playwright).",
"check:gallery-embed:js": "node internal/check-research-gallery-embed.mjs --mode js",
"check:gallery-embed:iframe": "node internal/check-research-gallery-embed.mjs --mode iframe",
"check:gallery-embed:maplibre": "node internal/check-research-gallery-embed.mjs --provider maplibre",
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs # the cap-5 SPA must open on Big Picture and keep the Driver's order (Big Picture, UPG-100, Prayer Progress, Gospel Resources) — asserted as PRESENCE + tab-1 + RELATIVE order on the RENDERED rail, because the rail gained three sections and now renders 12 rows with others between them. ⚠ NOT WAVE-WIRED: it FAILS rather than skips when the SPA is unreachable, deliberately (\"a gate that cannot reach its subject has not passed, it has not run\") — correct standalone, but gates:post runs on checkouts with no server. Needs `bun run serve` on 4173.",
"check:backlog-status": "node internal/check-backlog-status-enum.mjs",
"check:backlog-refs": "node internal/check-backlog-refs.mjs # a card may not depend on a card that does not exist. A dangling depends_on can never be done, so the card can never become ready and nothing says so — it reads as ordinary blocked work forever. FAILS only for depends_on on an OPEN card; a dangling parent or a stale link on a done card is REPORTED, because failing on filing debt teaches people to delete the reference rather than restore the card. Reads all three phase backlogs — references legitimately cross phases. SKIPs when _qa-cycle is absent.",
@ -167,8 +168,9 @@
"check:doc-scripts:self-test": "node internal/check-doc-quotes-scripts.mjs --self-test",
"check:legend-source": "node internal/check-legend-matches-source.mjs # the numbers the maps SHOW must match the numbers the live SOURCE has. Every other gate checks INTERNAL consistency — a page can agree with itself perfectly while its legend has stopped describing reality. Offline SKIPs (exit 0); only a reachable endpoint that disagrees fails.",
"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:wp-embed check:doc-scripts",
"gates:post": "node internal/run-parallel.mjs --label=post -- 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:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status 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:legend-source check:backlog-refs",
"gates:post": "node internal/run-parallel.mjs --label=post -- 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:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status 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:legend-source check:backlog-refs check:composite-embed-knobs",
"build:bundles": "node internal/run-parallel.mjs --bundles",
"check:composite-embed-knobs": "node internal/check-composite-embed-knobs.mjs # a composite must pass the HOST's tk and provider down to the map it nests, proven on a genuinely foreign origin (tree served on one port, partner page on another). Our own leaves bake a token and assign window.MAP_TOKEN, so this only ever breaks where there is no leaf - the partner page the embed snippet is written for, and the case nothing else tested. Keyless maplibre asserts ZERO requests to api.mapbox.com, because 'it rendered' would pass on a build that quietly fell back with a baked key.",
"check:maplibre-drops": "node internal/check-maplibre-layer-drops.mjs",
"check:tabs-localized": "node internal/check-tabs-localized.mjs # a chosen locale must CHANGE the rendered tab text. check:i18n-parity compares catalogs to each OTHER, so it cannot tell a shipped translation from a dead one; tabs.json is reached only via a DYNAMIC key (SemanticTreeLegend.vue:40), invisible to any grep. Renders it and reads the result.",
"check:packets-all": "node internal/check-packets-all.mjs # runs check:packet-render over EVERY generated packet, not one hand-named file. The single-file gate SKIPs with no argument, so it was in package.json, passing in all 11 locales, and in neither gates:pre nor gates:post — it only ran when someone typed a filename. SKIPs when no packets are generated.",

View file

@ -13,8 +13,9 @@
* Same "import the REAL components, no fork" discipline as big-picture: the grid is the exact
* people-groups-grid component; the dashboard is the exact research-dashboard component.
*/
import { reactive, provide, ref } from 'vue'
import { reactive, provide, ref, computed } from 'vue'
import { resolveEmbedBase } from '@map/utils/embedBase.js'
import { useSharedMapToken, publishMapToken } from '@map/composables/useSharedMapToken.js'
import StackLayout from '@map/layouts/StackLayout.vue'
import ResearchDashboard from '../research-dashboard/research-dashboard.vue'
import PeopleGroupsGrid from '../people-groups-grid/people-groups-grid.vue'
@ -217,13 +218,58 @@ function onDashboardFilter({ filterSet = {}, searchTerm = '' } = {}) {
// Note this is exactly the two-host defect class: research-dashboard standalone PASSES the same
// gate (1/1, verified this tick), and the identical component inside this composite FAILED. Only a
// gate that drives BOTH hosts can tell those apart, which is why the composite is now a target.
// ★ THE HOST'S OWN profile-config, WHICH THIS COMPOSITE USED TO THROW AWAY.
// It declared no props, so a partner's <dashboard-plus-grid profile-config='{"tk":"..."}'> lost its
// token entirely. MEASURED on a third-party origin before this: 'No Mapbox access token provided',
// 0 style and 0 tile requests to api.mapbox.com, no canvas — the JS embed, one of the two shipped
// embed methods, mounted a dashboard whose map could not draw. Our own leaf hides it, because that
// page carries a BAKED token; the failure exists only where there is no leaf, which is exactly the
// partner case. Same defect and same shape as research-map-plus-grid.
const props = defineProps({
/** The host's JSON config, exactly as the embed snippet writes it. */
profileConfig: { type: [String, Object], default: null },
})
const hostConfig = computed(() => {
const raw = props.profileConfig
if (!raw) return {}
if (typeof raw === 'object') return raw
// A malformed attribute must not take the element down with it.
try { return JSON.parse(raw) } catch { return {} }
})
const hostToken = computed(() => {
const tk = hostConfig.value?.tk
return typeof tk === 'string' && tk ? tk : ''
})
if (hostToken.value) publishMapToken(hostToken.value)
// The provider is a host knob as well: resolveMapProvider reads profile-config.provider, and the
// config the child sees is the one provided below. Dropping it left a host that asked for maplibre
// on mapbox — demanding a token for the one configuration that exists to need none.
const hostProvider = computed(() => {
const p = hostConfig.value?.provider
return p === 'maplibre' || p === 'mapbox' ? p : ''
})
// ★ AND PROVIDE IT THE WAY ProfileLoader WOULD. This composite bakes its children in rather than
// mounting them through ProfileLoader, and ProfileLoader is the ONLY provider of 'mapboxToken' in
// the repo — so DashboardMapPanel's inject('mapboxToken', ref('')) fell through to its empty
// default here. The shared page token keeps a late-arriving token working (the leaf FETCHES one),
// which is what the composable exists for; the host's own tk still wins.
const sharedToken = useSharedMapToken()
const mapboxToken = computed(() => hostToken.value || sharedToken.value || '')
provide('mapboxToken', mapboxToken)
const SHARE_LEAF = 'dashboard-plus-grid/dashboard-plus-grid/index.html'
const shareBase = resolveEmbedBase(null)
provide('profileConfig', ref({
provide('profileConfig', computed(() => ({
profile: 'research-dashboard',
layout: 'page-scroll',
shareUrl: (shareBase ? shareBase.replace(/\/+$/, '') + '/' : '') + SHARE_LEAF,
}))
// Spread last so a host that supplies none leaves the key absent, and never overwrite the two
// settings above — those are this composite's own layout and share decisions, not the host's.
...(hostToken.value ? { tk: hostToken.value } : {}),
...(hostProvider.value ? { provider: hostProvider.value } : {}),
})))
const sections = [
// The research-dashboard leaf SELF-TITLES (its own DashboardTopBar renders a "Research Dashboard"

View file

@ -39,6 +39,7 @@ Add `?tab=<id>` to the `src` to open a specific map.
| `tab` | device default (Big Picture on desktop, Simple Research on phones) | Tab id to open first. Changing it later switches tabs. `?tab=` on the page URL works too. |
| `language` | parent page / saved choice | UI language pushed to every tab (`en`, `es`, `fr`, …). |
| `api-base-url` | manifest `runtime.apiBaseUrl` | Data API. Only applied when the page has not already set `window.MAP_APP_API_URL`. |
| `provider` | each map's own default (Mapbox) | Map engine for every nested map: `maplibre` needs no token and makes no Mapbox requests; `mapbox`. Forwarded to each map and, when the page has not set `window.MAP_PROVIDER`, set there too so maps nested inside composites follow. In the iframe embed use `?provider=maplibre`. |
| `tk` | manifest `runtime.token` | Mapbox public token. Only fills `window.MAP_TOKEN` when the page has not set one. |
| `token-url` | none | Token endpoint forwarded to each map. |
| `tree-base` | derived from the script URL | Pin the tree root explicitly (needed only when the script is re-hosted apart from its tree). |
@ -74,5 +75,6 @@ Both headers are **required** on a host other than the one serving the tree:
## Prove it
```sh
bun run check:gallery-embed # serves the built tree + a foreign host page, drives every tab in both modes
bun run check:gallery-embed # serves the built tree + a foreign host page, drives every tab in both modes
bun run check:gallery-embed:maplibre # same, keyless: provider=maplibre must make zero api.mapbox.com requests
```

View file

@ -737,6 +737,9 @@ export default defineComponent({
// the tree's own manifest.json (its `runtime` block), so a bare two-line embed needs no config.
// See EMBED.md beside this file.
apiBaseUrl: { type: String, default: '' },
// The map engine for every nested map: 'maplibre' (keyless) or 'mapbox'. Also accepted as
// profile-config.provider. Unset = each map's own ladder (?provider= → window.MAP_PROVIDER → mapbox).
provider: { type: String, default: '' },
// Which tab opens first, by tab id (e.g. "upg-100", "gospel-resources"). Lets a host page deep-
// link into one map of the gallery. Falls back to ?tab= on the page URL, then the device default.
tab: { type: String, default: '' }
@ -905,6 +908,11 @@ export default defineComponent({
const fromProp = (this.language || this.cfg.language || this.cfg.locale || this.cfg.lang || '').trim()
return (fromProp || this.locale || '').split('-')[0].toLowerCase()
},
// The engine a HOST asked for ('' = none; each map keeps its own ladder).
effectiveProvider() {
const p = String(this.provider || this.cfg.provider || '').trim().toLowerCase()
return (p === 'maplibre' || p === 'mapbox') ? p : ''
},
activeKey() {
const t = this.tabs[this.activeIndex]
return t ? t.id : ''
@ -1239,6 +1247,11 @@ export default defineComponent({
const tk = this.tk || this.cfg.tk || this.runtime.token
if (tk) pc.tk = tk
if (this.tokenUrl || this.cfg.tokenUrl) pc.tokenUrl = this.tokenUrl || this.cfg.tokenUrl
// ★ AND THE ENGINE. resolveMapProvider's ladder starts at profile-config.provider, and a child
// reads only the config written HERE — so a gallery asked for maplibre mounted children that
// resolved mapbox (bundler-2 measured 19 api.mapbox.com requests for a maplibre request on the
// studio, the same hole). Validated to the two known engines so a typo cannot pick a third.
if (this.effectiveProvider) pc.provider = this.effectiveProvider
// PASS THE TREE BASE DOWN — a nested cap-5 shell (or any element that fetches the manifest)
// cannot guess it, and guessing is what produced the original 404 class.
if (this.base) pc.treeBase = this.base
@ -1259,6 +1272,14 @@ export default defineComponent({
if (api && !window.MAP_APP_API_URL) window.MAP_APP_API_URL = api.replace(/\/+$/, '')
const tk = this.tk || this.cfg.tk || this.runtime.token
if (tk && !window.MAP_TOKEN) window.MAP_TOKEN = tk
// window.MAP_PROVIDER — the engine, when a host explicitly asked this gallery for one.
// childConfig forwards it as profile-config.provider, which the simple map elements honour,
// but a COMPOSITE mounts its inner maps without passing its own config's provider on.
// Measured 2026-09-24 with provider="maplibre", one tab per clean page: Prayer Progress and
// All People Groups made 0 api.mapbox.com requests, while Big Picture (19), Regions (17),
// UPG-100 (15) and Simple Research (14) loaded mapbox-gl and Mapbox styles. The page global
// is rung 3 of every map's resolveMapProvider ladder, so it reaches maps nested at any depth.
if (this.effectiveProvider && !window.MAP_PROVIDER) window.MAP_PROVIDER = this.effectiveProvider
},
// Mount a tab's REAL custom element: inject the bundle's script once, wait for the tag to be
// registered, then create it in the pane's empty slot. This is the ONE path for every tab.

View file

@ -1458,8 +1458,14 @@ onMounted(async () => {
// custom MapToolbar buttons never needed mapbox ctrl CSS, so this profile
// skipped it until the shared GeocoderComponent (a real mapbox ctrl) arrived.
// Mirrors src/composables/useMapInstance.js ensureMapboxCSS (idempotent).
// ⚠ MAPBOX ONLY. This ran unconditionally, so a keyless maplibre embed still pulled a
// stylesheet from api.mapbox.com — token-free and harmless to render, but wrong-engine
// leakage on a page whose whole point is having no Mapbox dependency. It was the last
// mapbox-under-maplibre reference in this profile, carried as a named exemption in the
// maplibre-embed gate; guarding it lets that exemption be deleted and enforced instead.
// MapLibre ships its own ctrl CSS via ensureEngineScript, so nothing is lost.
try {
const shadow = mapEl.value && mapEl.value.getRootNode()
const shadow = !isMaplibre && mapEl.value && mapEl.value.getRootNode()
if (shadow instanceof ShadowRoot) {
const MAPBOX_CSS = 'https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css'
if (!shadow.querySelector(`link[href="${MAPBOX_CSS}"]`)) {

View file

@ -292,6 +292,12 @@ const HEAVY_ELEMENTS = new Set([
// doxa-maps-spa's NOT_EXPERIMENTAL already hides big-picture for the same reason.
const STUDIO_HIDDEN = new Set(['big-picture'])
// DISPLAY NAMES — what a row is CALLED, keyed by "<bundle>/<profile>". Display only: the row key, the
// URL hash, the stored last-tab and the <tag> under the name are all unchanged, so links and memory
// keep working. The Driver's name for the cap-5 shell is "Research Gallery" (its meta.json label);
// the manifest does not publish meta labels, so the studio carries the one it needs here.
const DISPLAY_NAME = { 'doxa-maps-spa/doxa-maps-spa': 'Research Gallery' }
// Load a bundle exactly once per URL; resolves after its define() has run.
//
// ★ A CLASSIC SCRIPT FIRES onload AND THEN FAILS, for a dev-server bundle. Measured 2026-09-22:
@ -340,6 +346,10 @@ export default defineComponent({
props: {
treeBase: { type: String, default: '' },
profileConfig: { type: [String, Object], default: '' },
// The map engine, as an attribute as well as a profile-config key — the studio previews map
// plugins, so a host showing it to partners wants to flip both at once. Forwarded to every pane
// below; an unrecognised value is ignored there rather than passed on.
provider: { type: String, default: '' },
// NOT `title` — that name collides with the NATIVE HTMLElement.title property.
// defineCustomElement maps every prop to an attribute, and `title` already exists on every
// element (the tooltip), so Vue warned on EVERY mount:
@ -418,17 +428,32 @@ export default defineComponent({
navGroups() {
const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
const treeOf = new Map(this.compositionTrees.map(t => [t.nodes[0].selects, t]))
// ★ FOLDED BY DEFAULT (Driver, 2026-09-24: "it needs to be collapsible, otherwise it is too
// hard to know how to navigate so many nested plugins"). Every node with parts is a BRANCH with
// its own chevron, closed until opened — so the rail first reads as a plain list of plugins, and
// you drill into one composite, then one level of it, at a time. Picking a composite opens it.
// Branch ids are namespaced by the composite's row key: the same child (dashboard-plus-grid)
// folds independently under doxa-maps-spa and in its own CAP 4 entry.
const parts = (n) => n + (n === 1 ? ' part' : ' parts')
const rowsFor = (r) => {
const out = [{ key: r.key, label: r.label, icon: r.icon, sub: '<code>&lt;' + esc(r.tag) + '&gt;</code>' }]
const t = treeOf.get(r.key)
const bid = (id) => 'b:' + r.key + ':' + id
const out = [{
key: r.key, label: r.label, icon: r.icon,
branch: t ? bid(0) : undefined,
sub: '<code>&lt;' + esc(r.tag) + '&gt;</code>' + (t ? ' · ' + parts(t.nodes[0].kids) : '')
}]
if (t) {
t.nodes.slice(1).forEach((n, i) => out.push({
key: 'tree-' + r.key + '-' + i,
t.nodes.slice(1).forEach((n) => out.push({
key: 'tree-' + r.key + '-' + n.id,
selects: n.selects || ('none:' + r.key),
depth: n.depth,
label: n.label,
badge: n.part ? 'part' : '',
sub: n.part ? 'no standalone page' : 'CAP ' + n.cap + ' · <code>&lt;' + esc(n.tag) + '&gt;</code>'
branch: n.kids ? bid(n.id) : undefined,
ancestors: n.ancestors.map(bid),
sub: n.part ? 'no standalone page'
: 'CAP ' + n.cap + ' · <code>&lt;' + esc(n.tag) + '&gt;</code>' + (n.kids ? ' · ' + parts(n.kids) : '')
}))
}
return out
@ -460,7 +485,9 @@ export default defineComponent({
// own page because it has none, so it opens the composite it lives in.
compositionTrees() {
const out = []
const walk = (row, depth, path, nodes) => {
// Every node records `ancestors` (the ids of the nodes above it) and, if anything was nested
// under it, `kids` — which is what lets the rail fold each level independently.
const walk = (row, depth, path, nodes, ancestors) => {
const b = this.manifestBundles.find(x => x.bundle === row.bundle)
const refs = (row.cap >= 4 && b && b.composes) || []
const kids = []
@ -475,12 +502,15 @@ export default defineComponent({
}
kids.sort((a, b2) => b2.cap - a.cap || a.label.localeCompare(b2.label))
for (const k of kids) {
nodes.push({ label: k.label, tag: k.tag, cap: k.cap, selects: k.key, depth: depth + 1 })
if (!path.has(k.key)) walk(k, depth + 1, new Set([...path, k.key]), nodes)
const node = { id: nodes.length, label: k.label, tag: k.tag, cap: k.cap, selects: k.key, depth: depth + 1, ancestors, kids: 0 }
nodes.push(node)
const before = nodes.length
if (!path.has(k.key)) walk(k, depth + 1, new Set([...path, k.key]), nodes, [...ancestors, node.id])
node.kids = nodes.filter((n, i) => i >= before && n.depth === depth + 2).length
}
// Unpublished parts have no tier, so they sort LAST — below every capability — and open the
// composite they live in.
for (const ref of parts) nodes.push({ label: ref, part: true, selects: row.key, depth: depth + 1 })
for (const ref of parts) nodes.push({ id: nodes.length, label: ref, part: true, selects: row.key, depth: depth + 1, ancestors, kids: 0 })
}
for (const b of this.manifestBundles) {
if (!b.composes || !b.composes.length || STUDIO_HIDDEN.has(b.bundle)) continue
@ -489,8 +519,9 @@ export default defineComponent({
// this rail already IS its composition.
const own = this.rows.find(r => r.bundle === b.bundle)
if (!own) continue
const nodes = [{ label: own.label, tag: own.tag, cap: own.cap, selects: own.key, depth: 0 }]
walk(own, 0, new Set([own.key]), nodes)
const nodes = [{ id: 0, label: own.label, tag: own.tag, cap: own.cap, selects: own.key, depth: 0, ancestors: [] }]
walk(own, 0, new Set([own.key]), nodes, [0])
nodes[0].kids = nodes.filter(n => n.depth === 1).length
if (nodes.length > 1) out.push({ bundle: b.bundle, nodes })
}
return out
@ -536,7 +567,7 @@ export default defineComponent({
const slot = this._slots && this._slots[this.activeKey]
const el = slot && slot.firstElementChild
if (el && el.setAttribute && row) {
const pc = { profile: row.label, instanceId: 'ps-' + this.activeKey.replace(/\//g, '-'), locale: code }
const pc = { profile: row.profile, instanceId: 'ps-' + this.activeKey.replace(/\//g, '-'), locale: code }
if (this.tk || this.cfg.tk) pc.tk = this.tk || this.cfg.tk
el.setAttribute('profile-config', JSON.stringify(pc))
}
@ -596,7 +627,10 @@ export default defineComponent({
out.push({
key: b.bundle + '/' + p.profile,
bundle: b.bundle,
label: p.profile,
label: DISPLAY_NAME[b.bundle + '/' + p.profile] || p.profile,
// The REAL profile id, handed to the element in profile-config. Never the label: a display
// name would mount <doxa-maps-spa> with profile "Research Gallery", which it does not have.
profile: p.profile,
tag,
cap,
icon: CAP_ICON[cap] || 'embed',
@ -715,9 +749,16 @@ export default defineComponent({
// renders in the studio's active language (ProfileLoader reads profile-config.locale).
// tk/tokenUrl ride along here now that map rows mount directly — this is the token ladder
// the removed iframe src used to carry as a query param.
const pc = { profile: row.label, instanceId: 'ps-' + row.key.replace(/\//g, '-'), locale: this.locale }
const pc = { profile: row.profile, instanceId: 'ps-' + row.key.replace(/\//g, '-'), locale: this.locale }
if (this.tk || this.cfg.tk) pc.tk = this.tk || this.cfg.tk
if (this.tokenUrl || this.cfg.tokenUrl) pc.tokenUrl = this.tokenUrl || this.cfg.tokenUrl
// ★ AND THE ENGINE, for the same reason tk rides along. resolveMapProvider's ladder starts
// at profile-config.provider, and a pane reads the config THIS file writes — so a studio
// embedded with provider=maplibre showed mapbox panes and, on a keyless host, panes that
// asked for a token. MEASURED on a foreign origin: 19 requests to api.mapbox.com for a
// maplibre request. Validated to the two known engines so a typo cannot select a third.
const wantProvider = this.provider || this.cfg.provider
if (wantProvider === 'maplibre' || wantProvider === 'mapbox') pc.provider = wantProvider
// PASS THE TREE BASE DOWN — a nested cap-5 shell cannot guess it.
// doxa-maps-spa carries its OWN hardcoded, TREE-RELATIVE tab list (doxa-maps-spa.vue:265-312,
// e.g. 'doxa-research-map/research-map/index.html') and resolves it against ITS treeBase,

View file

@ -20,10 +20,19 @@
// rem values would resolve against the host's root (16px by default) and every space would be
// 1.6x too large. That is exactly why these were resolved to px in the first place.
//
// THE VALUES ARE OURS AS THEY ALREADY WERE, not re-derived from live. Live carries more precision
// (2.352637rem = 23.52637px vs our 23.5264px). Naming the numbers and changing the numbers are two
// different changes; this one only names them, which is what lets a full computed-style
// fingerprint prove it altered nothing. Re-deriving would be a separate, separately measured step.
// THE VALUES ARE NOW LIVE'S FULL PRECISION. They started as the rounded numbers this file was
// created to name (23.5264px for --spacing-lg, where live is 2.352637rem = 23.52637px), and the
// header said re-deriving them would be "a separate, separately measured step". THIS IS THAT STEP,
// and a measurement forced it: their country-list bullet is sized in em, 1.5em of --font-size-md,
// and against live it came out 29.2812px where theirs is 29.2969px. 0.0157px, which is nothing to
// look at — but it is the ROUNDING of --font-size-md (19.5312 for their 19.53125) multiplied by
// 1.5, and every other em- or percentage-derived length on the page inherits the same error. The
// scales are exact powers of their two ratios (type 1.25, spacing 1.33) on a 10px root, so full
// precision is available for free and there is no reason to ship a rounded clone.
// type: 10px * 1.25^n -> 15.625 / 19.53125 / 24.4140625 / 30.517578125 / ...
// spacing: 10px * 1.33^n -> 13.3 / 17.689 / 23.52637 / 31.2900721 / ...
// The vs-live diffs compare with a +/-0.5px tolerance, so this cannot be what turns one green; the
// bullet's exact-equality compare is what it is for.
//
// Mapping verified against live's own custom properties, not arithmetic:
// spacing 3xs .5653230821rem · 2xs .7518796992 · md 1.7689 · lg 2.352637 · xl 3.12900721
@ -36,10 +45,10 @@
// a magic number because it reads as deliberate.
export const DXLIFE_TOKENS =
':host{' +
' --spacing-3xs:5.6532px; --spacing-2xs:7.5188px; --spacing-md:17.689px;' +
' --spacing-lg:23.5264px; --spacing-xl:31.2901px; --spacing-2xl:41.6158px;' +
' --spacing-3xl:55.349px; --spacing-4xl:73.6142px;' +
' --font-size-sm:15.625px; --font-size-md:19.5312px; --font-size-lg:24.4141px;' +
' --font-size-xl:30.5176px; --font-size-2xl:38.147px; --font-size-3xl:47.6837px;' +
' --font-size-4xl:59.6046px; --font-size-5xl:74.5058px;' +
' --spacing-3xs:5.653230821px; --spacing-2xs:7.518796992px; --spacing-md:17.689px;' +
' --spacing-lg:23.52637px; --spacing-xl:31.2900721px; --spacing-2xl:41.615795893px;' +
' --spacing-3xl:55.349008538px; --spacing-4xl:73.614181355px;' +
' --font-size-sm:15.625px; --font-size-md:19.53125px; --font-size-lg:24.4140625px;' +
' --font-size-xl:30.517578125px; --font-size-2xl:38.146972656px; --font-size-3xl:47.68371582px;' +
' --font-size-4xl:59.604644775px; --font-size-5xl:74.505805969px;' +
'}'

View file

@ -69,6 +69,10 @@ const cfg = computed(() => {
try { return JSON.parse(props.profileConfig) || {} } catch { return {} }
})
const prayBaseUrl = computed(() => props.prayBaseUrl || cfg.value.prayBaseUrl || 'https://pray.doxa.life')
// Their popup's second button. RegionDetail passes CountryMap `:research-url="localePath('/research') + '/'"`;
// we have no router, so it comes from profile-config with their public site as the default —
// the same ladder and the same default UupgsList.vue:74 already uses.
const researchUrl = computed(() => cfg.value.researchUrl || 'https://doxa.life/research/')
// The prayer API localises its LABELS from ?lang= — country names, region names, religion
// names. MEASURED: with locale=ar the UI translated but every country in the table stayed
// English, because this read only cfg.languageCode while the locale arrives as
@ -131,7 +135,13 @@ const sel = ref({
// identically -- the extra fields are read by the map, never by addToStats().
// Their PEOPLE_GROUP_LIST_FIELDS plus the two this tab's map needs (name, slug) — derived
// from the shared list so the two can never drift apart.
const FIELDS = 'name,slug,' + PEOPLE_GROUP_LIST_FIELDS
// ★ PLUS THEIR FIVE POPUP FIELDS. CountryMap.vue:89 fetches
// name,slug,country_code,rop1,religion,population,location_description,image_url,has_photo,
// latitude,longitude,people_committed
// and their pin popup renders every one of them. Ours fetched neither rop1, religion,
// location_description nor the photo, so the popup COULD NOT have been a clone however it was
// written — the data was not on the page. Same one request, five more columns.
const FIELDS = 'name,slug,rop1,religion,location_description,image_url,has_photo,' + PEOPLE_GROUP_LIST_FIELDS
function formatNumber(n) {
// THEIRS, RegionDetail.vue:16: `n.toLocaleString(`${locale.value}-u-nu-latn`)` — locale
@ -594,6 +604,12 @@ async function loadRegions() {
region: p.wagf_region?.value || '',
lng: parseFloat(p.longitude), lat: parseFloat(p.latitude),
committed: toNumber(p.people_committed), population: toNumber(p.population),
// Their popup's fields, mapped exactly as CountryMap.vue:107-115 maps them — including the
// has_photo GATE on the image, which is theirs: a row can carry an image_url that is a
// placeholder, and they only show it when the flag says there is a real photo.
rop1: p.rop1?.label || '', religion: p.religion?.label || '',
description: p.location_description || '',
imageUrl: (p.has_photo && p.image_url) ? p.image_url : '',
})).filter((p) => Number.isFinite(p.lng) && Number.isFinite(p.lat))
} catch (error) {
console.error('[region-dashboards] fetch error:', error)
@ -651,7 +667,13 @@ useShadowStyles(
' --dx-surface:#f3f3f1; --dx-elevated:#fff; --dx-text:#2a332a; --dx-header-bg:#3b463d;' +
' --dx-header-text:#fff; --dx-muted:#6d766e; --dx-line:#d1d4cd; --dx-accent:#92b195;' +
' --dx-dark:#2a332a; --dx-on-dark:#f3f3f1; --dx-track:#4e594f; --dx-partial:#f0c64a;' +
' --dx-radius:35px;' +
' --dx-radius:35px; --dx-radius-lg:25px;' +
// base/_globals.scss:99-105 + :205-207 style EVERY table on their site: --table-border-color
// is $color-gray-500 (#666) and --border-radius-lg is 2.5rem at their 10px root = 25px.
' --dx-table-border:#666;' +
// base/_globals.scss:132-141 styles EVERY anchor: --color-brand-light on hover to
// --color-brand-lighter. $color-green-400 / $color-green-300.
' --dx-link:#4e594f; --dx-link-hover:#6d766e;' +
' background:var(--dx-surface); color:var(--dx-text);' +
' font:var(--font-size-md)/1.5 Poppins,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }' +
// Same responsive context + same 1200px cap as tab 1 (regions-list.vue). DX-life applies its
@ -669,15 +691,24 @@ useShadowStyles(
// We cannot set this on the host document from inside a shadow root, and should not;
// this element's own scroller is the box every child measures against.
'.rd-scroll{ flex:1 1 auto; min-height:0; overflow-y:auto; scrollbar-gutter:stable; -webkit-overflow-scrolling:touch;' +
' padding:var(--spacing-3xl) 0 var(--spacing-4xl); display:flex; flex-direction:column; gap:var(--spacing-3xl);' +
' padding:var(--spacing-3xl) 0 var(--spacing-4xl); display:flex; flex-direction:column;' +
' container-type:inline-size; }' +
'.rd-scroll > * + *{ margin-top:var(--spacing-3xl); }' + // .stack .stack--3xl
'.rd-scroll > *{ box-sizing:border-box; width:calc(90% + 16px); max-width:1200px;' +
' margin-left:auto; margin-right:auto; }' +
'.rd-status{ padding:28px; text-align:center; color:var(--dx-muted); font-size:var(--font-size-md); }' +
'.rd-status.rd-failed{ font-weight:600; }' +
// ── head: RegionDetail.vue:47-51, a .stack--xs (7.5px) block, centred ──
'.rd-head{ display:flex; flex-direction:column; gap:10px; text-align:center; }' +
// ★ EVERY .stack HERE IS NOW THEIR MECHANISM, not `gap`. The goal card proved the two are not
// interchangeable (a child's margin-top:auto can override a margin and cannot override a gap),
// and the extended live diff then caught the residue: their .stack rowGap computes to `normal`
// because the spacing is margin, while ours read 17.689px. Same pixels, different code — and the
// DRIVER asked for the code to match, not just the screenshot. Converted, so the next
// margin-overriding child anywhere on this page behaves as it does on their page.
// .stack .stack--xs — --spacing-xs is 1rem at their 10px root.
'.rd-head{ display:flex; flex-direction:column; text-align:center; }' +
'.rd-head > * + *{ margin-top:10px; }' +
'.rd-backlink{ margin:0; }' +
// their .light-link on a dark-on-light page: brand-light, underlined on hover
// THEIR back-link is an <a class="light-link">, so it inherits the UA underline plus their
@ -734,7 +765,9 @@ useShadowStyles(
// clone below is untouched by it.
'.rd-chooser{ display:flex; flex-wrap:wrap; gap:13.3px; align-items:flex-end;' +
' justify-content:center; }' +
'.rd-mapblock{ display:flex; flex-direction:column; gap:13.3px; }' +
// .stack .stack--sm — --spacing-sm, 1.33rem.
'.rd-mapblock{ display:flex; flex-direction:column; }' +
'.rd-mapblock > * + *{ margin-top:13.3px; }' +
'.rd-mapnote{ margin:0; text-align:center; color:#92b195; font-weight:700; font-size:var(--font-size-md); }' +
// .rounded-xlg on their <CountryMap>; the child takes border-radius:inherit from here.
'.rd-mapframe{ border-radius:var(--dx-radius); overflow:hidden; }' +
@ -776,7 +809,16 @@ useShadowStyles(
// heading keeps the full clamp -- it has 1200px and no such constraint.
// .stack--lg (var(--spacing-lg)) inside each card, centred; .region-goal pins to the bottom
// (margin-top:auto) so three cards of different heights still align their goal lines.
'.rd-goal-card{ display:flex; flex-direction:column; gap:var(--spacing-lg); text-align:center; }' +
// ★ THEIR .stack IS margin-top, AND THAT IS LOAD-BEARING — not a stylistic detail.
// composition/_stack.scss: `.stack > * + * { margin-top: var(--stack-spacing) }`, with
// `.stack--lg` setting that to --spacing-lg. RegionDetail.vue's scoped
// `.region-goal { margin-top: auto }` then OVERRIDES the same property on the last child,
// so the goal line is pushed to the bottom and the 23.526px stack gap above it is GONE.
// This was ported as `gap`, which margin cannot override: gap AND the auto both applied,
// so every goal card carried ~23.5px of space their cards do not have. The DRIVER saw it
// ('spcing is nto ths ame', naming these three cards). Ported as their mechanism.
'.rd-goal-card{ display:flex; flex-direction:column; text-align:center; }' +
'.rd-goal-card > * + *{ margin-top:var(--spacing-lg); }' +
// h2: _globals.scss clamp(--font-size-2xl, 6vw, --font-size-4xl) = clamp(var(--font-size-2xl),_,var(--font-size-4xl))
'.rd-h2{ margin:0; font-size:clamp(var(--font-size-2xl), 6cqw, var(--font-size-4xl)); line-height:1;' +
' font-family:"Bebas Neue","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
@ -791,8 +833,16 @@ useShadowStyles(
'.rd-big{ margin:0; font-size:var(--font-size-4xl); font-weight:500; line-height:1.1; }' +
'.rd-goal-card .rd-big{ white-space:nowrap; }' +
'.rd-big-label{ margin:0; font-size:var(--font-size-lg); }' +
'.rd-stack-xs{ display:flex; flex-direction:column; gap:10px; }' +
'.rd-goal{ margin:auto 0 0; font-size:var(--font-size-sm); }' +
// .stack .stack--xs, inside the Prayer Coverage card.
'.rd-stack-xs{ display:flex; flex-direction:column; }' +
'.rd-stack-xs > * + *{ margin-top:10px; }' +
// .region-goal — margin-top:auto, spelled at higher specificity than the stack rule above
// so the override is unambiguous rather than depending on source order.
'.rd-goal{ margin:0; font-size:var(--font-size-sm); }' +
// base/_globals.scss:200-202 — <strong> is --font-weight-medium (500) on their site, not the
// UA's bold. The 'Goal:' label in all three cards is a <strong>, so ours was rendering 700.
'.rd-root strong{ font-weight:500; }' +
'.rd-goal-card > .rd-goal{ margin-top:auto; }' +
// ── .progress-bar: blocks/_progress-bar.scss. The HEIGHT comes from padding-top, not height
// -- clamp(15px, 6%, 25px) at [data-size="md"] -- so the track scales with its own width.
@ -817,16 +867,45 @@ useShadowStyles(
// ── the breakdown table: RegionDetail.vue's scoped block. min-width 560px inside an
// overflow-x:auto wrapper is how they keep five columns readable on a phone instead of
// crushing them -- the table scrolls sideways, the page does not.
'.rd-section{ display:flex; flex-direction:column; gap:var(--spacing-md); }' +
// ★ base/_globals.scss:19-23 — their countries block is a <section>, and EVERY <section> on
// their site carries padding-top clamp(--spacing-2xl, 6vw, --spacing-4xl) and padding-bottom
// --spacing-4xl. At 1440 that is 73.614px above and below, ON TOP of the --spacing-3xl the
// page stack already puts between blocks: 128.96px of air above the heading, where ours had
// 55.35px and nothing below. Another global rule, another thing reading RegionDetail.vue
// alone could not tell me. Same clamp, in container units for the embedded case.
'.rd-section{ display:flex; flex-direction:column;' +
' padding-top:clamp(var(--spacing-2xl), 6cqw, var(--spacing-4xl));' +
' padding-bottom:var(--spacing-4xl); }' +
'.rd-section > * + *{ margin-top:var(--spacing-md); }' + // .stack .stack--md
'.rd-table-scroll{ overflow-x:auto; }' +
'.rd-table{ width:100%; min-width:560px; border-collapse:collapse; font-size:var(--font-size-md); }' +
// ★ base/_globals.scss:99-105 — the part of their table that is NOT in RegionDetail's
// scoped block and was therefore missed: every <table> on their site is a rounded,
// ring-outlined WHITE card, and every <td> carries a 1px gridline (:205-207). Ours was a
// bare borderless table on the page background. That is what the DRIVER meant by 'the
// tbabel compoent is not the same' — the rule lived in a global stylesheet, not next to
// the markup, so reading RegionDetail.vue alone could not show it.
// overflow:hidden is what makes border-radius clip the header's own background.
'.rd-table{ width:100%; min-width:560px; border-collapse:collapse; font-size:var(--font-size-md);' +
' border-radius:var(--dx-radius-lg); overflow:hidden; background:#fff;' +
' box-shadow:0 0 0 1px var(--dx-table-border); }' +
// td ONLY — their th has no border of its own, it has the header fill.
'.rd-table td{ border:1px solid var(--dx-table-border); }' +
'.rd-table th, .rd-table td{ padding:var(--spacing-2xs) 13.3px; text-align:start; }' +
'.rd-table th:not(:first-child), .rd-table td:not(:first-child){ text-align:end; }' +
'.rd-table th{ background:var(--dx-muted); color:var(--dx-on-dark); font-weight:500; }' +
'.rd-tr--selected{ background:#e8ece7; font-weight:600; }' +
// ★ Their country cell is a <NuxtLink>, i.e. an <a>, which picks up base/_globals.scss:132-141:
// --color-brand-light, --font-weight-medium, the UA underline at 2px thickness / 4px offset,
// hovering to --color-brand-lighter. Ours is a <button> (it selects a subject in place
// rather than navigating, which is the one thing this element does differently) and a
// button inherits NONE of that — it read as plain body text, unweighted and unlined.
// So the anchor rule is spelled out here: same element role, same appearance.
'.rd-row-link{ appearance:none; background:none; border:0; padding:0; margin:0; cursor:pointer;' +
' font:inherit; color:var(--dx-text); text-align:start; }' +
'.rd-row-link:hover{ text-decoration:underline; }' +
' font:inherit; font-weight:500; color:var(--dx-link); text-align:start;' +
' text-decoration:underline; text-decoration-thickness:2px; text-underline-offset:4px;' +
' text-decoration-skip-ink:auto; transition:color .3s ease; }' +
'.rd-row-link:hover{ color:var(--dx-link-hover); }' +
'.rd-row-link:focus-visible{ outline:2px solid var(--dx-accent); outline-offset:2px; border-radius:4px; }',
'region-dashboards'
)
@ -837,7 +916,7 @@ useShadowStyles(
<!-- a landmark, so a screen reader can jump to the dashboard body; the app probe
measured none on the page and it costs nothing to name the region. -->
<main class="rd-scroll" :aria-label="t('Region dashboard')">
<div v-if="loading" class="rd-status">Loading region dashboards…</div>
<div v-if="loading" class="rd-status">{{ t('Loading region dashboards…') }}</div>
<div v-else-if="loadFailed" class="rd-status rd-failed" role="status">
Region data could not be loaded. This is a connection problem, not a filter.
</div>
@ -928,6 +1007,8 @@ useShadowStyles(
:color-mode="mapColorMode"
:highlight-region="hoveredRegion"
:select-on-pin-click="isAll"
:pray-base-url="prayBaseUrl"
:research-url="researchUrl"
@pick-country="onPickFromMap"
/>
</div>

View file

@ -413,8 +413,18 @@ useShadowStyles(
// not a text glyph. Ported verbatim (fill:currentColor tracks the surrounding text colour,
// same as their `.icon{ fill:currentColor }` utility) and rotated 90deg to point right, same
// as their `rotate-90` class. getComputedStyle measured it at 33.38px square on the live page.
// ★ 1rem WAS THE ONE UNIT IN THIS PORT THAT INHERITS THE HOST PAGE'S ROOT, and it was wrong on
// our own leaf, not only in an embed. `rem` resolves against the DOCUMENT root wherever the
// element lives — which is the whole reason dxlife-tokens.js resolves their scale to px — and
// their root is 10px (html{font-size: var(--font-size-base)}, $font-size-base: 10px). Ours
// inherits whatever the host sets: 16px by default, so this gap rendered 16px against their
// 10px, and on a host with the common html{font-size:62.5%} reset it would render 10px by
// accident. Pinned to the value their root produces, like every other token here.
// No diff caught it: the list diff's `link` spec reads colour, decoration, weight, display and
// alignItems — no margin — so the icon's own spacing was never a measured property. It is now.
// (Credit where due: the class of bug came to me as a flock relay from another agent's sweep.)
'.region-card__icon{ width:0.7em; height:0.7em; flex:none; fill:currentColor; transform:rotate(90deg);' +
' margin-inline-start:1rem; }' +
' margin-inline-start:10px; }' +
// .region-card__summary: --font-size-md (var(--font-size-md)), 0.85 opacity, on the header band.
'.region-card__summary{ font-size:var(--font-size-md); opacity:0.85; margin:0; }' +
// .region-card__body: padding --spacing-lg / clamp(...) / --spacing-xl -> var(--spacing-lg) / clamp / var(--spacing-xl)
@ -425,7 +435,9 @@ useShadowStyles(
// one list-item's worth even though DX-life's countries-grid a rule doesn't set it directly.
// Reproduced with list-style:none + the same 35.16px indent kept explicitly so a real list
// bullet has somewhere to sit without stealing width from the text column.
'.countries-grid{ list-style:none; margin:0; padding:0 0 0 35.2px; display:grid;' +
// padding-INLINE-start, as theirs is (1.8em). The physical `padding-left:35.2px` it replaces
// put the indent on the wrong side in Arabic, where the bullets hang off the right edge.
'.countries-grid{ list-style:none; margin:0; padding-inline-start:1.8em; display:grid;' +
' grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:10px var(--spacing-xl); }' +
// Plain inline text (not a flex row) so multi-line country names wrap the same way DX-life's
// <NuxtLink> text does -- a flex row with align-items:baseline pinned the bullet to the first
@ -441,8 +453,22 @@ useShadowStyles(
// inset-inline-start, NOT left: the bullet hangs off the text's starting edge, which is the
// RIGHT edge in Arabic. With `left:-16px` it stayed on the physical left in RTL — measured
// at locale=ar, computed right was 256.938px instead of the -16px near-edge overhang.
'.country-link::before{ content:""; position:absolute; inset-inline-start:-16px; top:0.6em; width:6px; height:6px;' +
' border-radius:50%; background:#92b195; }' +
// ★ THEIR BULLET, COPIED — not a dot of my own. base/_globals.scss:117-131 paints the list
// marker with a 16x16 data-URI SVG circle in #92b195 sized to 1.5em, hung off the text's start
// edge by margin-inline-start:-1.3em (-2em in RTL), top .3em, line-height .9. Their
// .countries-grid comment is explicit that keeping it is deliberate ("Keeps the site's list
// bullets (global ul padding + ::before dot)").
// I had re-created it as a 6px border-radius:50% square, and MEASURED against live it was wrong
// in 9 of 11 properties: 6px vs 29.2969px box, solid fill vs a transparent box carrying the
// image, top 11.72px vs 5.86px, and positioned with inset-inline-start:-16px instead of their
// negative margin. Not a near-miss — a different bullet.
// The rule is a double-quoted JS string because the data URI contains single quotes.
".country-link::before{ content:''; position:absolute; top:0.3em; width:1.5em; height:1.5em;" +
" line-height:0.9; margin-inline-start:-1.3em; background-repeat:no-repeat;" +
" background-image:url(\"data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='%2392b195' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='7' stroke='%2392b195' stroke-width='1.5'/%3E%3C/svg%3E\"); }" +
// Their RTL variant. It is a separate rule here rather than a nested [dir="rtl"] & because this
// CSS lives in a shadow root, where a [dir] selector on an outer ancestor does not reach in.
'[dir="rtl"] .country-link::before{ margin-inline-start:-2em; }' +
'.country-link:hover{ text-decoration:underline; text-decoration-color:#92b195; }' +
'.country-link:focus-visible{ outline:2px solid #92b195; outline-offset:2px; border-radius:4px; }' +
'.country-link__count{ color:var(--dx-muted); }' +
@ -514,7 +540,7 @@ useShadowStyles(
<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>
<div v-else-if="!regions.length" class="rl-status">No regions found.</div>
<div v-else-if="!regions.length" class="rl-status">{{ t('No regions found.') }}</div>
<template v-else>
<div

View file

@ -34,8 +34,14 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
// check:one-palette exists because a private palette in a consumer is what once made two
// surfaces disagree about what a colour meant.
import { applyColor as regionColorExpression, PALETTE as REGION_PALETTE, getColor as regionColorFor } from '@map/colors/wagf-region.js'
// ★ THIS FILE HAD NO t() AT ALL. Its legend rendered four hardcoded English strings on a page
// whose every other word is translated into eleven languages, and three of those four were
// ALREADY in the catalogue — translated for the Prayer Coverage card and hardcoded here.
// Their CountryMap.vue calls t() for the same strings (:268-272).
import { useRegionsI18n } from './useRegionsI18n.js'
const { t, locale } = useRegionsI18n()
const emit = defineEmits(['pick-country'])
const props = defineProps({
// ISO-3 codes to highlight, e.g. ['IND'] for a country or every code in a region.
@ -56,6 +62,10 @@ const props = defineProps({
zoom: { type: Number, default: 2 },
// [[west, south], [east, north]] — takes precedence over center/zoom, as theirs does.
bounds: { type: Array, default: null },
// Their popup's two buttons (CountryMap.vue:156-158): Pray -> prayBaseUrl/<slug>?source=doxalife,
// Full Profile -> researchUrl<slug>.
prayBaseUrl: { type: String, default: 'https://pray.doxa.life' },
researchUrl: { type: String, default: 'https://doxa.life/research/' },
})
// The token ladder: ProfileLoader provides it (the same seam every map bundle uses). A nested
@ -100,6 +110,9 @@ const geojson = computed(() => {
inCountry: focus.has(p.code), committed: Number(p.committed) || 0,
region: p.region || '', code: p.code || '',
population: Number(p.population) || 0,
// Their popup's four (CountryMap.vue:107-115). Primitives only, as their own comment says.
rop1: p.rop1 || '', religion: p.religion || '',
description: p.description || '', imageUrl: p.imageUrl || '',
},
})),
}
@ -298,19 +311,54 @@ function onPinClick(e) {
const g = window.mapboxgl
if (!g) return
const p = f.properties || {}
const pop = Number(p.population) || 0
const html = '<div style="font:14px/1.45 Poppins,sans-serif;color:#2a332a">' +
'<div style="font-weight:600;margin-bottom:2px">' + esc(p.name) + '</div>' +
(p.country ? '<div style="color:#6d766e">' + esc(p.country) + '</div>' : '') +
(pop ? '<div style="color:#6d766e">Population ' + pop.toLocaleString('en-US') + '</div>' : '') +
'<div style="color:#6d766e">' + (Number(p.committed) || 0).toLocaleString('en-US') + ' praying</div>' +
'</div>'
const html = popupHtml(p)
if (popup) popup.remove()
popup = new g.Popup({ closeButton: true, maxWidth: '264px', offset: 12 })
.setLngLat(f.geometry.coordinates).setHTML(html).addTo(map.value)
}
// THEIR escape, not a shorter one: CountryMap.vue:122-125 also escapes the two quote characters,
// and this string is interpolated into href="" and alt="" attributes below, where omitting them is
// the difference between an escape and a hole.
function esc(s) {
return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
}
// ★ THEIR PIN POPUP, PORTED — CountryMap.vue:130-161. Ours was a four-line stub: a bold name, the
// country, "Population 12,345" and "678 praying", in English, with no photo, no religion, no
// description and no way to act on it. Theirs is a card: the people group's photo, the name, the
// country and its primary ethnolinguistic group, a definition list of Population / Religion /
// Intercessors-out-of-100, up to three lines of the location description, and two buttons — Pray,
// which is the entire point of the site, and Full Profile.
// It could not have been ported before this pass because the DATA was not being fetched: rop1,
// religion, location_description and the photo were missing from our field list.
function popupHtml(p) {
const name = esc(p.name || '')
const subtitle = [p.country, p.rop1].filter(Boolean).map(esc).join(' · ')
// Their formatPopulation: toLocaleString(languageCode), NOT a hardcoded 'en'. Ours printed
// 'en-US' grouping in every one of the eleven locales.
const pop = Number(p.population) > 0 ? Number(p.population).toLocaleString(locale.value || 'en') : ''
const profileUrl = `${props.researchUrl}${p.slug || ''}`
const prayUrl = `${String(props.prayBaseUrl).replace(/\/+$/, '')}/${p.slug || ''}?source=doxalife`
const rows = []
if (pop) rows.push(`<div><dt>${esc(t('Population'))}</dt><dd>${pop}</dd></div>`)
if (p.religion) rows.push(`<div><dt>${esc(t('Religion'))}</dt><dd>${esc(p.religion)}</dd></div>`)
rows.push(`<div><dt>${esc(t('Intercessors'))}</dt><dd>${Number(p.committed) || 0}/${FULL_PRAYER_COVERAGE_COUNT}</dd></div>`)
return `<div class="cm-popup">
${p.imageUrl ? `<img class="cm-popup__img" src="${esc(p.imageUrl)}" alt="${name}" loading="lazy">` : ''}
<div class="cm-popup__body">
<div class="cm-popup__name">${name}</div>
${subtitle ? `<div class="cm-popup__sub">${subtitle}</div>` : ''}
${rows.length ? `<dl class="cm-popup__stats">${rows.join('')}</dl>` : ''}
${p.description ? `<p class="cm-popup__desc">${esc(p.description)}</p>` : ''}
<div class="cm-popup__actions">
<a class="cm-popup__btn cm-popup__btn--primary" href="${prayUrl}">${esc(t('Pray'))}</a>
<a class="cm-popup__btn" href="${profileUrl}">${esc(t('Full Profile'))}</a>
</div>
</div>
</div>`
}
// Re-frame and re-source on a new selection instead of tearing the map down: a rebuilt map
@ -474,7 +522,34 @@ useShadowStyles(
' flex-direction:column; gap:4px; padding:8px 10px; border-radius:8px;' +
' background:rgba(255,255,255,0.92); font-size:12.5px; font-weight:600; color:#2a332a; }' +
'.rcm__legend span{ display:inline-flex; align-items:center; gap:6px; }' +
'.rcm__legend i{ width:10px; height:10px; border-radius:100px; flex:0 0 auto; }',
'.rcm__legend i{ width:10px; height:10px; border-radius:100px; flex:0 0 auto; }' +
// ── THEIR PIN POPUP'S CSS, ported verbatim from CountryMap.vue's scoped block ──────────────
// Mapbox builds the popup inside the map container, which is inside this shadow root, so these
// reach it. The var() fallbacks are theirs and resolve to the same bytes here as there:
// --color-brand is #3b463d and --color-brand-lighter #6d766e on live, identical to the
// fallbacks, and --color-black is not defined on either side so #1f1f1f applies to both.
'.mapboxgl-popup-content:has(.cm-popup){ padding:12px; border-radius:10px;' +
' box-shadow:0 4px 16px rgba(0,0,0,0.18); }' +
'.cm-popup{ width:240px; }' +
'.cm-popup__img{ display:block; width:150px; aspect-ratio:4/5; object-fit:cover;' +
' border-radius:8px; margin:0 auto 8px; }' +
'.cm-popup__body{ padding:0; }' +
'.cm-popup__name{ font-weight:700; font-size:15px; line-height:1.25;' +
' color:var(--color-black, #1f1f1f); }' +
'.cm-popup__sub{ margin-top:2px; font-size:12px; color:var(--color-brand-lighter, #6d766e); }' +
'.cm-popup__stats{ margin:8px 0 0; display:flex; flex-direction:column; gap:3px; }' +
'.cm-popup__stats > div{ display:flex; justify-content:space-between; gap:10px; font-size:12px; }' +
'.cm-popup__stats dt{ margin:0; color:var(--color-brand-lighter, #6d766e); }' +
'.cm-popup__stats dd{ margin:0; text-align:right; font-weight:600;' +
' color:var(--color-black, #1f1f1f); }' +
'.cm-popup__desc{ margin:8px 0 0; font-size:12px; line-height:1.45; color:#555;' +
' display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; }' +
'.cm-popup__actions{ display:flex; gap:6px; margin-top:10px; }' +
'.cm-popup__btn{ flex:1; padding:6px 8px; border-radius:6px; font-size:12px; font-weight:600;' +
' text-align:center; text-decoration:none; background:#ecf0f1;' +
' color:var(--color-black, #1f1f1f); }' +
'.cm-popup__btn--primary{ background:var(--color-brand, #3b463d); color:#fff; }',
'region-country-map'
)
</script>
@ -495,10 +570,10 @@ useShadowStyles(
figures above are unaffected.
</p>
<div v-if="colorMode === 'prayer' && !mapFailed" class="rcm__legend">
<span><i :style="{ background: C.prayerFull }" />100+ People Praying</span>
<span><i :style="{ background: C.prayerPartial }" />1+ People Praying</span>
<span><i :style="{ background: C.prayerNone }" />No One Praying</span>
<span><i :style="{ background: C.outside }" />Outside this region</span>
<span><i :style="{ background: C.prayerFull }" />{{ t('100+ People Praying') }}</span>
<span><i :style="{ background: C.prayerPartial }" />{{ t('1+ People Praying') }}</span>
<span><i :style="{ background: C.prayerNone }" />{{ t('No One Praying') }}</span>
<span><i :style="{ background: C.outside }" />{{ t('Outside this region') }}</span>
</div>
</div>
</template>

View file

@ -347,15 +347,22 @@ useShadowStyles(
' background-color:#fff; width:fit-content;' +
' transition:border-color 150ms ease, background-color 150ms ease; }' +
'.filter-toggle:hover{ border-color:#92b195; }' +
// ★ THEIR ACTIVE STATE IS WHITE-ON-WHITE, CLONED ON PURPOSE. Their rule is
// background-color: var(--color-surface-brand-lighter, rgba(0,80,160,0.05));
// color: var(--color-white);
// and --color-surface-brand-lighter is NOT DEFINED in their token files, so the 5%-alpha
// fallback lands on white while the ink turns white. MEASURED on their live page: the
// active button computes backgroundColor rgb(255,255,255) and color rgb(255,255,255) —
// an invisible label. Reproduced so the clone matches; flagged for the Driver, because
// defining that one token upstream fixes it for them and for us at once.
'.filter-toggle[data-active]{ background-color:rgba(0, 80, 160, 0.05); color:#fff; }' +
// ★ THIS SHIPPED AN ILLEGIBLE BUTTON FOR A CLONE OF A LEGIBLE ONE, and the comment that used to
// sit here is why it lasted. It said their active state is white-on-white "on purpose", that
// --color-surface-brand-lighter "is NOT DEFINED in their token files", and that the white-on-white
// had been "MEASURED on their live page". Two of those three were wrong:
// · tokens/semantic/_colors.scss:64 DOES define it — $color-green-300, #6d766e.
// · getComputedStyle returns the SPECIFIED colour, never the composite, so neither candidate
// could ever serialise as white: the token is #6d766e and the declared fallback is
// rgba(0,80,160,0.05). The only rule on that button yielding white is .input's
// var(--color-surface-white) — which is the INACTIVE state. The "measurement" had almost
// certainly read a button without data-active set.
// MEASURED PROPERLY (_probe-filter-state-vs-live.mjs, which CLICKS the Adopted filter on both
// sides and then reads): their live active button is background rgb(109,118,110) on
// color rgb(255,255,255). White on grey-green. Perfectly legible. We were shipping their DEAD
// FALLBACK — 5%-alpha blue, which over the white .input ground leaves a white label on white.
// So: the token's value, as theirs resolves it.
'.filter-toggle[data-active]{ background-color:#6d766e; color:#fff; }' +
'.filters__panel{ display:flex; flex-wrap:wrap; gap:13.3px; justify-content:center;' +
' align-items:flex-start; }' +
'.filters__active{ display:flex; flex-wrap:wrap; gap:var(--spacing-2xs); align-items:center;' +
@ -366,8 +373,14 @@ useShadowStyles(
'.filter-chip__remove{ background:none; border:0; cursor:pointer; font-size:1.1em; line-height:1;' +
' color:inherit; padding:0; display:inline-flex; align-items:center; }' +
'.filter-chip__remove:hover{ opacity:0.7; }' +
// ★ Clear All is `.button compact link` on their side, and blocks/_buttons.scss:64-69 gives
// .button.link `--button-text-color: var(--color-brand)` = $color-green-500 = #3b463d. Ours used
// var(--dx-text) (#2a332a, the body ink) — a different green, measured against live as
// rgb(42,51,42) vs their rgb(59,70,61). Found twice over, independently: by the applied-filter
// diff, and by a static sweep of their markup's classes that flagged `.link` as a rule our CSS
// had no equivalent for at all.
'.filters__clear-all{ background:none; border:0; cursor:pointer; font:inherit; font-size:var(--font-size-sm);' +
' color:var(--dx-text); text-decoration:underline; text-underline-offset:4px;' +
' color:#3b463d; text-decoration:underline; text-underline-offset:4px;' +
' text-decoration-thickness:2px; }' +
// .repel — their space-between row (composition/_repel.scss), at --font-size-md gap.
'.ul-total{ display:flex; gap:var(--spacing-md); justify-content:space-between; align-items:center; }' +

View file

@ -55,8 +55,53 @@
* ON via prop. This is why the swap-inside-one-element idea was genuinely complex: the two top
* elements do not merely look different, they imply OPPOSITE control ownership below.
*/
import { computed } from 'vue'
import StackLayout from '@map/layouts/StackLayout.vue'
import { resolveEmbedBase } from '@map/utils/embedBase.js'
import { publishMapToken } from '@map/composables/useSharedMapToken.js'
// ★ THE HOST'S OWN profile-config, WHICH THIS COMPOSITE USED TO THROW AWAY.
// It declared no props at all, so a partner's <research-map-plus-grid profile-config='{"tk":"..."}'>
// had its token dropped on the floor. On our own generated leaf that was invisible: the leaf page
// carries a BAKED token, assigns window.MAP_TOKEN, and the shared-token bridge hands it to the
// nested map. A partner page has no leaf and no baked token, so the JS embed — one of the two
// shipped embed methods — mounted a map that could not draw. MEASURED on a third-party origin
// before this: 'No Mapbox access token provided', 0 style and 0 tile requests, no canvas.
const props = defineProps({
/** The host's JSON config, exactly as the embed snippet writes it. */
profileConfig: { type: [String, Object], default: null },
})
const hostConfig = computed(() => {
const raw = props.profileConfig
if (!raw) return {}
if (typeof raw === 'object') return raw
// A malformed attribute must not take the whole element down with it — the map half is more
// useful token-less than the page is with a thrown setup().
try { return JSON.parse(raw) } catch { return {} }
})
// Publish page-wide as well as threading it below. Threading covers THIS child deterministically;
// publishing covers any element on the page that mounted before the attribute was read, which is
// the shape the shared-token composable exists for.
const hostToken = computed(() => {
const tk = hostConfig.value?.tk
return typeof tk === 'string' && tk ? tk : ''
})
if (hostToken.value) publishMapToken(hostToken.value)
// ★ THE PROVIDER IS A HOST KNOB TOO, AND FORGETTING IT BREAKS THE KEYLESS CASE ENTIRELY.
// resolveMapProvider's ladder starts at profile-config.provider, and the child reads its OWN
// profile-config — the one this file writes. A host asking for <research-map-plus-grid
// profile-config='{"provider":"maplibre"}'> therefore got a child still on mapbox, which then
// demanded a token. MEASURED on a third-party origin before this: 'No Mapbox access token
// provided', 0 tiles, no canvas — for the exact configuration whose whole purpose is needing no
// token. Keyless at volume is the reason MapLibre support exists; a composite that drops the
// prop cancels it.
const hostProvider = computed(() => {
const p = hostConfig.value?.provider
return p === 'maplibre' || p === 'mapbox' ? p : ''
})
// The absolute url this composite is shared and iframed as. Built here rather than written as a
// literal because resolveEmbedBase() walks profileConfig.embedBaseUrl → window.MAP_EMBED_BASE_URL →
@ -103,7 +148,7 @@ const MAP_H = '88svh'
const bundleSrc = (bundle, file) =>
import.meta.env.DEV ? `/plugin-designs/${bundle}/index.js` : `../../${bundle}/${file}.js`
const sections = [
const sections = computed(() => [
// No section title: the research map self-titles through its own chrome, so a StackLayout title
// here would render the name twice — the duplicate-header bug the Driver rejected on the desktop
// sibling (BUG-RESEARCH-DASHBOARD-HEADER-STACK-WRONG). One header per element.
@ -155,6 +200,12 @@ const sections = [
// reader is scanning, a fly-tour that moves the camera underneath fights the other half.
// The standalone leaf keeps both; this is a per-host decision, which is why it is config.
legendTools: false,
// Thread the host's token through to the child. Spread so a host that supplies none
// leaves the key ABSENT rather than empty: ProfileLoader's ladder is `config.tk ||
// sharedToken`, and an empty string would be falsy anyway, but an absent key keeps the
// child's own config identical to what its standalone leaf sets.
...(hostToken.value ? { tk: hostToken.value } : {}),
...(hostProvider.value ? { provider: hostProvider.value } : {}),
assetsBase: '../../assets',
shareUrl: SHARE_URL
})
@ -176,7 +227,7 @@ const sections = [
src: bundleSrc('people-groups-grid', 'people-groups-grid'),
attrs: { 'show-search': 'true', 'show-filters': 'true', 'show-total': 'true' }
}
]
])
</script>
<template>

View file

@ -13,7 +13,9 @@
* layouts in library/layouts/. StackLayout mounts each section's `element` eagerly (all visible to
* scroll) via the same element/src/attrs contract VerticalTabsLayout uses.
*/
import { computed } from 'vue'
import StackLayout from '@map/layouts/StackLayout.vue'
import { publishMapToken } from '@map/composables/useSharedMapToken.js'
// The two elements to stack. `src` is TREE-RELATIVE at build; the leaf pages resolve it against the
// bundle tree root. Both elements ship their own bundles in the same catalog.
@ -42,7 +44,30 @@ import StackLayout from '@map/layouts/StackLayout.vue'
const bundleSrc = (bundle, file) =>
import.meta.env.DEV ? `/plugin-designs/${bundle}/index.js` : `../../${bundle}/${file}.js`
const sections = [
// ★ THE HOST'S OWN profile-config. This demo declared no props, so a partner embedding it with
// <stack-demo profile-config='{"tk":"pk...."}'> had that token read by nobody and the nested
// dashboard reported "No Mapbox access token provided" on a page with no baked token to fall back
// on. Same defect the two shipped composites had; check:composite-embed-knobs names it.
const props = defineProps({
profileConfig: { type: [String, Object], default: null },
})
const hostConfig = computed(() => {
const raw = props.profileConfig
if (!raw) return {}
if (typeof raw === 'object') return raw
try { return JSON.parse(raw) } catch { return {} }
})
const hostToken = computed(() => {
const tk = hostConfig.value?.tk
return typeof tk === 'string' && tk ? tk : ''
})
if (hostToken.value) publishMapToken(hostToken.value)
const hostProvider = computed(() => {
const p = hostConfig.value?.provider
return p === 'maplibre' || p === 'mapbox' ? p : ''
})
const sections = computed(() => [
{
key: 'dashboard',
title: 'Research Dashboard',
@ -62,6 +87,10 @@ const sections = [
profile: 'research-dashboard',
dataSource: 'pray-tools',
instanceId: 'stack-demo-research-dashboard',
// Spread only when the host supplied one, so the nested mount otherwise asks for exactly
// what its standalone leaf asks for.
...(hostToken.value ? { tk: hostToken.value } : {}),
...(hostProvider.value ? { provider: hostProvider.value } : {}),
assetsBase: '../../assets'
})
}
@ -73,7 +102,7 @@ const sections = [
element: 'people-groups-grid',
src: bundleSrc('people-groups-grid', 'people-groups-grid')
}
]
])
</script>
<template>

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 <all-people-groups> custom element. -->
<script src="../all-people-groups.js" integrity="sha384-3kskcN7GLnCNNOrJINJt4n5ERXa0qpwyRNcuVuw4++2KzU0LwGYnLW3aepEu8jDg" crossorigin="anonymous"></script>
<script src="../all-people-groups.js" integrity="sha384-i2Qv1FkHiMWhxFsOKGhfP4qSGTke/57SE1nrvk7ZV0lxfA5O92Ez6ZWf2dRbO88U" 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

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-b7gkA5+oZ8azx5oSxl4c985U9gcOFyNAh3TfhJd4FSUX4XawU0927HvYSFRTNzpr" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-cLy6LqX5n+KGC7VdympqtscDcHqEKchYgMh4GJbHO5Z6X9nYcD3IT7/3Crq/BGEE" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-b7gkA5+oZ8azx5oSxl4c985U9gcOFyNAh3TfhJd4FSUX4XawU0927HvYSFRTNzpr" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-cLy6LqX5n+KGC7VdympqtscDcHqEKchYgMh4GJbHO5Z6X9nYcD3IT7/3Crq/BGEE" 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 <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-b7gkA5+oZ8azx5oSxl4c985U9gcOFyNAh3TfhJd4FSUX4XawU0927HvYSFRTNzpr" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-cLy6LqX5n+KGC7VdympqtscDcHqEKchYgMh4GJbHO5Z6X9nYcD3IT7/3Crq/BGEE" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-amheyqep13c83pKspBiNh8x4DIm5S/L7ifL34zSxsAe+LbpB0YW7DPH+a30ba6Xc" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-S0O8LKY8FDeykxEoMkbMbI7dHp8DZQlNAxW+xj+9rJ/XDvlGUvY4t9zEnuRym7hd" 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-amheyqep13c83pKspBiNh8x4DIm5S/L7ifL34zSxsAe+LbpB0YW7DPH+a30ba6Xc" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-S0O8LKY8FDeykxEoMkbMbI7dHp8DZQlNAxW+xj+9rJ/XDvlGUvY4t9zEnuRym7hd" 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-amheyqep13c83pKspBiNh8x4DIm5S/L7ifL34zSxsAe+LbpB0YW7DPH+a30ba6Xc" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-S0O8LKY8FDeykxEoMkbMbI7dHp8DZQlNAxW+xj+9rJ/XDvlGUvY4t9zEnuRym7hd" 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

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../../dashboard-plus-grid.js" integrity="sha384-VOkoTUw4VURBoPSbk3YnU3Wc/JuN+ywf1eF9D6RHBI2Be/8N8FlY3WX/2CkNk5NS" crossorigin="anonymous"></script>
<script src="../../dashboard-plus-grid.js" integrity="sha384-azKIIIa3yMfrc8/hxS9kf+J2zIQGcw/p/SxLoyil51AqGaH+MJad4mxPI9bycxiL" 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 <dashboard-plus-grid> custom element. -->
<script src="../dashboard-plus-grid.js" integrity="sha384-VOkoTUw4VURBoPSbk3YnU3Wc/JuN+ywf1eF9D6RHBI2Be/8N8FlY3WX/2CkNk5NS" crossorigin="anonymous"></script>
<script src="../dashboard-plus-grid.js" integrity="sha384-azKIIIa3yMfrc8/hxS9kf+J2zIQGcw/p/SxLoyil51AqGaH+MJad4mxPI9bycxiL" 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 <demo-map> custom element. -->
<script src="../demo-map.js" integrity="sha384-WLB+/GTY8u9CWAKt72vKJroKi26oGzzjlGdA0iA3DSSP9oWHnUb8r6jtjAWSw0ee" crossorigin="anonymous"></script>
<script src="../demo-map.js" integrity="sha384-VGZMQWOWVouP4IkUTsZ/ElUORaUYu9+wJSlbs/8r6Lc6EU8VvLYHhmTOGCJv2Y6H" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -27,7 +27,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
<script src="../../demo-map.js" integrity="sha384-WLB+/GTY8u9CWAKt72vKJroKi26oGzzjlGdA0iA3DSSP9oWHnUb8r6jtjAWSw0ee" crossorigin="anonymous"></script>
<script src="../../demo-map.js" integrity="sha384-VGZMQWOWVouP4IkUTsZ/ElUORaUYu9+wJSlbs/8r6Lc6EU8VvLYHhmTOGCJv2Y6H" 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

@ -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-fmp37RdhrkxuguGQ5MmwnakDu0So2YCJSYj8Bca867HoKCT1497o9z3K/wlHDlxP" crossorigin="anonymous"></script>
<script src="../../doxa-maps-spa.js" integrity="sha384-Cmdw0USq0X4/+OshXBayXKy4U5ZBD0bR8YR/yaR/CZ76CYEMpb4bymgfug+5AGyU" 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-fmp37RdhrkxuguGQ5MmwnakDu0So2YCJSYj8Bca867HoKCT1497o9z3K/wlHDlxP" crossorigin="anonymous"></script>
<script src="../doxa-maps-spa.js" integrity="sha384-Cmdw0USq0X4/+OshXBayXKy4U5ZBD0bR8YR/yaR/CZ76CYEMpb4bymgfug+5AGyU" 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 <doxa-research-map> custom element. -->
<script src="../doxa-research-map.js" integrity="sha384-LyIs0sqdkyJcj3DESqefXc7jsD96o+H+YVcOIq2kCdwaS+ZeBLqcn//06n1W1blE" crossorigin="anonymous"></script>
<script src="../doxa-research-map.js" integrity="sha384-E7/WP0pDnwiG6noaZZI3Sx92Cg3JwluCqYOA5eU+x+UARKkliojcJBk3Yx6jua7a" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -27,7 +27,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../../doxa-research-map.js" integrity="sha384-LyIs0sqdkyJcj3DESqefXc7jsD96o+H+YVcOIq2kCdwaS+ZeBLqcn//06n1W1blE" crossorigin="anonymous"></script>
<script src="../../doxa-research-map.js" integrity="sha384-E7/WP0pDnwiG6noaZZI3Sx92Cg3JwluCqYOA5eU+x+UARKkliojcJBk3Yx6jua7a" 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 <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-FtEGLeSRNfBCdmK26IURIxCldSnfKphxRT74XgjftFGzNpvube+ov/EVBFi2gXjo" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-r/K0P50Wt1Qz2KkJ+fVPurKtAr68GqWKREiLb8DYZ2L0VmOQUE9FwZhrDMZzYscr" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-FtEGLeSRNfBCdmK26IURIxCldSnfKphxRT74XgjftFGzNpvube+ov/EVBFi2gXjo" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-r/K0P50Wt1Qz2KkJ+fVPurKtAr68GqWKREiLb8DYZ2L0VmOQUE9FwZhrDMZzYscr" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../doxa-simple-map.js" integrity="sha384-FtEGLeSRNfBCdmK26IURIxCldSnfKphxRT74XgjftFGzNpvube+ov/EVBFi2gXjo" crossorigin="anonymous"></script>
<script src="../doxa-simple-map.js" integrity="sha384-r/K0P50Wt1Qz2KkJ+fVPurKtAr68GqWKREiLb8DYZ2L0VmOQUE9FwZhrDMZzYscr" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-FtEGLeSRNfBCdmK26IURIxCldSnfKphxRT74XgjftFGzNpvube+ov/EVBFi2gXjo" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-r/K0P50Wt1Qz2KkJ+fVPurKtAr68GqWKREiLb8DYZ2L0VmOQUE9FwZhrDMZzYscr" 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 <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-ltb70sEfk/M2TgjZ3hyZxrAL3R3TEpo7BnLnNXD+8fiEZjiNdln70Tj4z80qCn5M" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-Zk0IwHEx3TcFvlDr4oICFoK+g1HlmsVtkxl0xj/qQoifnjvbMN/DTKdI5VpncEf7" 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 <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-ltb70sEfk/M2TgjZ3hyZxrAL3R3TEpo7BnLnNXD+8fiEZjiNdln70Tj4z80qCn5M" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-Zk0IwHEx3TcFvlDr4oICFoK+g1HlmsVtkxl0xj/qQoifnjvbMN/DTKdI5VpncEf7" 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 <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-ltb70sEfk/M2TgjZ3hyZxrAL3R3TEpo7BnLnNXD+8fiEZjiNdln70Tj4z80qCn5M" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-Zk0IwHEx3TcFvlDr4oICFoK+g1HlmsVtkxl0xj/qQoifnjvbMN/DTKdI5VpncEf7" 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

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-ltb70sEfk/M2TgjZ3hyZxrAL3R3TEpo7BnLnNXD+8fiEZjiNdln70Tj4z80qCn5M" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-Zk0IwHEx3TcFvlDr4oICFoK+g1HlmsVtkxl0xj/qQoifnjvbMN/DTKdI5VpncEf7" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<script src="../gospel-resources.js" integrity="sha384-ltb70sEfk/M2TgjZ3hyZxrAL3R3TEpo7BnLnNXD+8fiEZjiNdln70Tj4z80qCn5M" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-Zk0IwHEx3TcFvlDr4oICFoK+g1HlmsVtkxl0xj/qQoifnjvbMN/DTKdI5VpncEf7" 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

@ -13,7 +13,7 @@
"bundle": "all-people-groups",
"scriptUrl": "./all-people-groups/all-people-groups.js",
"output": "doxa-maps/all-people-groups/all-people-groups.js",
"integrity": "sha384-3kskcN7GLnCNNOrJINJt4n5ERXa0qpwyRNcuVuw4++2KzU0LwGYnLW3aepEu8jDg",
"integrity": "sha384-i2Qv1FkHiMWhxFsOKGhfP4qSGTke/57SE1nrvk7ZV0lxfA5O92Ez6ZWf2dRbO88U",
"elements": [
"all-people-groups"
],
@ -29,7 +29,7 @@
"scriptUrl": "./all-people-groups/all-people-groups.js",
"embed": {
"iframe": "<iframe src=\"./all-people-groups/all-people-groups/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=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-3kskcN7GLnCNNOrJINJt4n5ERXa0qpwyRNcuVuw4++2KzU0LwGYnLW3aepEu8jDg\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-i2Qv1FkHiMWhxFsOKGhfP4qSGTke/57SE1nrvk7ZV0lxfA5O92Ez6ZWf2dRbO88U\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
},
"bundle": "all-people-groups"
}
@ -105,7 +105,7 @@
"bundle": "cities-internet",
"scriptUrl": "./cities-internet/cities-internet.js",
"output": "doxa-maps/cities-internet/cities-internet.js",
"integrity": "sha384-b7gkA5+oZ8azx5oSxl4c985U9gcOFyNAh3TfhJd4FSUX4XawU0927HvYSFRTNzpr",
"integrity": "sha384-cLy6LqX5n+KGC7VdympqtscDcHqEKchYgMh4GJbHO5Z6X9nYcD3IT7/3Crq/BGEE",
"elements": [
"cities-internet-map"
],
@ -121,7 +121,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-b7gkA5+oZ8azx5oSxl4c985U9gcOFyNAh3TfhJd4FSUX4XawU0927HvYSFRTNzpr\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-cLy6LqX5n+KGC7VdympqtscDcHqEKchYgMh4GJbHO5Z6X9nYcD3IT7/3Crq/BGEE\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -135,7 +135,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-b7gkA5+oZ8azx5oSxl4c985U9gcOFyNAh3TfhJd4FSUX4XawU0927HvYSFRTNzpr\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-cLy6LqX5n+KGC7VdympqtscDcHqEKchYgMh4GJbHO5Z6X9nYcD3IT7/3Crq/BGEE\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -149,7 +149,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/data-tables/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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-b7gkA5+oZ8azx5oSxl4c985U9gcOFyNAh3TfhJd4FSUX4XawU0927HvYSFRTNzpr\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-cLy6LqX5n+KGC7VdympqtscDcHqEKchYgMh4GJbHO5Z6X9nYcD3IT7/3Crq/BGEE\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
}
]
@ -158,7 +158,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "doxa-maps/country-posters/country-posters.js",
"integrity": "sha384-amheyqep13c83pKspBiNh8x4DIm5S/L7ifL34zSxsAe+LbpB0YW7DPH+a30ba6Xc",
"integrity": "sha384-S0O8LKY8FDeykxEoMkbMbI7dHp8DZQlNAxW+xj+9rJ/XDvlGUvY4t9zEnuRym7hd",
"elements": [
"country-posters-map"
],
@ -178,7 +178,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-amheyqep13c83pKspBiNh8x4DIm5S/L7ifL34zSxsAe+LbpB0YW7DPH+a30ba6Xc\" 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-S0O8LKY8FDeykxEoMkbMbI7dHp8DZQlNAxW+xj+9rJ/XDvlGUvY4t9zEnuRym7hd\" 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>"
}
},
{
@ -192,7 +192,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-amheyqep13c83pKspBiNh8x4DIm5S/L7ifL34zSxsAe+LbpB0YW7DPH+a30ba6Xc\" 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-S0O8LKY8FDeykxEoMkbMbI7dHp8DZQlNAxW+xj+9rJ/XDvlGUvY4t9zEnuRym7hd\" 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>"
}
},
{
@ -206,7 +206,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-amheyqep13c83pKspBiNh8x4DIm5S/L7ifL34zSxsAe+LbpB0YW7DPH+a30ba6Xc\" 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-S0O8LKY8FDeykxEoMkbMbI7dHp8DZQlNAxW+xj+9rJ/XDvlGUvY4t9zEnuRym7hd\" 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>"
}
}
]
@ -215,7 +215,7 @@
"bundle": "dashboard-plus-grid",
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"output": "doxa-maps/dashboard-plus-grid/dashboard-plus-grid.js",
"integrity": "sha384-VOkoTUw4VURBoPSbk3YnU3Wc/JuN+ywf1eF9D6RHBI2Be/8N8FlY3WX/2CkNk5NS",
"integrity": "sha384-azKIIIa3yMfrc8/hxS9kf+J2zIQGcw/p/SxLoyil51AqGaH+MJad4mxPI9bycxiL",
"elements": [
"dashboard-plus-grid"
],
@ -235,7 +235,7 @@
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"embed": {
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-VOkoTUw4VURBoPSbk3YnU3Wc/JuN+ywf1eF9D6RHBI2Be/8N8FlY3WX/2CkNk5NS\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-azKIIIa3yMfrc8/hxS9kf+J2zIQGcw/p/SxLoyil51AqGaH+MJad4mxPI9bycxiL\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
},
"instances": [
{
@ -259,7 +259,7 @@
"bundle": "demo-map",
"scriptUrl": "./demo-map/demo-map.js",
"output": "doxa-maps/demo-map/demo-map.js",
"integrity": "sha384-WLB+/GTY8u9CWAKt72vKJroKi26oGzzjlGdA0iA3DSSP9oWHnUb8r6jtjAWSw0ee",
"integrity": "sha384-VGZMQWOWVouP4IkUTsZ/ElUORaUYu9+wJSlbs/8r6Lc6EU8VvLYHhmTOGCJv2Y6H",
"elements": [
"demo-map"
],
@ -275,7 +275,7 @@
"scriptUrl": "./demo-map/demo-map.js",
"embed": {
"iframe": "<iframe src=\"./demo-map/demo-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=\"./demo-map/demo-map.js\" integrity=\"sha384-WLB+/GTY8u9CWAKt72vKJroKi26oGzzjlGdA0iA3DSSP9oWHnUb8r6jtjAWSw0ee\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./demo-map/demo-map.js\" integrity=\"sha384-VGZMQWOWVouP4IkUTsZ/ElUORaUYu9+wJSlbs/8r6Lc6EU8VvLYHhmTOGCJv2Y6H\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
},
"instances": [
{
@ -302,7 +302,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-fmp37RdhrkxuguGQ5MmwnakDu0So2YCJSYj8Bca867HoKCT1497o9z3K/wlHDlxP",
"integrity": "sha384-Cmdw0USq0X4/+OshXBayXKy4U5ZBD0bR8YR/yaR/CZ76CYEMpb4bymgfug+5AGyU",
"elements": [
"doxa-maps-spa"
],
@ -327,7 +327,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-fmp37RdhrkxuguGQ5MmwnakDu0So2YCJSYj8Bca867HoKCT1497o9z3K/wlHDlxP\" 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-Cmdw0USq0X4/+OshXBayXKy4U5ZBD0bR8YR/yaR/CZ76CYEMpb4bymgfug+5AGyU\" 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": [
{
@ -354,7 +354,7 @@
"bundle": "doxa-research-map",
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"output": "doxa-maps/doxa-research-map/doxa-research-map.js",
"integrity": "sha384-LyIs0sqdkyJcj3DESqefXc7jsD96o+H+YVcOIq2kCdwaS+ZeBLqcn//06n1W1blE",
"integrity": "sha384-E7/WP0pDnwiG6noaZZI3Sx92Cg3JwluCqYOA5eU+x+UARKkliojcJBk3Yx6jua7a",
"elements": [
"doxa-research-map"
],
@ -370,7 +370,7 @@
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-research-map/research-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=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-LyIs0sqdkyJcj3DESqefXc7jsD96o+H+YVcOIq2kCdwaS+ZeBLqcn//06n1W1blE\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-E7/WP0pDnwiG6noaZZI3Sx92Cg3JwluCqYOA5eU+x+UARKkliojcJBk3Yx6jua7a\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
},
"instances": [
{
@ -396,7 +396,7 @@
"bundle": "doxa-simple-map",
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"output": "doxa-maps/doxa-simple-map/doxa-simple-map.js",
"integrity": "sha384-FtEGLeSRNfBCdmK26IURIxCldSnfKphxRT74XgjftFGzNpvube+ov/EVBFi2gXjo",
"integrity": "sha384-r/K0P50Wt1Qz2KkJ+fVPurKtAr68GqWKREiLb8DYZ2L0VmOQUE9FwZhrDMZzYscr",
"elements": [
"doxa-map",
"doxa-simple-map"
@ -413,7 +413,7 @@
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-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=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-FtEGLeSRNfBCdmK26IURIxCldSnfKphxRT74XgjftFGzNpvube+ov/EVBFi2gXjo\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-r/K0P50Wt1Qz2KkJ+fVPurKtAr68GqWKREiLb8DYZ2L0VmOQUE9FwZhrDMZzYscr\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
},
"instances": [
{
@ -596,7 +596,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "doxa-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-ltb70sEfk/M2TgjZ3hyZxrAL3R3TEpo7BnLnNXD+8fiEZjiNdln70Tj4z80qCn5M",
"integrity": "sha384-Zk0IwHEx3TcFvlDr4oICFoK+g1HlmsVtkxl0xj/qQoifnjvbMN/DTKdI5VpncEf7",
"elements": [
"gospel-resources-map"
],
@ -616,7 +616,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/charts/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-ltb70sEfk/M2TgjZ3hyZxrAL3R3TEpo7BnLnNXD+8fiEZjiNdln70Tj4z80qCn5M\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-Zk0IwHEx3TcFvlDr4oICFoK+g1HlmsVtkxl0xj/qQoifnjvbMN/DTKdI5VpncEf7\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -630,7 +630,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/data-tables/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-ltb70sEfk/M2TgjZ3hyZxrAL3R3TEpo7BnLnNXD+8fiEZjiNdln70Tj4z80qCn5M\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-Zk0IwHEx3TcFvlDr4oICFoK+g1HlmsVtkxl0xj/qQoifnjvbMN/DTKdI5VpncEf7\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -644,7 +644,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-ltb70sEfk/M2TgjZ3hyZxrAL3R3TEpo7BnLnNXD+8fiEZjiNdln70Tj4z80qCn5M\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-Zk0IwHEx3TcFvlDr4oICFoK+g1HlmsVtkxl0xj/qQoifnjvbMN/DTKdI5VpncEf7\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -658,7 +658,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-ltb70sEfk/M2TgjZ3hyZxrAL3R3TEpo7BnLnNXD+8fiEZjiNdln70Tj4z80qCn5M\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-Zk0IwHEx3TcFvlDr4oICFoK+g1HlmsVtkxl0xj/qQoifnjvbMN/DTKdI5VpncEf7\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
},
{
@ -672,7 +672,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-ltb70sEfk/M2TgjZ3hyZxrAL3R3TEpo7BnLnNXD+8fiEZjiNdln70Tj4z80qCn5M\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-Zk0IwHEx3TcFvlDr4oICFoK+g1HlmsVtkxl0xj/qQoifnjvbMN/DTKdI5VpncEf7\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
}
}
]
@ -681,7 +681,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-4WnbHvh/qt/xIADBpWm8xKj0gcGj6hStHIGhrTLibVZtGGga8Bd/YXh31SD/tDCv",
"integrity": "sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf",
"elements": [
"my-upg-100-list-map"
],
@ -701,7 +701,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-4WnbHvh/qt/xIADBpWm8xKj0gcGj6hStHIGhrTLibVZtGGga8Bd/YXh31SD/tDCv\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -715,7 +715,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-4WnbHvh/qt/xIADBpWm8xKj0gcGj6hStHIGhrTLibVZtGGga8Bd/YXh31SD/tDCv\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
},
{
@ -729,7 +729,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-4WnbHvh/qt/xIADBpWm8xKj0gcGj6hStHIGhrTLibVZtGGga8Bd/YXh31SD/tDCv\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
}
}
]
@ -799,7 +799,7 @@
"bundle": "plugin-studio",
"scriptUrl": "./plugin-studio/plugin-studio.js",
"output": "doxa-maps/plugin-studio/plugin-studio.js",
"integrity": "sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH",
"integrity": "sha384-D++rzhYeOQb6jBAa8CIB+8VvmvRij/YX1WF9TVHHLkGmUZUZSoOwUx6cgFuzkHOS",
"elements": [
"plugin-studio"
],
@ -827,7 +827,7 @@
"scriptUrl": "./plugin-studio/plugin-studio.js",
"embed": {
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/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=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-D++rzhYeOQb6jBAa8CIB+8VvmvRij/YX1WF9TVHHLkGmUZUZSoOwUx6cgFuzkHOS\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
},
"instances": [
{
@ -879,7 +879,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-o2n8vzVstdRyYCQ4YnWVCpZYCOfaNGa+4BABLAeVw5vuXbnm1R71Zx3WT9FJc22n",
"integrity": "sha384-RROi/svcB/vhqo1W8SwZCYhJbi2HbGft9lGxxJQ4UAYjNzrbHKjSzJ6MmS7imr6o",
"elements": [
"regions-dashboard"
],
@ -899,7 +899,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-o2n8vzVstdRyYCQ4YnWVCpZYCOfaNGa+4BABLAeVw5vuXbnm1R71Zx3WT9FJc22n\" 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-RROi/svcB/vhqo1W8SwZCYhJbi2HbGft9lGxxJQ4UAYjNzrbHKjSzJ6MmS7imr6o\" 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>"
}
},
{
@ -913,7 +913,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-o2n8vzVstdRyYCQ4YnWVCpZYCOfaNGa+4BABLAeVw5vuXbnm1R71Zx3WT9FJc22n\" 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-RROi/svcB/vhqo1W8SwZCYhJbi2HbGft9lGxxJQ4UAYjNzrbHKjSzJ6MmS7imr6o\" 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>"
}
},
{
@ -927,7 +927,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-o2n8vzVstdRyYCQ4YnWVCpZYCOfaNGa+4BABLAeVw5vuXbnm1R71Zx3WT9FJc22n\" 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-RROi/svcB/vhqo1W8SwZCYhJbi2HbGft9lGxxJQ4UAYjNzrbHKjSzJ6MmS7imr6o\" 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>"
}
}
]
@ -936,7 +936,7 @@
"bundle": "research-dashboard",
"scriptUrl": "./research-dashboard/research-dashboard.js",
"output": "doxa-maps/research-dashboard/research-dashboard.js",
"integrity": "sha384-tAUIe2wERKg/ZRvINilfjrgfNYZeix7iSXYTJI1iSvf3eRtafkVt1a8cHIGyyRTF",
"integrity": "sha384-hlPWMwkdwPg7Ze8QD4PwXTNv65fDKqwUOCrtX8NsH8h+rZHK1rZSnq43Vsz9O7Kt",
"elements": [
"research-dashboard-map"
],
@ -952,7 +952,7 @@
"scriptUrl": "./research-dashboard/research-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-tAUIe2wERKg/ZRvINilfjrgfNYZeix7iSXYTJI1iSvf3eRtafkVt1a8cHIGyyRTF\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-hlPWMwkdwPg7Ze8QD4PwXTNv65fDKqwUOCrtX8NsH8h+rZHK1rZSnq43Vsz9O7Kt\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
},
"bundle": "research-dashboard"
}
@ -962,7 +962,7 @@
"bundle": "research-map-plus-grid",
"scriptUrl": "./research-map-plus-grid/research-map-plus-grid.js",
"output": "doxa-maps/research-map-plus-grid/research-map-plus-grid.js",
"integrity": "sha384-sOkaMV0UFr2uNXfoJpRvaD+gHD6xFlkmpR7MzEwwFdarYdid8aN9Nj1Y8TqQJuFo",
"integrity": "sha384-LQYR+uASPJFjo/VTpIpAVxUv+O5KrgBqfOFYOcOi0ZmIvtwcjWTZV4AjAr2ewc7Z",
"elements": [
"research-map-plus-grid"
],
@ -982,7 +982,7 @@
"scriptUrl": "./research-map-plus-grid/research-map-plus-grid.js",
"embed": {
"iframe": "<iframe src=\"./research-map-plus-grid/research-map-plus-grid/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=\"./research-map-plus-grid/research-map-plus-grid.js\" integrity=\"sha384-sOkaMV0UFr2uNXfoJpRvaD+gHD6xFlkmpR7MzEwwFdarYdid8aN9Nj1Y8TqQJuFo\" crossorigin=\"anonymous\"></script>\n<research-map-plus-grid profile-config='{\"profile\":\"research-map-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-map-plus-grid>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-map-plus-grid/research-map-plus-grid.js\" integrity=\"sha384-LQYR+uASPJFjo/VTpIpAVxUv+O5KrgBqfOFYOcOi0ZmIvtwcjWTZV4AjAr2ewc7Z\" crossorigin=\"anonymous\"></script>\n<research-map-plus-grid profile-config='{\"profile\":\"research-map-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-map-plus-grid>"
},
"instances": [
{
@ -1006,7 +1006,7 @@
"bundle": "stack-demo",
"scriptUrl": "./stack-demo/stack-demo.js",
"output": "doxa-maps/stack-demo/stack-demo.js",
"integrity": "sha384-gnwM6s+puf1K60Nl7crQcC2QC+9BxwpkaTx4OepLZ8VUCraZc+aRlWpsb6nj6Gyf",
"integrity": "sha384-EI3V6JX9ws4fGxKBzMOgwhG5de8MvL6va3zqmB2gTXecd/m0mxNq8eUht/SGzH1B",
"elements": [
"stack-demo"
],
@ -1026,7 +1026,7 @@
"scriptUrl": "./stack-demo/stack-demo.js",
"embed": {
"iframe": "<iframe src=\"./stack-demo/stack-demo/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=\"./stack-demo/stack-demo.js\" integrity=\"sha384-gnwM6s+puf1K60Nl7crQcC2QC+9BxwpkaTx4OepLZ8VUCraZc+aRlWpsb6nj6Gyf\" crossorigin=\"anonymous\"></script>\n<stack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></stack-demo>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./stack-demo/stack-demo.js\" integrity=\"sha384-EI3V6JX9ws4fGxKBzMOgwhG5de8MvL6va3zqmB2gTXecd/m0mxNq8eUht/SGzH1B\" crossorigin=\"anonymous\"></script>\n<stack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></stack-demo>"
},
"instances": [
{
@ -1050,7 +1050,7 @@
"bundle": "template-bundle",
"scriptUrl": "./template-bundle/template-bundle.js",
"output": "doxa-maps/template-bundle/template-bundle.js",
"integrity": "sha384-Arx3XL5Mhdv0VcU7RX9gwggLBjx3SH4WFltOaoEJ6mW/jw/E6wgGQHIOkFyRtBRd",
"integrity": "sha384-LhhbW7T6VzMBsasbX4ezhA2vembuHwp1cNM1WNooIHMdGp00x8rOr7F9NdMndaG4",
"elements": [
"template-bundle"
],
@ -1066,7 +1066,7 @@
"scriptUrl": "./template-bundle/template-bundle.js",
"embed": {
"iframe": "<iframe src=\"./template-bundle/template-map-a/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=\"./template-bundle/template-bundle.js\" integrity=\"sha384-Arx3XL5Mhdv0VcU7RX9gwggLBjx3SH4WFltOaoEJ6mW/jw/E6wgGQHIOkFyRtBRd\" crossorigin=\"anonymous\"></script>\n<template-bundle profile-config='{\"profile\":\"template-map-a\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></template-bundle>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-LhhbW7T6VzMBsasbX4ezhA2vembuHwp1cNM1WNooIHMdGp00x8rOr7F9NdMndaG4\" crossorigin=\"anonymous\"></script>\n<template-bundle profile-config='{\"profile\":\"template-map-a\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></template-bundle>"
},
"bundle": "template-bundle"
},
@ -1081,7 +1081,7 @@
"scriptUrl": "./template-bundle/template-bundle.js",
"embed": {
"iframe": "<iframe src=\"./template-bundle/template-map-b/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=\"./template-bundle/template-bundle.js\" integrity=\"sha384-Arx3XL5Mhdv0VcU7RX9gwggLBjx3SH4WFltOaoEJ6mW/jw/E6wgGQHIOkFyRtBRd\" crossorigin=\"anonymous\"></script>\n<template-bundle profile-config='{\"profile\":\"template-map-b\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></template-bundle>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-LhhbW7T6VzMBsasbX4ezhA2vembuHwp1cNM1WNooIHMdGp00x8rOr7F9NdMndaG4\" crossorigin=\"anonymous\"></script>\n<template-bundle profile-config='{\"profile\":\"template-map-b\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></template-bundle>"
},
"bundle": "template-bundle"
}
@ -1091,7 +1091,7 @@
"bundle": "upg-neighbor",
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"output": "doxa-maps/upg-neighbor/upg-neighbor.js",
"integrity": "sha384-nhreYhnnIGRgIkQXr3h0xmwX71B2Yhd47erUuAOAqAYgTyNw/IvXaO/FjR09wvKz",
"integrity": "sha384-EwHRhn6v9ffMpvgViAzs6KpLdGI5FS0jhrU0E4bFfERGt0NywaxilnsRPQdcRcdX",
"elements": [
"upg-neighbor"
],
@ -1107,7 +1107,7 @@
"scriptUrl": "./upg-neighbor/upg-neighbor.js",
"embed": {
"iframe": "<iframe src=\"./upg-neighbor/upg-neighbor/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=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-nhreYhnnIGRgIkQXr3h0xmwX71B2Yhd47erUuAOAqAYgTyNw/IvXaO/FjR09wvKz\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-EwHRhn6v9ffMpvgViAzs6KpLdGI5FS0jhrU0E4bFfERGt0NywaxilnsRPQdcRcdX\" crossorigin=\"anonymous\"></script>\n<upg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></upg-neighbor>"
},
"instances": [
{

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 <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-4WnbHvh/qt/xIADBpWm8xKj0gcGj6hStHIGhrTLibVZtGGga8Bd/YXh31SD/tDCv" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-4WnbHvh/qt/xIADBpWm8xKj0gcGj6hStHIGhrTLibVZtGGga8Bd/YXh31SD/tDCv" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-4WnbHvh/qt/xIADBpWm8xKj0gcGj6hStHIGhrTLibVZtGGga8Bd/YXh31SD/tDCv" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-eEbOnVD6pMXOvGXN1VAnX8J3UfTAVruDCirw1585wIGTS+LwN/gCvupPPecye4yf" 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

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <plugin-studio> custom element. -->
<script src="../plugin-studio.js" integrity="sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH" crossorigin="anonymous"></script>
<script src="../plugin-studio.js" integrity="sha384-D++rzhYeOQb6jBAa8CIB+8VvmvRij/YX1WF9TVHHLkGmUZUZSoOwUx6cgFuzkHOS" 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 <plugin-studio> custom element. -->
<script src="../../plugin-studio.js" integrity="sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH" crossorigin="anonymous"></script>
<script src="../../plugin-studio.js" integrity="sha384-D++rzhYeOQb6jBAa8CIB+8VvmvRij/YX1WF9TVHHLkGmUZUZSoOwUx6cgFuzkHOS" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-o2n8vzVstdRyYCQ4YnWVCpZYCOfaNGa+4BABLAeVw5vuXbnm1R71Zx3WT9FJc22n" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-RROi/svcB/vhqo1W8SwZCYhJbi2HbGft9lGxxJQ4UAYjNzrbHKjSzJ6MmS7imr6o" 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-o2n8vzVstdRyYCQ4YnWVCpZYCOfaNGa+4BABLAeVw5vuXbnm1R71Zx3WT9FJc22n" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-RROi/svcB/vhqo1W8SwZCYhJbi2HbGft9lGxxJQ4UAYjNzrbHKjSzJ6MmS7imr6o" 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-o2n8vzVstdRyYCQ4YnWVCpZYCOfaNGa+4BABLAeVw5vuXbnm1R71Zx3WT9FJc22n" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-RROi/svcB/vhqo1W8SwZCYhJbi2HbGft9lGxxJQ4UAYjNzrbHKjSzJ6MmS7imr6o" 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 <research-dashboard-map> custom element. -->
<script src="../research-dashboard.js" integrity="sha384-tAUIe2wERKg/ZRvINilfjrgfNYZeix7iSXYTJI1iSvf3eRtafkVt1a8cHIGyyRTF" crossorigin="anonymous"></script>
<script src="../research-dashboard.js" integrity="sha384-hlPWMwkdwPg7Ze8QD4PwXTNv65fDKqwUOCrtX8NsH8h+rZHK1rZSnq43Vsz9O7Kt" 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

@ -37,7 +37,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <research-map-plus-grid> custom element. -->
<script src="../../research-map-plus-grid.js" integrity="sha384-sOkaMV0UFr2uNXfoJpRvaD+gHD6xFlkmpR7MzEwwFdarYdid8aN9Nj1Y8TqQJuFo" crossorigin="anonymous"></script>
<script src="../../research-map-plus-grid.js" integrity="sha384-LQYR+uASPJFjo/VTpIpAVxUv+O5KrgBqfOFYOcOi0ZmIvtwcjWTZV4AjAr2ewc7Z" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -37,7 +37,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <research-map-plus-grid> custom element. -->
<script src="../research-map-plus-grid.js" integrity="sha384-sOkaMV0UFr2uNXfoJpRvaD+gHD6xFlkmpR7MzEwwFdarYdid8aN9Nj1Y8TqQJuFo" crossorigin="anonymous"></script>
<script src="../research-map-plus-grid.js" integrity="sha384-LQYR+uASPJFjo/VTpIpAVxUv+O5KrgBqfOFYOcOi0ZmIvtwcjWTZV4AjAr2ewc7Z" 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

@ -37,7 +37,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <stack-demo> custom element. -->
<script src="../stack-demo.js" integrity="sha384-gnwM6s+puf1K60Nl7crQcC2QC+9BxwpkaTx4OepLZ8VUCraZc+aRlWpsb6nj6Gyf" crossorigin="anonymous"></script>
<script src="../stack-demo.js" integrity="sha384-EI3V6JX9ws4fGxKBzMOgwhG5de8MvL6va3zqmB2gTXecd/m0mxNq8eUht/SGzH1B" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -37,7 +37,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <stack-demo> custom element. -->
<script src="../../stack-demo.js" integrity="sha384-gnwM6s+puf1K60Nl7crQcC2QC+9BxwpkaTx4OepLZ8VUCraZc+aRlWpsb6nj6Gyf" crossorigin="anonymous"></script>
<script src="../../stack-demo.js" integrity="sha384-EI3V6JX9ws4fGxKBzMOgwhG5de8MvL6va3zqmB2gTXecd/m0mxNq8eUht/SGzH1B" 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

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <template-bundle> custom element. -->
<script src="../template-bundle.js" integrity="sha384-Arx3XL5Mhdv0VcU7RX9gwggLBjx3SH4WFltOaoEJ6mW/jw/E6wgGQHIOkFyRtBRd" crossorigin="anonymous"></script>
<script src="../template-bundle.js" integrity="sha384-LhhbW7T6VzMBsasbX4ezhA2vembuHwp1cNM1WNooIHMdGp00x8rOr7F9NdMndaG4" 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 <template-bundle> custom element. -->
<script src="../template-bundle.js" integrity="sha384-Arx3XL5Mhdv0VcU7RX9gwggLBjx3SH4WFltOaoEJ6mW/jw/E6wgGQHIOkFyRtBRd" crossorigin="anonymous"></script>
<script src="../template-bundle.js" integrity="sha384-LhhbW7T6VzMBsasbX4ezhA2vembuHwp1cNM1WNooIHMdGp00x8rOr7F9NdMndaG4" 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 <upg-neighbor> custom element. -->
<script src="../../upg-neighbor.js" integrity="sha384-nhreYhnnIGRgIkQXr3h0xmwX71B2Yhd47erUuAOAqAYgTyNw/IvXaO/FjR09wvKz" crossorigin="anonymous"></script>
<script src="../../upg-neighbor.js" integrity="sha384-EwHRhn6v9ffMpvgViAzs6KpLdGI5FS0jhrU0E4bFfERGt0NywaxilnsRPQdcRcdX" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

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

View file

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

View file

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

View file

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

View file

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