Some checks are pending
build-gate / build (push) Waiting to run
120 keys across three locales: legend.json (12 each), search.json kinds (10
each) and tabs.json (18 each, previously absent entirely). Each locale follows
its own established vocabulary rather than a parallel one — Popolo/Popor/民族,
matching the terms already used in that catalog's detail and legend strings.
zh stays Simplified here, as the rest of this tree is, even though the prayer
packet's zh block is Traditional: a reader sees 民族 next to 祷告进度, never
the two files side by side.
Ceilings drop 94 -> 54 per locale. regions.json is the only i18n debt left in
the app: 54 keys of full sentences, three locales.
WHY THE NEW GATE. check:i18n-parity compares catalogs TO EACH OTHER, so it
certifies a file no t() call ever reaches — perfect symmetry, zero consumers,
green forever. Grepping for t('tabs. found ZERO call sites and tabs.json was
nearly recorded as dead. It is not: the only consumer builds the key
dynamically (SemanticTreeLegend.vue:40, 'tabs.' + (tab.i18nKey || tab.id)),
which no literal-prefix grep can see. The reverse error appeared in the same
pass — regions. and options. matched only poster-regions.mjs and a JS options
parameter. Static reachability is unavailable in both directions, so
check:tabs-localized renders the labels and reads them.
It asserts DIFFERENCE from English, not mere presence: te() falls back to the
hardcoded English label, so "shows some text" passes on the exact failure
being tested. Mutation-tested three ways — missing catalog FAILs, an all-English
catalog FAILs (first draft PASSed it, taking a per-pair exemption seven times),
one legitimate collision still passes with a note.
Both i18n gates now run in gates:post; neither was wired into the build before.
Also fixed, found by the build rather than sought:
- check-i18n-parity hardcoded an absolute path, so the studio copy silently
graded wt-packet's locales and reported PASS on a tree it never read. Now
repo-relative. That exposed the studio's own locale tree as a strict subset,
103 keys behind including buttons.json:share — the very orphan this gate's
header says it was written to catch, reappeared unseen in a second repo.
Synced forward: zero studio-only keys, zero text conflicts, lossless.
- index.json still declared field-access-visas with rows:1146, a leftover from
the commit that removed that table as private. The generator already filters
private tables; the committed artifact predated the filter. Removing the
declaration also removes the row-count leak. Copying the file in would have
reopened the leak.
- check:sfc-parse needs an installed node_modules and is the first pre-gate to
fail without one, so a dependency-less worktree reads as a parse error.
Documented, with the reason switching it to bun only appears to fix it.
414 lines
68 KiB
HTML
414 lines
68 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>doxa-maps</title>
|
|
<link rel="icon" href="data:,">
|
|
<style>
|
|
:root{--bg:#fff;--bg2:#f6f7f9;--bg3:#eceef2;--bd:#e2e5ea;--fg:#15181d;--mut:#667085;--err:#d13c3c}
|
|
@media(prefers-color-scheme:dark){:root{--bg:#111318;--bg2:#181b21;--bg3:#232730;--bd:#2b3038;--fg:#eef0f3;--mut:#9aa3b2;--err:#ff7b7b}}
|
|
*{box-sizing:border-box}html,body{margin:0;height:100%}
|
|
body{background:var(--bg);color:var(--fg);font:14px/1.4 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;display:flex;flex-direction:column;height:100vh}
|
|
header{flex:0 0 auto;display:flex;align-items:center;gap:10px;padding:8px 14px;border-bottom:1px solid var(--bd)}
|
|
header b{font-size:15px}.n{color:var(--mut);font-size:12px}
|
|
#menu{display:none;margin-left:auto}
|
|
button,.btn{font:inherit;font-size:12px;padding:5px 10px;border:1px solid var(--bd);border-radius:8px;background:var(--bg2);color:var(--fg);cursor:pointer;text-decoration:none;white-space:nowrap}
|
|
button:hover,.btn:hover{background:var(--bg3)}
|
|
.body{flex:1 1 auto;display:flex;min-height:0}
|
|
nav{flex:0 0 270px;overflow:auto;border-right:1px solid var(--bd);padding:6px 0 20px}
|
|
.gh{padding:12px 14px 4px;font-weight:600;font-size:13px;display:flex;justify-content:space-between;gap:8px}.gh span{color:var(--mut);font-weight:400}
|
|
.row{display:block;width:100%;text-align:left;border:0;border-radius:0;background:none;padding:6px 14px;font-size:13px;line-height:1.3;white-space:normal}
|
|
.row.in{padding-left:28px}.row code{display:block;font-size:11px;color:var(--mut)}
|
|
.row:hover{background:var(--bg2)}.row[aria-current=true]{background:var(--bg3);font-weight:600}
|
|
main{flex:1 1 auto;display:flex;flex-direction:column;min-width:0;padding:10px 12px 12px;gap:8px}
|
|
.bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
.bar .t{flex:1 1 auto;min-width:0}.bar b{font-size:15px}.s{display:block;font-size:12px;color:var(--mut)}
|
|
code{font-family:ui-monospace,Menlo,monospace}
|
|
#embed{display:flex;flex-direction:column;gap:6px;padding:8px;border:1px solid var(--bd);border-radius:8px;background:var(--bg2)}
|
|
#embed label{font-size:12px;color:var(--mut);display:flex;justify-content:space-between;align-items:center}
|
|
#embed textarea{width:100%;height:58px;font:11px/1.35 ui-monospace,Menlo,monospace;border:1px solid var(--bd);border-radius:6px;background:var(--bg);color:var(--fg);padding:6px;resize:vertical}
|
|
#slot{flex:1 1 auto;position:relative;min-height:240px;border:1px solid var(--bd);border-radius:10px;overflow:hidden;background:var(--bg2)}
|
|
#slot>*{position:absolute;inset:0;width:100%;height:100%;display:block;border:0}
|
|
.hint{display:grid;place-items:center;padding:20px;text-align:center;color:var(--mut);font-size:13px}
|
|
.hint.err{color:var(--err)}
|
|
.hint input{font:12px ui-monospace,Menlo,monospace;padding:6px 8px;border-radius:6px;border:1px solid var(--bd);background:var(--bg);color:var(--fg);width:min(320px,70vw)}
|
|
/* Capability tiers (5 → 1): the vertical-tab sections of the nested-mfe-spa sidebar. */
|
|
.tier{padding:12px 14px 2px;display:flex;align-items:baseline;gap:7px}
|
|
.tier .cap{font:700 10px/1 system-ui,sans-serif;letter-spacing:.5px;color:var(--bg);background:var(--mut);border-radius:999px;padding:3px 7px}
|
|
.tier .tl{font-weight:600;font-size:12px}.tier .td{color:var(--mut);font-size:11px;margin-left:auto}
|
|
#app{flex:1 1 auto;display:flex;flex-direction:column;min-height:0}
|
|
[hidden]{display:none!important}
|
|
@media(max-width:700px){.body{flex-direction:column}nav{flex:0 0 auto;max-height:45vh;border-right:0;border-bottom:1px solid var(--bd)}#menu{display:inline-block}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- The doxa-maps nested-mfe-spa (capability 5, the reserved app-shell): a Vue single-page app
|
|
whose sidebar is the composability ladder rendered as vertical tabs (capability 5 → 1),
|
|
each row mounting that plugin's custom element in the one slot. This SAME page is the
|
|
served studio root (public/index.html) AND the Capacitor mobile-app entry (webDir: public).
|
|
Vue drives the shell; the map elements mount imperatively into #slot (they own their DOM). -->
|
|
<div id="app">
|
|
<header>
|
|
<b>doxa-maps</b><span class="n">{{ countLabel }}</span>
|
|
<button id="menu" type="button" aria-controls="nav" aria-expanded="true" @click="navOpen = !navOpen">Maps</button>
|
|
</header>
|
|
<div class="body">
|
|
<nav id="nav" aria-label="Maps · sorted by capability 5 → 1" v-show="navOpen">
|
|
<div class="grp" v-for="grp in groups" :key="grp.cap">
|
|
<div class="tier"><span class="cap">CAP {{ grp.cap }}</span><span class="tl">{{ grp.label }}</span><span class="td">{{ grp.rows.length }}</span></div>
|
|
<!-- data-k EXPOSES THE ROW IDENTITY TO THE DOM. A Vue :key is a RECONCILIATION HINT and is NOT
|
|
rendered as an attribute — a distinction that had smoke:gallery failing on a working page.
|
|
That gate reads row.dataset.k to confirm a deep link selected the row it asked for; with no
|
|
such attribute it read undefined and reported "deep link resolved to undefined".
|
|
MEASURED 2026-09-22: data-k appeared ZERO times in this generator and zero times in the
|
|
built page, while the gallery itself mounts its elements correctly — a false RED, which is
|
|
as corrosive as a false green because it gets the gate switched off.
|
|
Asserting on the rendered LABEL instead would be worse: it couples a machine check to
|
|
display text, so renaming a profile would break the gate. Identity belongs in the DOM.
|
|
NOTE: this markup lives inside a TEMPLATE LITERAL — no backticks anywhere in this comment,
|
|
or the literal ends here and the rest of the file is parsed as code. -->
|
|
<button v-for="r in grp.rows" :key="r.key" :data-k="r.key" type="button" class="row" :class="{ in: r.kind === 'instance' }"
|
|
:aria-current="String(activeKey === r.key)" @click="go(r.key)">
|
|
<span>{{ r.label }}</span><code><{{ r.tag }}></code>
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
<main>
|
|
<div class="bar">
|
|
<div class="t"><b>{{ ttl }}</b><span class="s">{{ sub }}</span></div>
|
|
<a class="btn" :href="openHref" target="_blank" rel="noopener" v-show="openHref">Open page</a>
|
|
<button type="button" v-show="hasSel" :aria-expanded="String(embedOpen)" @click="embedOpen = !embedOpen">Copy embed</button>
|
|
</div>
|
|
<div id="embed" v-show="embedOpen && hasSel">
|
|
<label>iframe embed <button type="button" @click="copyText(embedIframe, $event.target)">Copy</button></label>
|
|
<textarea readonly :value="embedIframe"></textarea>
|
|
<div v-show="embedScript"><label>script embed <button type="button" @click="copyText(embedScript, $event.target)">Copy</button></label>
|
|
<textarea readonly :value="embedScript"></textarea></div>
|
|
</div>
|
|
<div id="slot" ref="slot"><div class="hint">Choose a map from the list.</div></div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
var TREE = "../doxa-maps", TOKEN_SRC = "window.MAP_TOKEN_URL", BAKED_TOKEN = "pk.eyJ1IjoiZ29zeXN0ZW1zIiwiYSI6ImNtZm9vM291czA3amQya3BzYW42YW9ibTcifQ.52WM0IpxY9FcB0zc9f1zbQ", API_BASE = "https://pray.doxa.life", ASSETS_BASE = "", QA = false;
|
|
var DEFAULT_TOKEN_URL = '/api/maps/token';
|
|
var BAKED = [{"b":"all-people-groups","s":"./all-people-groups/all-people-groups.js","i":"sha384-wRewQ3RMeMMt4aBtsLefcXPap77pkP0fomuLnBKvQMLRtcqOvlHlFRcAByztpXyt","e":["all-people-groups"],"p":[{"p":"all-people-groups","e":"all-people-groups","u":"./all-people-groups/all-people-groups/index.html","d":"All 12,379 people groups (peoplegroups.org) in four colors — reached,","c":1,"em":{"iframe":"\u003ciframe src=\"./all-people-groups/all-people-groups/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-wRewQ3RMeMMt4aBtsLefcXPap77pkP0fomuLnBKvQMLRtcqOvlHlFRcAByztpXyt\" crossorigin=\"anonymous\">\u003c/script>\n\u003call-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/all-people-groups>"}}]},{"b":"big-picture","s":"./big-picture/big-picture.js","i":"sha384-voYmtWeACTFBJOOeqcbf5cuBKSjyskA89cjvm5qcvrx9IUFTAhLuICyRXUM6fL+Y","e":["bp-big-picture","bp-research-header","people-groups-grid"],"p":[{"p":"big-picture","e":"bp-big-picture","u":"./big-picture/big-picture/index.html","d":"The Cat-4 composite that PAIRS the two real capability elements of the","c":4,"em":{"iframe":"\u003ciframe src=\"./big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./big-picture/big-picture.js\" integrity=\"sha384-voYmtWeACTFBJOOeqcbf5cuBKSjyskA89cjvm5qcvrx9IUFTAhLuICyRXUM6fL+Y\" crossorigin=\"anonymous\">\u003c/script>\n\u003cbp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/bp-big-picture>"},"in":[{"id":"big-picture","t":"Big Picture — research header ↔ people-groups grid (composite)","a":"","pr":{"profile":"big-picture","provider":"maplibre","instanceId":"bp-big-picture"},"e":"bp-big-picture","u":"./big-picture/big-picture/big-picture/index.html"},{"id":"frontier","t":"Big Picture — frontier accent","a":"","pr":{"profile":"big-picture","provider":"maplibre","instanceId":"bp-big-picture-frontier","accent":"#22d3ee","title":"Big Picture — Frontier Focus"},"e":"bp-big-picture","u":"./big-picture/big-picture/frontier/index.html"}]}]},{"b":"cities-internet","s":"./cities-internet/cities-internet.js","i":"sha384-yXvole+EYjOppLtZOqOW4pba/oWzbObcdkoSpYcnukHuARdg6R/qHzhOd5cpCJY0","e":["cities-internet-map"],"p":[{"p":"connectivity-list","e":"cities-internet-map","u":"./cities-internet/connectivity-list/index.html","d":"Countries where DX people groups live, grouped by region, showing measured internet penetration","c":1,"em":{"iframe":"\u003ciframe src=\"./cities-internet/connectivity-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-yXvole+EYjOppLtZOqOW4pba/oWzbObcdkoSpYcnukHuARdg6R/qHzhOd5cpCJY0\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/cities-internet-map>"}},{"p":"connectivity-map","e":"cities-internet-map","u":"./cities-internet/connectivity-map/index.html","d":"Every country shaded by measured internet penetration, with its named carriers and dominant","c":1,"em":{"iframe":"\u003ciframe src=\"./cities-internet/connectivity-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-yXvole+EYjOppLtZOqOW4pba/oWzbObcdkoSpYcnukHuARdg6R/qHzhOd5cpCJY0\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/cities-internet-map>"}},{"p":"data-tables","e":"cities-internet-map","u":"./cities-internet/data-tables/index.html","d":"Four validated research tables, one tab each, with sortable and filterable columns, CSV and JSON","c":1,"em":{"iframe":"\u003ciframe src=\"./cities-internet/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-yXvole+EYjOppLtZOqOW4pba/oWzbObcdkoSpYcnukHuARdg6R/qHzhOd5cpCJY0\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/cities-internet-map>"}}]},{"b":"country-posters","s":"./country-posters/country-posters.js","i":"sha384-mk3L38FZ9gEiv/eRg89RXFkn0yzzOw3uSoxZ4NFr2UUEgVecyCe07hI0tgUQPMWY","e":["country-posters-map"],"p":[{"p":"countries-map","e":"country-posters-map","u":"./country-posters/countries-map/index.html","d":"The researcher's workbench retargeted from people groups to COUNTRIES — region","c":1,"em":{"iframe":"\u003ciframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-mk3L38FZ9gEiv/eRg89RXFkn0yzzOw3uSoxZ4NFr2UUEgVecyCe07hI0tgUQPMWY\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/country-posters-map>"}},{"p":"country-poster-wall","e":"country-posters-map","u":"./country-posters/country-poster-wall/index.html","d":"One printable card per country, grouped into COLLAPSIBLE regional sections so the","c":1,"em":{"iframe":"\u003ciframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-mk3L38FZ9gEiv/eRg89RXFkn0yzzOw3uSoxZ4NFr2UUEgVecyCe07hI0tgUQPMWY\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/country-posters-map>"}},{"p":"country-posters-mfe","e":"country-posters-mfe","u":"./country-posters/country-posters-mfe/index.html","d":"Country Posters as ONE two-tab element — the countries map and the printable","c":4,"em":{"iframe":"\u003ciframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-mk3L38FZ9gEiv/eRg89RXFkn0yzzOw3uSoxZ4NFr2UUEgVecyCe07hI0tgUQPMWY\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/country-posters-map>"}}]},{"b":"dashboard-plus-grid","s":"./dashboard-plus-grid/dashboard-plus-grid.js","i":"sha384-q5UVHN5QptVZvJHqrqIFgRpWCj2HRQGiTAO/JYxf5OHmg6XegLjaBMk1AZXWqHRG","e":["dashboard-plus-grid"],"p":[{"p":"dashboard-plus-grid","e":"dashboard-plus-grid","u":"./dashboard-plus-grid/dashboard-plus-grid/index.html","d":"Both children are IMPORTED DIRECTLY (compiled into this ONE bundle — no runtime bundle","c":4,"em":{"iframe":"\u003ciframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-q5UVHN5QptVZvJHqrqIFgRpWCj2HRQGiTAO/JYxf5OHmg6XegLjaBMk1AZXWqHRG\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/dashboard-plus-grid>"},"in":[{"id":"default","t":"default","a":"","pr":{"profile":"dashboard-plus-grid"},"e":"dashboard-plus-grid","u":"./dashboard-plus-grid/dashboard-plus-grid/default/index.html"}]}]},{"b":"demo-map","s":"./demo-map/demo-map.js","i":"sha384-bdvQUz/ahi3yvFWY4niHIt4hJ/2Thd5T/6KP/mobZrtEKchO1XclRQQcc35jt9Cd","e":["demo-map"],"p":[{"p":"demo-map","e":"demo-map","u":"./demo-map/demo-map/index.html","d":"Simple two-tab demo map. Tab 1 colors pins by religion (shared","c":2,"em":{"iframe":"\u003ciframe src=\"./demo-map/demo-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./demo-map/demo-map.js\" integrity=\"sha384-bdvQUz/ahi3yvFWY4niHIt4hJ/2Thd5T/6KP/mobZrtEKchO1XclRQQcc35jt9Cd\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdemo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/demo-map>"},"in":[{"id":"maplibre","t":"Demo map — MapLibre (token-free)","a":"","pr":{"profile":"demo-map","provider":"maplibre","instanceId":"demo-maplibre"},"e":"demo-map","u":"./demo-map/demo-map/maplibre/index.html"}]}]},{"b":"doxa-maps-spa","s":"./doxa-maps-spa/doxa-maps-spa.js","i":"sha384-1f+/rBSGo/1jyUmRmGg34CIU/PcGkB7M3jah1DO0ocb5O0zj81wpUemXcz4WAFz8","e":["doxa-maps-spa"],"p":[{"p":"doxa-maps-spa","e":"doxa-maps-spa","u":"./doxa-maps-spa/doxa-maps-spa/index.html","d":"The DX-maps app itself: one element whose vertical-tab rail nests every other","c":5,"em":{"iframe":"\u003ciframe src=\"./doxa-maps-spa/doxa-maps-spa/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-1f+/rBSGo/1jyUmRmGg34CIU/PcGkB7M3jah1DO0ocb5O0zj81wpUemXcz4WAFz8\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/doxa-maps-spa>"},"in":[{"id":"app","t":"DX-Maps SPA — Big Picture + every research map (app-shell)","a":"","pr":{"profile":"doxa-maps-spa","provider":"maplibre","instanceId":"doxa-maps-spa-app","title":"DX-Maps"},"e":"doxa-maps-spa","u":"./doxa-maps-spa/doxa-maps-spa/app/index.html"}]}]},{"b":"doxa-research-map","s":"./doxa-research-map/doxa-research-map.js","i":"sha384-CTfwl1rhm6JTKoUkSjawyQfHWYf55RBcwSDBYmVyQQ6f64Ag3v2dn5eV18yQSKwD","e":["doxa-research-map"],"p":[{"p":"research-map","e":"doxa-research-map","u":"./doxa-research-map/research-map/index.html","d":"The researcher's workbench: every piece a missiologist, demographer or","c":3,"em":{"iframe":"\u003ciframe src=\"./doxa-research-map/research-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-CTfwl1rhm6JTKoUkSjawyQfHWYf55RBcwSDBYmVyQQ6f64Ag3v2dn5eV18yQSKwD\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/doxa-research-map>"},"in":[{"id":"maplibre","t":"Research map — MapLibre (token-free, no search bar)","a":"","pr":{"profile":"research-map","provider":"maplibre","instanceId":"research-map-maplibre"},"e":"doxa-research-map","u":"./doxa-research-map/research-map/maplibre/index.html"}]}]},{"b":"doxa-simple-map","s":"./doxa-simple-map/doxa-simple-map.js","i":"sha384-edWUmiU1eWxE8Ju7FFg5PDfmw21E2NLoVaYmCgM8gXlm8S/9rowTJ7045NvdeIQA","e":["doxa-map","doxa-simple-map"],"p":[{"p":"doxa-simple-map","e":"doxa-map","u":"./doxa-simple-map/doxa-simple-map/index.html","d":"The simple world map, shipped as three presets off ONE prop table —","c":2,"em":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-edWUmiU1eWxE8Ju7FFg5PDfmw21E2NLoVaYmCgM8gXlm8S/9rowTJ7045NvdeIQA\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/doxa-map>"},"in":[{"id":"prayer","t":"Prayer Map","a":"https://pray.doxa.life","pr":{"profile":"doxa-simple-map","instanceId":"embed-prayer","dataSource":"pray-tools","tabs":[{"id":"prayer","colorStrategy":"prayer","legend":"prayer","popup":"prayer"}]},"e":"doxa-map","u":"./doxa-simple-map/doxa-simple-map/prayer/index.html"},{"id":"engagement","t":"Engagement Map","a":"https://pray.doxa.life","pr":{"profile":"doxa-simple-map","instanceId":"embed-engagement","dataSource":"pray-tools","tabs":[{"id":"engagement","colorStrategy":"engagement","legend":"engagement","popup":"engagement"}]},"e":"doxa-map","u":"./doxa-simple-map/doxa-simple-map/engagement/index.html"},{"id":"adoption","t":"Adoption Map","a":"https://pray.doxa.life","pr":{"profile":"doxa-simple-map","instanceId":"embed-adoption","dataSource":"pray-tools","tabs":[{"id":"adoption","colorStrategy":"adoption","legend":"adoption","popup":"adoption"}]},"e":"doxa-map","u":"./doxa-simple-map/doxa-simple-map/adoption/index.html"}]}]},{"b":"doxa-vision-card","s":"./doxa-vision-card/doxa-vision-card.js","i":"sha384-2mza3cltN5LQQIrlENG/IVL6Icd9zzcFve3E8R8VWuCdYOGW9uGKFvxcbu6eFfwn","e":["doxa-vision-card"],"p":[{"p":"doxa-vision-card","e":"doxa-vision-card","u":"./doxa-vision-card/doxa-vision-card/index.html","d":"The Capability-1 example: ONE folder -> ONE bundle -> ONE element.","c":2,"em":{"iframe":"\u003ciframe src=\"./doxa-vision-card/doxa-vision-card/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./doxa-vision-card/doxa-vision-card.js\" integrity=\"sha384-2mza3cltN5LQQIrlENG/IVL6Icd9zzcFve3E8R8VWuCdYOGW9uGKFvxcbu6eFfwn\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-vision-card profile-config='{\"profile\":\"doxa-vision-card\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/doxa-vision-card>"},"in":[{"id":"default","t":"Vision card — reach at a glance","a":"","pr":{"profile":"doxa-vision-card","provider":"maplibre","instanceId":"vision-card-default"},"e":"doxa-vision-card","u":"./doxa-vision-card/doxa-vision-card/default/index.html"},{"id":"frontier","t":"Vision card — frontier accent","a":"","pr":{"profile":"doxa-vision-card","provider":"maplibre","instanceId":"vision-card-frontier","accent":"#22d3ee","title":"The last frontier of the gospel.","tagline":"Frontier people groups — where fewer than one in a thousand follow Jesus.","stats":[{"value":"1,910","label":"Frontier groups","note":"under 0.1% believers"},{"value":"2.1B","label":"People","note":"living in frontier settings"},{"value":"0.1%","label":"Christian","note":"or fewer, in each group"}]},"e":"doxa-vision-card","u":"./doxa-vision-card/doxa-vision-card/frontier/index.html"}]}]},{"b":"golden-path-check","s":"./golden-path-check/golden-path-check.js","i":"sha384-1V/sT5SkV0U9yTCK9ELIOFfU1ZIJkInIF6nG3UllJ4dxax3RsEAupSGLJEFK6e14","e":["golden-path-check"],"p":[{"p":"golden-path-check","e":"golden-path-check","u":"./golden-path-check/golden-path-check/index.html","d":"A live self-test of the develop-a-plugin-and-see-it-live path: it reads the built","c":1,"em":{"iframe":"\u003ciframe src=\"./golden-path-check/golden-path-check/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./golden-path-check/golden-path-check.js\" integrity=\"sha384-1V/sT5SkV0U9yTCK9ELIOFfU1ZIJkInIF6nG3UllJ4dxax3RsEAupSGLJEFK6e14\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgolden-path-check profile-config='{\"profile\":\"golden-path-check\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/golden-path-check>"}}]},{"b":"gospel-resources","s":"./gospel-resources/gospel-resources.js","i":"sha384-uljHGYhda+DqMVTe8uCnYCfMNupgLezn47K71zOn+2H6hPaGpILlh/LtNU4wlqrY","e":["gospel-resources-map"],"p":[{"p":"charts","e":"gospel-resources-map","u":"./gospel-resources/charts/index.html","d":"The gap by LANGUAGE (ISO 639-3) rather than by people group — because one translation serves every","c":1,"em":{"iframe":"\u003ciframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-uljHGYhda+DqMVTe8uCnYCfMNupgLezn47K71zOn+2H6hPaGpILlh/LtNU4wlqrY\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/gospel-resources-map>"}},{"p":"data-tables","e":"gospel-resources-map","u":"./gospel-resources/data-tables/index.html","d":"Two validated tables: what resources exist per people group, and whether that access is keeping","c":1,"em":{"iframe":"\u003ciframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-uljHGYhda+DqMVTe8uCnYCfMNupgLezn47K71zOn+2H6hPaGpILlh/LtNU4wlqrY\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/gospel-resources-map>"}},{"p":"resources-list","e":"gospel-resources-map","u":"./gospel-resources/resources-list/index.html","d":"Which DX people groups have scripture in their own language, and which have none. The zero-resource","c":1,"em":{"iframe":"\u003ciframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-uljHGYhda+DqMVTe8uCnYCfMNupgLezn47K71zOn+2H6hPaGpILlh/LtNU4wlqrY\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/gospel-resources-map>"}},{"p":"resources-map","e":"gospel-resources-map","u":"./gospel-resources/resources-map/index.html","d":"Every country shaded by how much of its unengaged population has NO gospel resource in their own","c":1,"em":{"iframe":"\u003ciframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-uljHGYhda+DqMVTe8uCnYCfMNupgLezn47K71zOn+2H6hPaGpILlh/LtNU4wlqrY\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/gospel-resources-map>"}}]},{"b":"my-upg-100-list","s":"./my-upg-100-list/my-upg-100-list.js","i":"sha384-/g1IZXqStjHu34i4B12LQdGpK965qFY0vSThdph2gKrVqmtl4NTFdzh5IvJptHHV","e":["my-upg-100-list-map"],"p":[{"p":"poster-wall","e":"my-upg-100-list-map","u":"./my-upg-100-list/poster-wall/index.html","d":"A wall of printable poster cards, one per 100-list entry, each with a \"Show on map\" action that","c":1,"em":{"iframe":"\u003ciframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-/g1IZXqStjHu34i4B12LQdGpK965qFY0vSThdph2gKrVqmtl4NTFdzh5IvJptHHV\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/my-upg-100-list-map>"}},{"p":"upg-100-list","e":"my-upg-100-list-map","u":"./my-upg-100-list/upg-100-list/index.html","d":"A personal unreached-people-groups list painted as true boundary polygons —","c":1,"em":{"iframe":"\u003ciframe 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>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-/g1IZXqStjHu34i4B12LQdGpK965qFY0vSThdph2gKrVqmtl4NTFdzh5IvJptHHV\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/my-upg-100-list-map>"}},{"p":"upg-100-mfe","e":"my-upg-100-list-map","u":"./my-upg-100-list/upg-100-mfe/index.html","d":"The standalone, shareable and embeddable composite of the my-upg-100-list family — it nests the map","c":4,"em":{"iframe":"\u003ciframe 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>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-/g1IZXqStjHu34i4B12LQdGpK965qFY0vSThdph2gKrVqmtl4NTFdzh5IvJptHHV\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/my-upg-100-list-map>"}}]},{"b":"people-groups-grid","s":"./people-groups-grid/people-groups-grid.js","i":"sha384-ss5PmeSkKuxafBwvxZG3r7roP0OB/oVGwFbfrHnydEhjbtrlB4eisdsPXEyCJF37","e":["people-groups-grid"],"p":[{"p":"people-groups-grid","e":"people-groups-grid","u":"./people-groups-grid/people-groups-grid/index.html","d":"The people-groups grid as an owned custom element: card list, search and four","c":2,"em":{"iframe":"\u003ciframe src=\"./people-groups-grid/people-groups-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-ss5PmeSkKuxafBwvxZG3r7roP0OB/oVGwFbfrHnydEhjbtrlB4eisdsPXEyCJF37\" crossorigin=\"anonymous\">\u003c/script>\n\u003cpeople-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/people-groups-grid>"},"in":[{"id":"people-groups-grid","t":"People Groups Grid — standalone (own search + 4 dropdowns)","a":"","pr":{"profile":"people-groups-grid","provider":"maplibre","instanceId":"people-groups-grid","pray-base-url":"https://pray.doxa.life"},"e":"people-groups-grid","u":"./people-groups-grid/people-groups-grid/people-groups-grid/index.html"},{"id":"driven","t":"People Groups Grid — externally driven (controls off, props/events)","a":"","pr":{"profile":"people-groups-grid","provider":"maplibre","instanceId":"people-groups-grid-driven","pray-base-url":"https://pray.doxa.life","show-search":"false","show-filters":"false"},"e":"people-groups-grid","u":"./people-groups-grid/people-groups-grid/driven/index.html"}]}]},{"b":"plugin-studio","s":"./plugin-studio/plugin-studio.js","i":"sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH","e":["plugin-studio"],"p":[{"p":"plugin-studio","e":"plugin-studio","u":"./plugin-studio/plugin-studio/index.html","d":"The developer's viewing surface: a sidebar that is the composability ladder","c":5,"em":{"iframe":"\u003ciframe src=\"./plugin-studio/plugin-studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH\" crossorigin=\"anonymous\">\u003c/script>\n\u003cplugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/plugin-studio>"},"in":[{"id":"studio","t":"Plugin Studio — every element by capability 5 -> 1 (developer studio)","a":"","pr":{"profile":"plugin-studio","provider":"maplibre","instanceId":"plugin-studio","title":"Plugin Studio"},"e":"plugin-studio","u":"./plugin-studio/plugin-studio/studio/index.html"}]}]},{"b":"prayer-walk-planner","s":"./prayer-walk-planner/prayer-walk-planner.js","i":"sha384-F0+r4avX8xkXp0hBpU5EBPU0InMRCObIn2Ieh9HJNn1r2amk8/qHmJ68WuNfsirC","e":["prayer-walk-planner"],"p":[{"p":"prayer-walk-planner","e":"prayer-walk-planner","u":"./prayer-walk-planner/prayer-walk-planner/index.html","d":"A simple polygon-drawing tool for planning prayer-walking routes.","c":1,"em":{"iframe":"\u003ciframe src=\"./prayer-walk-planner/prayer-walk-planner/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-F0+r4avX8xkXp0hBpU5EBPU0InMRCObIn2Ieh9HJNn1r2amk8/qHmJ68WuNfsirC\" crossorigin=\"anonymous\">\u003c/script>\n\u003cprayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/prayer-walk-planner>"}}]},{"b":"regions-dashboard","s":"./regions-dashboard/regions-dashboard.js","i":"sha384-r0Xo6MLumWczZhDLa+U3zlxf4Wal/ZOW5zlgQiOC15aIH20NaY2RCYf1GyWKwRiG","e":["regions-dashboard"],"p":[{"p":"region-dashboards","e":"regions-dashboard","u":"./regions-dashboard/region-dashboards/index.html","d":"The dashboard half of the Driver's 2-tab Regions element: given a region (or a","c":1,"em":{"iframe":"\u003ciframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-r0Xo6MLumWczZhDLa+U3zlxf4Wal/ZOW5zlgQiOC15aIH20NaY2RCYf1GyWKwRiG\" crossorigin=\"anonymous\">\u003c/script>\n\u003cregions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/regions-dashboard>"}},{"p":"regions-list","e":"regions-dashboard","u":"./regions-dashboard/regions-list/index.html","d":"A refactor of DX-life's /regions index page (app/pages/regions/index.vue) into","c":1,"em":{"iframe":"\u003ciframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-r0Xo6MLumWczZhDLa+U3zlxf4Wal/ZOW5zlgQiOC15aIH20NaY2RCYf1GyWKwRiG\" crossorigin=\"anonymous\">\u003c/script>\n\u003cregions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/regions-dashboard>"}},{"p":"regions-mfe","e":"regions-dashboard","u":"./regions-dashboard/regions-mfe/index.html","d":"The composite the Driver asked for: ONE element carrying the SELECT VIEW switcher","c":4,"em":{"iframe":"\u003ciframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-r0Xo6MLumWczZhDLa+U3zlxf4Wal/ZOW5zlgQiOC15aIH20NaY2RCYf1GyWKwRiG\" crossorigin=\"anonymous\">\u003c/script>\n\u003cregions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/regions-dashboard>"}}]},{"b":"research-dashboard","s":"./research-dashboard/research-dashboard.js","i":"sha384-cMZfw0g3/v0hHkyHQhHYiv9GeD7hvFaUD0qF/G3FowqUNjgDykvI+btwOIu/p6Qy","e":["research-dashboard-map"],"p":[{"p":"research-dashboard","e":"research-dashboard-map","u":"./research-dashboard/research-dashboard/index.html","d":"Coordinated-Multiple-Views research dashboard for 10/40-window UPGs (live pray.doxa.life): docked facet rail + map-as-hero + linked table/charts, brushing-and-linking over one in-memory store.","c":3,"em":{"iframe":"\u003ciframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-cMZfw0g3/v0hHkyHQhHYiv9GeD7hvFaUD0qF/G3FowqUNjgDykvI+btwOIu/p6Qy\" crossorigin=\"anonymous\">\u003c/script>\n\u003cresearch-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/research-dashboard-map>"}}]},{"b":"research-map-plus-grid","s":"./research-map-plus-grid/research-map-plus-grid.js","i":"sha384-sOkaMV0UFr2uNXfoJpRvaD+gHD6xFlkmpR7MzEwwFdarYdid8aN9Nj1Y8TqQJuFo","e":["research-map-plus-grid"],"p":[{"p":"research-map-plus-grid","e":"research-map-plus-grid","u":"./research-map-plus-grid/research-map-plus-grid/index.html","d":"The monolithic 7-tab research map (cap-3) on top, the People Groups grid beneath,","c":4,"em":{"iframe":"\u003ciframe 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>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./research-map-plus-grid/research-map-plus-grid.js\" integrity=\"sha384-sOkaMV0UFr2uNXfoJpRvaD+gHD6xFlkmpR7MzEwwFdarYdid8aN9Nj1Y8TqQJuFo\" crossorigin=\"anonymous\">\u003c/script>\n\u003cresearch-map-plus-grid profile-config='{\"profile\":\"research-map-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/research-map-plus-grid>"},"in":[{"id":"default","t":"default","a":"","pr":{"profile":"research-map-plus-grid"},"e":"research-map-plus-grid","u":"./research-map-plus-grid/research-map-plus-grid/default/index.html"}]}]},{"b":"stack-demo","s":"./stack-demo/stack-demo.js","i":"sha384-gnwM6s+puf1K60Nl7crQcC2QC+9BxwpkaTx4OepLZ8VUCraZc+aRlWpsb6nj6Gyf","e":["stack-demo"],"p":[{"p":"stack-demo","e":"stack-demo","u":"./stack-demo/stack-demo/index.html","d":"Groups TWO independent elements TOP-TO-BOTTOM so you can embed the pair on a page and","c":4,"em":{"iframe":"\u003ciframe src=\"./stack-demo/stack-demo/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./stack-demo/stack-demo.js\" integrity=\"sha384-gnwM6s+puf1K60Nl7crQcC2QC+9BxwpkaTx4OepLZ8VUCraZc+aRlWpsb6nj6Gyf\" crossorigin=\"anonymous\">\u003c/script>\n\u003cstack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/stack-demo>"},"in":[{"id":"pair","t":"pair","a":"","pr":{"profile":"stack-demo"},"e":"stack-demo","u":"./stack-demo/stack-demo/pair/index.html"}]}]},{"b":"template-bundle","s":"./template-bundle/template-bundle.js","i":"sha384-4YT4Amt8yc0KANs5xlODSxpkWKYgWWCWd9B0CNiHCZebzvGbXp4fS+wYtPbqJJx7","e":["template-bundle"],"p":[{"p":"template-map-a","e":"template-bundle","u":"./template-bundle/template-map-a/index.html","d":"Minimal example profile A — a colored placeholder, no Mapbox or data.","c":1,"em":{"iframe":"\u003ciframe src=\"./template-bundle/template-map-a/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-4YT4Amt8yc0KANs5xlODSxpkWKYgWWCWd9B0CNiHCZebzvGbXp4fS+wYtPbqJJx7\" crossorigin=\"anonymous\">\u003c/script>\n\u003ctemplate-bundle profile-config='{\"profile\":\"template-map-a\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/template-bundle>"}},{"p":"template-map-b","e":"template-bundle","u":"./template-bundle/template-map-b/index.html","d":"Minimal example profile B — a second profile in the same bundle.","c":1,"em":{"iframe":"\u003ciframe src=\"./template-bundle/template-map-b/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-4YT4Amt8yc0KANs5xlODSxpkWKYgWWCWd9B0CNiHCZebzvGbXp4fS+wYtPbqJJx7\" crossorigin=\"anonymous\">\u003c/script>\n\u003ctemplate-bundle profile-config='{\"profile\":\"template-map-b\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/template-bundle>"}}]},{"b":"upg-neighbor","s":"./upg-neighbor/upg-neighbor.js","i":"sha384-JDJr4qoVZuiSi/rvgdzeuUNrzf7vjPdyh/g8dg7X8RHkZk/Y+tYXIQ3gpEzrU07q","e":["upg-neighbor"],"p":[{"p":"upg-neighbor","e":"upg-neighbor","u":"./upg-neighbor/upg-neighbor/index.html","d":"Rescues the five pre-bundler experimental maps that live inside the legacy","c":2,"em":{"iframe":"\u003ciframe src=\"./upg-neighbor/upg-neighbor/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./upg-neighbor/upg-neighbor.js\" integrity=\"sha384-JDJr4qoVZuiSi/rvgdzeuUNrzf7vjPdyh/g8dg7X8RHkZk/Y+tYXIQ3gpEzrU07q\" crossorigin=\"anonymous\">\u003c/script>\n\u003cupg-neighbor profile-config='{\"profile\":\"upg-neighbor\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/upg-neighbor>"},"in":[{"id":"send-map","t":"Send Map (legacy)","a":"","pr":{"profile":"upg-neighbor","legacyElement":"send-map","legacyTitle":"Send Map"},"e":"upg-neighbor","u":"./upg-neighbor/upg-neighbor/send-map/index.html"},{"id":"upg-neighbors","t":"UPG Neighbors (legacy)","a":"","pr":{"profile":"upg-neighbor","legacyElement":"upg-neighbors","legacyTitle":"UPG Neighbors"},"e":"upg-neighbor","u":"./upg-neighbor/upg-neighbor/upg-neighbors/index.html"},{"id":"lang-cluster","t":"Language Clusters (legacy)","a":"","pr":{"profile":"upg-neighbor","legacyElement":"lang-cluster","legacyTitle":"Language Clusters"},"e":"upg-neighbor","u":"./upg-neighbor/upg-neighbor/lang-cluster/index.html"},{"id":"bloom-cockpit","t":"Bloom Cockpit (legacy)","a":"","pr":{"profile":"upg-neighbor","legacyElement":"bloom-cockpit","legacyTitle":"Bloom Cockpit"},"e":"upg-neighbor","u":"./upg-neighbor/upg-neighbor/bloom-cockpit/index.html"},{"id":"flower-mesh","t":"Flower Mesh (legacy)","a":"","pr":{"profile":"upg-neighbor","legacyElement":"flower-mesh","legacyTitle":"Flower Mesh"},"e":"upg-neighbor","u":"./upg-neighbor/upg-neighbor/flower-mesh/index.html"}]}]}];
|
|
var snip = function snip(b, p, x) {
|
|
var u = (x || p).u, tag = (x && x.e) || p.e || b.e[0] || b.b
|
|
var r = { iframe: '<iframe src="' + u + '?utm_source=YOUR_SOURCE" width="100%" height="600" style="border:none;border-radius:12px;" allowfullscreen></iframe>' }
|
|
if (!x) r.script = '<script src="' + b.s + '"' + (b.i ? ' integrity="' + b.i + '" crossorigin="anonymous"' : '') + '><\/script>\n<' + tag + " profile-config='" + JSON.stringify({ profile: p.p, tk: 'YOUR_MAPBOX_TOKEN', utmSource: 'YOUR_SOURCE' }) + "'></" + tag + '>'
|
|
return r
|
|
};
|
|
var compact = function compact(m, lean, tierMap) {
|
|
tierMap = tierMap || {}
|
|
// Composition tier → capability number, self-contained so the browser copy of this function
|
|
// (shipped via src(compact) and re-run on the live manifest) needs no import. 5 = app-shell
|
|
// (nested-mfe-spa) … 1 = single. The model may bake `sp.type`; else tierMap resolves it.
|
|
var PT = { 'nested-mfe-spa': 0, 'nested-elements-mfe': 1, 'monolithic': 2, 'parameterized': 3, 'single': 4 }
|
|
var capFor = function (t) { return PT[t] != null ? 5 - PT[t] : 1 }
|
|
return (m.bundles || []).map(function (src) {
|
|
var b = { b: src.bundle, s: src.scriptUrl || '', i: src.integrity || '', e: src.elements || [], p: [] }
|
|
b.p = (src.profiles || []).map(function (sp) {
|
|
var type = sp.type || tierMap[src.bundle + '/' + sp.profile] || 'single'
|
|
var c = capFor(type)
|
|
var p = { p: sp.profile, e: sp.element || '', u: sp.iframeUrl || '', d: sp.description || '', c: c }
|
|
var em = sp.embed || {}, d = snip(b, p, null)
|
|
if (!lean || em.iframe !== d.iframe || em.script !== d.script) p.em = em
|
|
var ins = (sp.instances || []).map(function (sx) {
|
|
var x = { id: sx.id, t: sx.title || sx.id, a: sx.apiBaseUrl || '', pr: sx.props || {}, e: sx.element || '', u: sx.iframeUrl || '' }
|
|
var xe = sx.embed || {}, xd = snip(b, p, x)
|
|
if (!lean || xe.iframe !== xd.iframe) x.em = xe
|
|
return x
|
|
})
|
|
if (ins.length) p.in = ins
|
|
return p
|
|
})
|
|
return b
|
|
})
|
|
};
|
|
// Peer engines, the same tags every leaf page carries; loaded once, before the bundle.
|
|
var PEERS = {
|
|
mapbox: { css: ['https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css', 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css'],
|
|
js: ['https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js', 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js'] },
|
|
maplibre: { css: ['https://cdn.jsdelivr.net/npm/maplibre-gl@5/dist/maplibre-gl.css'], js: ['https://cdn.jsdelivr.net/npm/maplibre-gl@5/dist/maplibre-gl.js'] }
|
|
};
|
|
var $ = function (id) { return document.getElementById(id) };
|
|
// The map custom elements mount imperatively into this slot (they own their own DOM and must
|
|
// not be re-rendered by Vue); the Vue app writes its ref here after mount. Falls back to the
|
|
// element id so the loader works even before the app boots.
|
|
var slotEl = function () { return (window.__spa && window.__spa.slot) || $('slot') };
|
|
var TIER_MAP = {"all-people-groups/all-people-groups":"single","big-picture/big-picture":"nested-elements-mfe","cities-internet/connectivity-list":"single","cities-internet/connectivity-map":"single","cities-internet/data-tables":"single","country-posters/countries-map":"single","country-posters/country-poster-wall":"single","country-posters/country-posters-mfe":"nested-elements-mfe","dashboard-plus-grid/dashboard-plus-grid":"nested-elements-mfe","demo-map/demo-map":"parameterized","doxa-maps-spa/doxa-maps-spa":"nested-mfe-spa","doxa-research-map/research-map":"monolithic","doxa-simple-map/doxa-simple-map":"parameterized","doxa-vision-card/doxa-vision-card":"parameterized","golden-path-check/golden-path-check":"single","gospel-resources/charts":"single","gospel-resources/data-tables":"single","gospel-resources/resources-list":"single","gospel-resources/resources-map":"single","key-cities/cities-list":"single","key-cities/cities-map":"single","key-cities/city-poster-wall":"single","key-cities/data-tables":"single","my-upg-100-list/poster-wall":"single","my-upg-100-list/upg-100-list":"single","my-upg-100-list/upg-100-mfe":"nested-elements-mfe","people-groups-grid/people-groups-grid":"parameterized","plugin-studio/plugin-studio":"nested-mfe-spa","prayer-walk-planner/prayer-walk-planner":"single","regions-dashboard/region-dashboards":"single","regions-dashboard/regions-list":"single","regions-dashboard/regions-mfe":"nested-elements-mfe","research-dashboard/research-dashboard":"monolithic","research-map-plus-grid/research-map-plus-grid":"nested-elements-mfe","stack-demo/stack-demo":"nested-elements-mfe","template-bundle/template-map-a":"single","template-bundle/template-map-b":"single","upg-neighbor/upg-neighbor":"parameterized"}; // "<bundle>/<profile>" → composition tier, for live re-compact
|
|
// THIS PAGE'S OWN DIRECTORY. A host may serve the folder at /…/doxa-maps-build (no trailing
|
|
// slash); the browser would then resolve every relative ref against the PARENT directory and
|
|
// drop the folder name, so bundles 404. Derive the directory explicitly and build refs from it.
|
|
var BASE = (function () {
|
|
var p = location.pathname || '/';
|
|
return /\.[a-z0-9]+$/i.test(p) ? p.replace(/[^/]*$/, '') : p.replace(/\/*$/, '') + '/';
|
|
})();
|
|
// ★ THE TREE ROOT AS ONE ABSOLUTE URL — derive it ONCE, never re-join TREE by hand.
|
|
// TREE is PAGE-RELATIVE and its value differs per surface ('../doxa-maps' in the built tree,
|
|
// '__plugin-gallery' in dev), so a bare TREE + '/x' join is only accidentally right. It was right
|
|
// in the built tree (where the '../' climbs out of /__gallery/) and WRONG in dev, where it asked
|
|
// for /__gallery/__plugin-gallery/manifest.json. MEASURED 2026-09-22, both servers running:
|
|
// dev /__gallery/__plugin-gallery/manifest.json ... 404 (what the page asked for)
|
|
// dev /__plugin-gallery/manifest.json ............. 200 (what it meant)
|
|
// built /doxa-maps/manifest.json .................... 200
|
|
// rel() never had the bug because it always anchored to BASE. Resolving through the URL parser
|
|
// (rather than string-joining) also collapses the '../' properly for BOTH surfaces at once.
|
|
// Resolve TREE *against* the page's own directory — do NOT string-join it onto BASE. TREE may be
|
|
// ROOTED ('/__plugin-gallery' in dev), and 'BASE + TREE' would glue it on to yield
|
|
// /__gallery//__plugin-gallery, which 404s just as surely as the original bug. Passing BASE as
|
|
// the base URL lets the URL parser apply the right rule per shape: a rooted TREE replaces the
|
|
// path, a '../' TREE climbs out of /__gallery/, and a bare name resolves beside it.
|
|
var TREE_URL = new URL(TREE, new URL(BASE, location.href)).href.replace(/\/+$/, '');
|
|
// ASSETS live INSIDE the tree (public/<tree>/assets), which is what every leaf page points at:
|
|
// a leaf at /<tree>/<bundle>/<profile>/ bakes assetsBase "../../assets" -> /<tree>/assets. So
|
|
// anchor to TREE_URL, not to this PAGE's directory.
|
|
// This was BASE + 'assets' — correct only while the gallery WAS the root page, where the two
|
|
// coincide. Since the gallery moved to /__gallery it asked for /__gallery/assets/... MEASURED
|
|
// on the built tree with the page at /__gallery/:
|
|
// /__gallery/assets/vendor/vue/vue.global.prod.js ... 404 (what the page asked for)
|
|
// /doxa-maps/assets/vendor/vue/vue.global.prod.js ... 200 (where it lives)
|
|
// It degraded quietly rather than blanking — loadVue() falls back to the jsDelivr CDN — so the
|
|
// page still worked while silently losing the portable/offline/no-CDN property the vendored
|
|
// copy exists to provide. The dev server passes assetsBaseUrl explicitly, so this is the
|
|
// built-tree half of the same fix its comment already documents.
|
|
var ASSETS = ASSETS_BASE || TREE_URL + '/assets';
|
|
var REG = BAKED, active = null, seq = 0, loaded = {}, fetchedToken = '';
|
|
var narrow = function () { return window.matchMedia && window.matchMedia('(max-width:700px)').matches };
|
|
|
|
// Manifest refs are relative to the tree root; this page sits one level above it. Refs are
|
|
// anchored to BASE (above) so they resolve the same with or without a trailing slash.
|
|
function rel(u) { return /^([a-z]+:|\/)/i.test(u) ? u : BASE + TREE + '/' + u.replace(/^\.\//, '') }
|
|
function qp(name) { try { return new URLSearchParams(location.search).get(name) || '' } catch (e) { return '' } }
|
|
function hint(msg, err) { var slot = slotEl(); var d = document.createElement('div'); d.className = 'hint' + (err ? ' err' : ''); d.textContent = msg; slot.innerHTML = ''; slot.appendChild(d); return d }
|
|
function assign(t) { for (var i = 1; i < arguments.length; i++) { var s = arguments[i]; for (var k in s) if (Object.prototype.hasOwnProperty.call(s, k)) t[k] = s[k] } return t }
|
|
|
|
function once(url, kind, attrs) {
|
|
if (loaded[url]) return loaded[url];
|
|
loaded[url] = new Promise(function (ok, fail) {
|
|
var el = document.createElement(kind === 'css' ? 'link' : 'script');
|
|
if (kind === 'css') { el.rel = 'stylesheet'; el.href = url } else { el.src = url; assign(el, attrs || {}) }
|
|
el.onload = function () { ok() }; el.onerror = function () { delete loaded[url]; fail(new Error('failed to load ' + url)) };
|
|
document.head.appendChild(el);
|
|
});
|
|
return loaded[url];
|
|
}
|
|
function peer(provider) {
|
|
var p = PEERS[provider] || PEERS.mapbox, chain = Promise.resolve();
|
|
p.css.forEach(function (u) { once(u, 'css') });
|
|
p.js.forEach(function (u) { chain = chain.then(function () { return once(u, 'js') }) });
|
|
return chain;
|
|
}
|
|
function bundle(b) { return once(rel(b.s), 'js', b.i ? { integrity: b.i, crossOrigin: 'anonymous' } : {}) }
|
|
|
|
// Resolve a deep-link key ("<bundle>/<profile>[/<instance>]") against the live registry.
|
|
function find(parts) {
|
|
for (var i = 0; i < REG.length; i++) if (REG[i].b === parts[0]) {
|
|
var b = REG[i];
|
|
for (var j = 0; j < b.p.length; j++) if (b.p[j].p === parts[1]) {
|
|
var p = b.p[j];
|
|
if (parts.length < 3) return { b: b, p: p, x: null };
|
|
for (var k = 0; k < (p.in || []).length; k++) if (p.in[k].id === parts[2]) return { b: b, p: p, x: p.in[k] };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Same profile-config the leaf page bakes (profile page: profile + dataSource + instanceId;
|
|
// instance page: its props), plus the host-supplied share URL of that leaf page.
|
|
function mount(b, p, x, tag) {
|
|
var cfg = x ? assign({ assetsBase: ASSETS }, x.pr) : { profile: p.p, dataSource: 'pray-tools', instanceId: b.b + '-' + p.p, assetsBase: ASSETS };
|
|
if (QA) cfg.qa = true;
|
|
cfg.shareUrl = new URL(rel((x || p).u), location.href).href;
|
|
// ★ HAND THE TREE ROOT DOWN — an element that fetches the manifest cannot guess it from here.
|
|
// A cap-5 shell derives its tree root from location.pathname when nothing is passed, which is
|
|
// right on its own <tree>/<bundle>/<profile>/ leaf page and WRONG in this gallery, where the
|
|
// path belongs to the PAGE, not the element. doxa-maps-spa's relative fallback is '../', so
|
|
// from /__gallery/ it asked for /manifest.json. MEASURED by the smoke:gallery gate:
|
|
// ✗ doxa-maps-spa/doxa-maps-spa data: 404 http://localhost:5173/manifest.json
|
|
// The SPA's OWN nested-mount path already sets both of these for exactly this reason; this
|
|
// gallery was the one mount path that never learned the lesson. Set both, because not every
|
|
// element reads profile-config (an element mounting its own SFC has no ProfileLoader above it
|
|
// and takes tree-base as a prop); an element that declares neither ignores both.
|
|
cfg.treeBase = TREE_URL;
|
|
window.MAP_APP_API_URL = (x && x.a) || API_BASE;
|
|
var provider = cfg.provider === 'maplibre' ? 'maplibre' : 'mapbox', my = ++seq;
|
|
hint(provider === 'maplibre' ? 'loading map…' : 'loading token…');
|
|
peer(provider).then(function () { return bundle(b) }).then(function () {
|
|
if (my !== seq) return;
|
|
resolveToken(cfg, provider === 'maplibre', function (token) {
|
|
if (my !== seq) return;
|
|
try {
|
|
var c = token ? assign({}, cfg, { tk: token }) : cfg;
|
|
var el = document.createElement(tag); el.setAttribute('profile-config', JSON.stringify(c));
|
|
el.setAttribute('tree-base', TREE_URL); // see cfg.treeBase above — both, for both kinds of element
|
|
var slot = slotEl(); slot.innerHTML = ''; slot.appendChild(el);
|
|
} catch (e) { hint(e && e.message ? e.message : String(e), true) }
|
|
});
|
|
}).catch(function (e) { if (my === seq) hint('Failed to load ' + b.b + ': ' + (e && e.message ? e.message : e), true) });
|
|
}
|
|
|
|
// Mapbox token resolution — the leaf pages' ladder, in precedence order:
|
|
// 1. prop (config.tk) 2. ?tk= on this page's URL (stored once for every page of the tree)
|
|
// 3. baked token — then, none found, a token ENDPOINT is fetched, first answer wins:
|
|
// 4. window.MAP_TOKEN_URL 5. configured TOKEN_SRC 6. same-origin /api/maps/token — else ask once.
|
|
function resolveToken(cfg, tokenFree, done) {
|
|
if (tokenFree) return done('');
|
|
var stored = ''; try { stored = localStorage.getItem('MAPS_STAGING_TK') || '' } catch (e) {}
|
|
var urlTk = qp('tk'); if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk) } catch (e) {} }
|
|
var directToken = cfg.tk || urlTk || stored || BAKED_TOKEN || fetchedToken;
|
|
if (directToken) return done(directToken);
|
|
function askToken(prefix) {
|
|
var d = hint('', true);
|
|
d.innerHTML = (prefix ? '<div></div>' : '') + '<div>map needs a Mapbox token — <input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)"> <button id="tkGo" type="button">use it</button></div>';
|
|
if (prefix) d.firstChild.textContent = prefix;
|
|
$('tkGo').onclick = function () { var v = ($('tkIn').value || '').trim(); if (!v) return; try { localStorage.setItem('MAPS_STAGING_TK', v) } catch (e) {} done(v) };
|
|
}
|
|
var candidates = [];
|
|
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u) }
|
|
addCandidate(window.MAP_TOKEN_URL);
|
|
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
|
|
addCandidate(DEFAULT_TOKEN_URL);
|
|
function extractToken(data) { if (typeof data === 'string') return data.trim(); if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || ''; return '' }
|
|
function fetchToken(i, errors) {
|
|
if (i >= candidates.length) return askToken('Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')');
|
|
var url = candidates[i];
|
|
return fetch(url).then(function (r) {
|
|
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
|
|
return (r.headers.get('content-type') || '').indexOf('json') !== -1 ? r.json() : r.text();
|
|
}).then(function (data) {
|
|
var tk = extractToken(data); if (!tk) throw new Error(url + ' → no token in response');
|
|
fetchedToken = tk; done(tk);
|
|
}).catch(function (e) { errors.push(e && e.message ? e.message : String(e)); return fetchToken(i + 1, errors) });
|
|
}
|
|
fetchToken(0, []);
|
|
}
|
|
|
|
function copyText(text, btn) {
|
|
var ok = function () { var t = btn.textContent; btn.textContent = 'Copied'; setTimeout(function () { btn.textContent = t }, 1200) };
|
|
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(ok, function () {}); else {
|
|
try { var ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); ok() } catch (e) {}
|
|
}
|
|
}
|
|
|
|
// ── The capability ladder for the sidebar headings (5 → 1), reader-facing names ──
|
|
var CAP_LABEL = { 5: 'App shell · nested-mfe-spa', 4: 'Composite element', 3: 'Multi-view element', 2: 'Preset element', 1: 'Single element' };
|
|
// REG → tab groups keyed by capability, most-composed first (5 → 1). Each group lists every
|
|
// profile (and its preset instances) as one row; a bundle contributes rows to the tier of each
|
|
// profile it holds, so one bundle can appear under several capability headings.
|
|
function toGroups() {
|
|
var byCap = {};
|
|
REG.forEach(function (b) {
|
|
(b.p || []).forEach(function (p) {
|
|
var cap = p.c || 1, tag = p.e || b.e[0] || b.b;
|
|
(byCap[cap] = byCap[cap] || []).push({ key: b.b + '/' + p.p, kind: 'profile', label: p.p, tag: tag, bundle: b.b });
|
|
(p.in || []).forEach(function (x) {
|
|
byCap[cap].push({ key: b.b + '/' + p.p + '/' + x.id, kind: 'instance', label: x.t, tag: x.e || tag, bundle: b.b });
|
|
});
|
|
});
|
|
});
|
|
return [5, 4, 3, 2, 1].filter(function (c) { return byCap[c] && byCap[c].length }).map(function (c) {
|
|
return { cap: c, label: CAP_LABEL[c] || ('capability ' + c), rows: byCap[c] };
|
|
});
|
|
}
|
|
|
|
function bootVue(Vue) {
|
|
var app = Vue.createApp({
|
|
data: function () {
|
|
return {
|
|
groups: toGroups(), activeKey: '', navOpen: true, embedOpen: false,
|
|
ttl: 'Choose a map', sub: '', openHref: '', hasSel: false, embedIframe: '', embedScript: ''
|
|
};
|
|
},
|
|
computed: {
|
|
countLabel: function () {
|
|
var nb = REG.length, np = 0, ni = 0;
|
|
REG.forEach(function (b) { np += (b.p || []).length; (b.p || []).forEach(function (p) { ni += (p.in || []).length }) });
|
|
return nb + ' bundles · ' + np + ' profiles · ' + ni + ' instances';
|
|
}
|
|
},
|
|
methods: {
|
|
go: function (key) { location.hash = '#/' + key.split('/').map(encodeURIComponent).join('/') },
|
|
copyText: copyText,
|
|
rebuild: function (m) { if (m) { try { REG = compact(m, false, TIER_MAP) } catch (e) { REG = BAKED } } this.groups = toGroups(); this.route(); },
|
|
route: function () { var h = (location.hash || '').replace(/^#\/?/, ''); if (h) this.select(h); },
|
|
select: function (key) {
|
|
var parts = key.split('/').map(function (s) { try { return decodeURIComponent(s) } catch (e) { return s } });
|
|
var hit = find(parts); this.activeKey = parts.join('/');
|
|
if (!hit) { this.ttl = 'No such map'; this.sub = key; this.openHref = ''; this.hasSel = false; this.embedOpen = false; hint('No such map: ' + key, true); return; }
|
|
var b = hit.b, p = hit.p, x = hit.x, tag = (x && x.e) || p.e || b.e[0] || b.b;
|
|
this.ttl = x ? x.t : p.p; this.sub = b.b + ' · <' + tag + '>' + (p.d ? ' · ' + p.d : '');
|
|
this.openHref = rel((x || p).u); this.hasSel = true; this.embedOpen = false;
|
|
var em = x ? (x.em || snip(b, p, x)) : (p.em || snip(b, p, null));
|
|
this.embedIframe = em.iframe || ''; this.embedScript = em.script || '';
|
|
if (narrow()) this.navOpen = false;
|
|
mount(b, p, x, tag);
|
|
}
|
|
},
|
|
mounted: function () {
|
|
var self = this;
|
|
window.__spa = { slot: this.$refs.slot, app: this };
|
|
window.addEventListener('hashchange', function () { self.route() });
|
|
try { window.matchMedia('(max-width:700px)').addEventListener('change', function (e) { if (!e.matches) self.navOpen = true }) } catch (e) {}
|
|
// Live manifest first (the page stays right when bundles change); baked copy is the fallback.
|
|
if (typeof fetch === 'function' && location.protocol !== 'file:') {
|
|
fetch(TREE_URL + '/manifest.json', { cache: 'no-cache' }).then(function (r) { return r.ok ? r.json() : null })
|
|
.then(function (m) { self.rebuild(m) }, function () { self.route() });
|
|
} else { self.route(); }
|
|
}
|
|
});
|
|
app.config.compilerOptions.isCustomElement = function () { return true }; // map tags are custom elements
|
|
app.mount('#app');
|
|
}
|
|
|
|
// Vue drives the shell (the nested-mfe-spa). Self-hosted first (portable / offline / no-CDN),
|
|
// then a CDN fallback; if neither loads, degrade to a plain rendered list so the page never
|
|
// blanks. once() resolves the tree-relative ASSETS path the same way every leaf page does.
|
|
function loadVue() {
|
|
if (window.Vue) return Promise.resolve(window.Vue);
|
|
var local = ASSETS + '/vendor/vue/vue.global.prod.js';
|
|
var cdn = 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js';
|
|
return once(local, 'js').then(function () { return window.Vue }, function () {
|
|
return once(cdn, 'js').then(function () { return window.Vue });
|
|
});
|
|
}
|
|
loadVue().then(function (Vue) {
|
|
if (Vue) bootVue(Vue);
|
|
else fallbackRender();
|
|
}).catch(function () { fallbackRender(); });
|
|
|
|
// No-Vue degrade: render the same capability-sorted list with plain DOM so a map is still
|
|
// reachable (parse-safe, dependency-free). Rare — Vue is vendored in the tree.
|
|
function fallbackRender() {
|
|
var host = $('app'); if (!host) return;
|
|
function paint() {
|
|
var nav = host.querySelector('#nav'); if (!nav) return;
|
|
nav.innerHTML = '';
|
|
toGroups().forEach(function (g) {
|
|
var h = document.createElement('div'); h.className = 'tier'; h.textContent = 'CAP ' + g.cap + ' · ' + g.label; nav.appendChild(h);
|
|
g.rows.forEach(function (r) {
|
|
var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'row' + (r.kind === 'instance' ? ' in' : '');
|
|
btn.textContent = r.label; btn.onclick = function () { location.hash = '#/' + r.key.split('/').map(encodeURIComponent).join('/') };
|
|
nav.appendChild(btn);
|
|
});
|
|
});
|
|
}
|
|
function route() {
|
|
var h = (location.hash || '').replace(/^#\/?/, ''); if (!h) return;
|
|
var parts = h.split('/').map(function (s) { try { return decodeURIComponent(s) } catch (e) { return s } });
|
|
var hit = find(parts); if (hit) mount(hit.b, hit.p, hit.x, (hit.x && hit.x.e) || hit.p.e || hit.b.e[0] || hit.b.b); else hint('No such map: ' + h, true);
|
|
}
|
|
window.__spa = { slot: $('slot') };
|
|
if (typeof fetch === 'function' && location.protocol !== 'file:') {
|
|
fetch(TREE_URL + '/manifest.json', { cache: 'no-cache' }).then(function (r) { return r.ok ? r.json() : null })
|
|
.then(function (m) { if (m) { try { REG = compact(m, false, TIER_MAP) } catch (e) { REG = BAKED } } paint(); route(); }, function () { paint(); route(); });
|
|
} else { paint(); route(); }
|
|
window.addEventListener('hashchange', route);
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|