Take main

# 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-25 09:30:10 +00:00
commit 52ab608925
132 changed files with 2479 additions and 663 deletions

109
README.md
View file

@ -24,6 +24,72 @@ capabilities, five deployment methods, one shared library, one build.
--- ---
## The 7 production maps
**Seven maps DX uses today** — each one a plugin you can embed on its own, and all seven together in
the Research Gallery.
<table>
<tr>
<td width="25%" valign="top"><img src="documentation/readme-images/prod-big-picture.png" alt="Big Picture"><br><b>Big Picture</b><br><sub>the whole research dashboard: filters, map, charts, people groups</sub></td>
<td width="25%" valign="top"><img src="documentation/readme-images/prod-simple-research.png" alt="Simple Research"><br><b>Simple Research</b><br><sub>the research map with the people-groups grid, made for phones</sub></td>
<td width="25%" valign="top"><img src="documentation/readme-images/prod-regions.png" alt="Regions"><br><b>Regions</b><br><sub>every region and its countries, then a dashboard per region</sub></td>
<td width="25%" valign="top"><img src="documentation/readme-images/cap4-upg100-mfe.png" alt="UPG-100 List"><br><b>UPG-100 List</b><br><sub>prayer lists of 100 people groups, with posters to print</sub></td>
</tr>
<tr>
<td width="25%" valign="top"><img src="documentation/readme-images/cap2-prayer.png" alt="Prayer Progress"><br><b>Prayer Progress</b><br><sub>where prayer is happening, and where it is still needed</sub></td>
<td width="25%" valign="top"><img src="documentation/readme-images/prod-cities-internet.png" alt="Cities with Internet"><br><b>Cities with Internet</b><br><sub>internet access where unreached people groups live</sub></td>
<td width="25%" valign="top"><img src="documentation/readme-images/prod-gospel-resources.png" alt="Gospel Resources"><br><b>Gospel Resources</b><br><sub>which people groups have scripture, film or audio — and which have nothing</sub></td>
<td width="25%" valign="top"><img src="documentation/readme-images/cap5-doxa-maps-spa.png" alt="Research Gallery"><br><b>Research Gallery</b><br><sub>all seven in one app — capability 5</sub></td>
</tr>
</table>
### On a phone
<table>
<tr>
<td width="20%" valign="top"><img src="documentation/readme-images/prod-regions-phone.png" alt="Regions on a phone"><br><b>Regions</b></td>
<td width="20%" valign="top"><img src="documentation/readme-images/deploy2-mobile-upg100.png" alt="UPG-100 List on a phone"><br><b>UPG-100 List</b></td>
<td width="20%" valign="top"><img src="documentation/readme-images/prod-prayer-phone.png" alt="Prayer Progress on a phone"><br><b>Prayer Progress</b></td>
<td width="20%" valign="top"><img src="documentation/readme-images/prod-cities-internet-phone.png" alt="Cities with Internet on a phone"><br><b>Cities with Internet</b></td>
<td width="20%" valign="top"><img src="documentation/readme-images/prod-gospel-resources-phone.png" alt="Gospel Resources on a phone"><br><b>Gospel Resources</b></td>
</tr>
</table>
<sub>Big Picture is a desktop dashboard by design — on phones, Simple Research is the dashboard. Simple Research's phone layout is still being finished.</sub>
### Every capability ships every way
| Capability | Example | iframe | Mobile app | JavaScript embed | WordPress / Disciple Tools | Desktop |
|---|---|:-:|:-:|:-:|:-:|:-:|
| **5 — app shell** | Research Gallery | ✅ | ✅ | ✅ | 🧪 | 🧪 |
| **4 — composite** | UPG-100 List | ✅ | ✅ | ✅ | 🧪 | 🧪 |
| **3 — multi-view** | research map | ✅ | ✅ | ✅ | 🧪 | 🧪 |
| **2 — presets** | Prayer Progress | ✅ | ✅ | ✅ | 🧪 | 🧪 |
| **1 — single element** | all people groups | ✅ | ✅ | ✅ | 🧪 | 🧪 |
<sub>✅ works today · 🧪 proven, not yet in production. Every plugin builds into the same folder, and that folder is what all five methods ship — so a capability never limits where a plugin can go.</sub>
<details>
<summary><b>How one build becomes five ways to ship — the "map cruncher"</b></summary>
```text
map · map · map · map · map · map · map many maps, each once its own codebase
\ \ | | | / /
+----- plugin-designs/ ------+ one folder per plugin — the free zone
|
library/ (@map) shared, reusable pieces — the high bar
|
ONE build step
|
public/doxa-maps/ one drop-anywhere folder + manifest.json
/ | \ \ iframe mobile JS embed WordPress desktop
```
</details>
---
## The 5 plugin capabilities ## The 5 plugin capabilities
A capability is **how much a plugin nests**, from one element (1) to an app of apps (5). Every A capability is **how much a plugin nests**, from one element (1) to an app of apps (5). Every
@ -429,14 +495,45 @@ The studio is a plugin like any other: `plugin-designs/plugin-studio/plugin-stud
## Repository layout ## Repository layout
```text ```text
plugin-designs/ YOUR maps — one folder per plugin, a free zone doxa-maps/
library/ the SHARED toolkit (layouts, components, composables) — a high bar ├── plugin-designs/ YOUR maps — one folder per plugin (the free zone)
public/doxa-maps/ the build output: the drop-anywhere folder + manifest.json │ ├── doxa-maps-spa/ the Research Gallery (capability 5)
deployment/ mobile (Capacitor) · WordPress · desktop (Tauri) │ ├── plugin-studio/ the Plugin Studio (capability 5)
internal/ the build and its checks │ ├── my-upg-100-list/ UPG-100 List (capability 4)
documentation/ tutorial · how-to · reference · architecture — start at documentation/README.md │ ├── doxa-simple-map/ Prayer Progress and its presets (capability 2)
│ ├── all-people-groups/ a single map (capability 1)
│ └── template-bundle/ copy this to start a new plugin
│ ├── template-map-a.vue the map itself
│ ├── index.js registers its HTML tag
│ ├── index.html a preview page
│ └── src/ private pieces only this plugin uses
├── library/ the SHARED toolkit, imported as @map (the high bar)
│ ├── layouts/ VerticalTabsLayout, StackLayout — how composites compose
│ ├── components/ legends, map controls, search, posters, detail panels
│ ├── composables/ map instance, data, layers, shared helpers
│ ├── colors/ styles/ colour strategies and the house style
│ ├── i18n/locales/ the 11 languages
│ └── data/ api/ lookup tables and data sources
├── public/doxa-maps/ the BUILD OUTPUT — the drop-anywhere folder + manifest.json
├── deployment/ mobile (Capacitor) · WordPress · desktop (Tauri)
├── internal/ the build and its checks
└── documentation/ tutorial · how-to · reference · architecture
``` ```
<details>
<summary><b>Where your code goes — a plugin, or the shared library?</b></summary>
- **Write your app in `plugin-designs/<your-map>/`.** On the build step it becomes its own plugin in
the output folder — its own embeddable app. Nothing you do there can break anyone else's map.
- **Only put something in `library/` if it is genuinely reusable *and* parameterized** — built so
other maps can use it with their own settings. The library is shared by every map, so every edit
there can break every map: it is a high bar. Keep anything specific to one map inside that map's
own folder.
- The full contribution guide: [documentation/CONTRIBUTING.md](./documentation/CONTRIBUTING.md) ·
every reusable piece you can import: [documentation/reference-library-index.md](./documentation/reference-library-index.md).
</details>
> **Maps should be open source, run everywhere, and multiply.** One build makes each map a > **Maps should be open source, run everywhere, and multiply.** One build makes each map a
> self-contained file for your site, your partners' sites and a phone. The Plugin Studio is where > self-contained file for your site, your partners' sites and a phone. The Plugin Studio is where
> you compose them into something bigger. > you compose them into something bigger.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 KiB

After

Width:  |  Height:  |  Size: 828 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -1,46 +1,82 @@
{ {
"cap1-single-element": { "cap1-single-element": {
"bundle": "all-people-groups", "bundle": "all-people-groups",
"sha256": "312b3a0f64b0842b498ae8c0c88491a9225c353a60c0420e05ac18fa0333a0a5" "sha256": "33b9b1e760d930f5a92c0b2c194c2ab0542ffe6e0d84ac6a0896587e551b18fc"
}, },
"cap2-adoption": { "cap2-adoption": {
"bundle": "doxa-simple-map", "bundle": "doxa-simple-map",
"sha256": "95ab02edf30ab19b1637238bb50f580286daa2808fbcb43478dbb0c3d0115d95" "sha256": "c46bc416c687dc4e41d5bae5a387995aec65d4928cde3aa55e0188c295c50546"
}, },
"cap2-engagement": { "cap2-engagement": {
"bundle": "doxa-simple-map", "bundle": "doxa-simple-map",
"sha256": "95ab02edf30ab19b1637238bb50f580286daa2808fbcb43478dbb0c3d0115d95" "sha256": "c46bc416c687dc4e41d5bae5a387995aec65d4928cde3aa55e0188c295c50546"
}, },
"cap2-prayer": { "cap2-prayer": {
"bundle": "doxa-simple-map", "bundle": "doxa-simple-map",
"sha256": "95ab02edf30ab19b1637238bb50f580286daa2808fbcb43478dbb0c3d0115d95" "sha256": "c46bc416c687dc4e41d5bae5a387995aec65d4928cde3aa55e0188c295c50546"
}, },
"cap3-monolithic-research": { "cap3-monolithic-research": {
"bundle": "doxa-research-map", "bundle": "doxa-research-map",
"sha256": "7e3637a635faf8223488a4bc21c1f308c5b3ea94fa6c80d5f1560a04ef7ec9cb" "sha256": "9cbe76cb74f992dbbc1dc61c50270f98f787cb983d402b5ee6cc6a26bc6878a4"
}, },
"cap4-upg100-list": { "cap4-upg100-list": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d" "sha256": "95ee299d03809f7cc9bb9c84b7f68b6706ffaa482873900289028fede38aa9c6"
}, },
"cap4-upg100-mfe": { "cap4-upg100-mfe": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d" "sha256": "95ee299d03809f7cc9bb9c84b7f68b6706ffaa482873900289028fede38aa9c6"
}, },
"cap4-upg100-poster-wall": { "cap4-upg100-poster-wall": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d" "sha256": "95ee299d03809f7cc9bb9c84b7f68b6706ffaa482873900289028fede38aa9c6"
}, },
"cap5-doxa-maps-spa": { "cap5-doxa-maps-spa": {
"bundle": "doxa-maps-spa", "bundle": "doxa-maps-spa",
"sha256": "e24edf74f5609549b8d83250ebef269f3d6a00423cd3ca28b9a86e23bfba8e91" "sha256": "3d5467fd47ada47979fb878a5a355329ddc3ad88071ec947bf6c4a61d53df143"
}, },
"cap5-plugin-studio": { "cap5-plugin-studio": {
"bundle": "plugin-studio", "bundle": "plugin-studio",
"sha256": "fee321fdfd05fc83567987c682f826f890cbf76722d9a94290335f2228b9dd61" "sha256": "4d3910b06d55c7fccdba7102be4f9420a0e1368efd1695a6abaf12442b4fcda0"
}, },
"deploy2-mobile-upg100": { "deploy2-mobile-upg100": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d" "sha256": "95ee299d03809f7cc9bb9c84b7f68b6706ffaa482873900289028fede38aa9c6"
},
"prod-big-picture": {
"bundle": "dashboard-plus-grid",
"sha256": "3c1fc9c4af52752e545365676d630e9dfee3a5cd67018ca5181d499304742f61"
},
"prod-cities-internet": {
"bundle": "cities-internet",
"sha256": "1372e30e723497c6164c409b2e41e095f5f18f38caba606edb4ca58293640d39"
},
"prod-cities-internet-phone": {
"bundle": "cities-internet",
"sha256": "1372e30e723497c6164c409b2e41e095f5f18f38caba606edb4ca58293640d39"
},
"prod-gospel-resources": {
"bundle": "gospel-resources",
"sha256": "abf213dca851c7ef0d3dd94cb9456f4dd01b1b8e9dea4acf65452f56fd0c5879"
},
"prod-gospel-resources-phone": {
"bundle": "gospel-resources",
"sha256": "abf213dca851c7ef0d3dd94cb9456f4dd01b1b8e9dea4acf65452f56fd0c5879"
},
"prod-prayer-phone": {
"bundle": "doxa-simple-map",
"sha256": "c46bc416c687dc4e41d5bae5a387995aec65d4928cde3aa55e0188c295c50546"
},
"prod-regions": {
"bundle": "regions-dashboard",
"sha256": "95ea2a4720a0c2081f3f3c757b658f9a5b4358b1943d11aefb2b3566df6e88b6"
},
"prod-regions-phone": {
"bundle": "regions-dashboard",
"sha256": "95ea2a4720a0c2081f3f3c757b658f9a5b4358b1943d11aefb2b3566df6e88b6"
},
"prod-simple-research": {
"bundle": "research-map-plus-grid",
"sha256": "458139bb4b1da0e79fefeb3c671b78d81da4ef832e411e54789915ca07007ee7"
} }
} }

View file

@ -50,6 +50,10 @@ const ONLY = argOf('--only', '') ? argOf('--only', '').split(',') : null
// in iframe mode) and every tab must still paint — with ZERO requests to api.mapbox.com. That is the // 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. // promise a keyless partner embed depends on, and the shell once broke it by not forwarding the engine.
const PROVIDER = argOf('--provider', '') const PROVIDER = argOf('--provider', '')
// --live-cors: serve the tree with the LIVE static layer's CORS rule (no Access-Control-Allow-Origin
// on the tree-root manifest.json; see serve-strict LIVE_CORS). The gallery must still boot on a
// foreign page, by reading the CORS-readable copy generate-tree writes beside its bundle.
const LIVE_CORS = argv.includes('--live-cors')
// KNOWN, ROUTED exemptions from the zero-Mapbox rule — each names its owner. Printed on every run so // 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). // it cannot become invisible; DELETE the entry when the owner lands the fix (the gate then enforces it).
// KNOWN, ROUTED good-neighbour violations by NESTED plugins (key = '<tab id>:<scroll|url|title>'). // KNOWN, ROUTED good-neighbour violations by NESTED plugins (key = '<tab id>:<scroll|url|title>').
@ -123,7 +127,7 @@ const hostPage = () => {
<style>body{margin:0} header,footer{height:60px;background:#eee}</style></head> <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>` <body><header id=hdr>HOST HEADER</header><main style="max-width:1200px;margin:0 auto">${embed}</main><footer>HOST FOOTER</footer></body></html>`
} }
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' }) const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore', env: { ...process.env, LIVE_CORS: LIVE_CORS ? '1' : '' } })
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html' }); r.end(hostPage()) }).listen(HOST_PORT, 'localhost') const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html' }); r.end(hostPage()) }).listen(HOST_PORT, 'localhost')
const cleanup = () => { try { tree.kill() } catch {} try { host.close() } catch {} } const cleanup = () => { try { tree.kill() } catch {} try { host.close() } catch {} }
await new Promise(r => setTimeout(r, 800)) await new Promise(r => setTimeout(r, 800))

View file

@ -161,6 +161,19 @@ const DEFINE_RE = /customElements\.define\(\s*['"]([^'"]+)['"]/g // registered
// manifest lists exactly what the dev manifest lists (a blank template-map-a/b in the built studio was // manifest lists exactly what the dev manifest lists (a blank template-map-a/b in the built studio was
// the only difference, found 2026-09-25). Not gatedProfiles: that list is governance (private data). // the only difference, found 2026-09-25). Not gatedProfiles: that list is governance (private data).
const SCAFFOLD_BUNDLES = ['template-bundle'] const SCAFFOLD_BUNDLES = ['template-bundle']
// ★ THE MAPBOX ENGINE URL, read from the ONE place that loads it (useMapInstance.js ENGINES.mapbox.js),
// so the embed snippet's preload can never point at a different version than the one the bundle
// requests — a mismatched preload is not used, and the engine would download twice.
const MAPBOX_GL_JS = (() => {
try {
const src = readFileSync(resolve(__dirname, '..', 'library', 'composables', 'useMapInstance.js'), 'utf8')
const m = src.match(/mapbox:\s*\{[^}]*?js:\s*'([^']+mapbox-gl\.js)'/)
if (m) return m[1]
} catch { /* fall through */ }
console.warn('[generate-tree] could not read the Mapbox engine URL from useMapInstance.js; embed snippets get no preload')
return ''
})()
const isBundleDir = (name) => !name.startsWith('_') && !name.startsWith('.') && !DEST_CONFIG.gatedProfiles.includes(name) && !SCAFFOLD_BUNDLES.includes(name) const isBundleDir = (name) => !name.startsWith('_') && !name.startsWith('.') && !DEST_CONFIG.gatedProfiles.includes(name) && !SCAFFOLD_BUNDLES.includes(name)
// ★ CUSTOM-ELEMENT NAME RULE (HTML spec): lowercase, must contain a hyphen, no spaces. // ★ CUSTOM-ELEMENT NAME RULE (HTML spec): lowercase, must contain a hyphen, no spaces.
@ -1204,7 +1217,20 @@ function scanBundles() {
isMap, isMap,
iframeUrl, iframeUrl,
scriptUrl, scriptUrl,
embed: { iframe: iframeEmbed, script: scriptEmbed }, // ★ ENGINE PRELOAD, map snippets only (measured 2026-09-25, cold, 10 Mbps). A partner page that
// pastes this snippet has no leaf page to fetch mapbox-gl.js, so the bundle requested it only
// after it had downloaded, run and mounted: doxa-simple-map's engine started at 1.06 s, right
// after its bundle (0.99 s); regions-mfe's at 2.67 s. A preload lets the ~480 KB engine download
// alongside the bundle. NO crossorigin attribute, on purpose: ensureEngineScript injects a plain
// <script src>, and a preload is only reused when its request mode matches — with crossorigin it
// would download twice. Mapbox only: a non-map profile gets none, and a partner switching to
// provider:"maplibre" is told to drop the line, since that engine must make zero Mapbox requests.
embed: {
iframe: iframeEmbed,
script: isMap && MAPBOX_GL_JS
? `<!-- Mapbox engine, fetched early; remove this line if you set "provider":"maplibre" -->\n<link rel="preload" as="script" href="${MAPBOX_GL_JS}">\n` + scriptEmbed
: scriptEmbed,
},
...(instances.length ? { instances } : {}), ...(instances.length ? { instances } : {}),
// Extra profile-config keys from <profile>.meta.json `config` (e.g. pinHint:false). // Extra profile-config keys from <profile>.meta.json `config` (e.g. pinHint:false).
// GENERATOR-INTERNAL: stripped before the manifest is written (see writeManifestAndHtml) // GENERATOR-INTERNAL: stripped before the manifest is written (see writeManifestAndHtml)
@ -1284,6 +1310,13 @@ function writeManifestAndHtml(root, bundles, tokenUrlExpr, bakedToken) {
} }
mkdirSync(root, { recursive: true }) mkdirSync(root, { recursive: true })
writeFileSync(join(root, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n') writeFileSync(join(root, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n')
// ★ A CORS-READABLE COPY for the Research Gallery, byte-identical, written in the SAME step so the
// two can never diverge. The live static layer sends Access-Control-Allow-Origin on paths inside a
// folder of the tree but not on the root manifest (measured 2026-09-25), so a partner page's
// script embed of <doxa-maps-spa> reads this copy. See doxa-maps-spa.vue loadManifest.
if (existsSync(join(root, 'doxa-maps-spa'))) {
writeFileSync(join(root, 'doxa-maps-spa', 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n')
}
writeFileSync(join(root, 'index.html'), rootHtml(bundles, manifest)) writeFileSync(join(root, 'index.html'), rootHtml(bundles, manifest))
// ── THE WEB-ROOT PAGE = THE STUDIO ────────────────────────────────────────────────────────── // ── THE WEB-ROOT PAGE = THE STUDIO ──────────────────────────────────────────────────────────

View file

@ -87,7 +87,10 @@ for (const ref of ONLY) {
const first = (k) => all.filter((x) => x.kind === k).sort((a, c) => a.start - c.start)[0] const first = (k) => all.filter((x) => x.kind === k).sort((a, c) => a.start - c.start)[0]
const bundleReq = all.find((x) => x.kind === 'bundle' && x.url.includes(`/${bundle}/${bundle}.js`)) || first('bundle') const bundleReq = all.find((x) => x.kind === 'bundle' && x.url.includes(`/${bundle}/${bundle}.js`)) || first('bundle')
const eng = first('engine'), sty = first('style'), dat = first('data') const eng = first('engine'), sty = first('style'), dat = first('data')
// A preload the browser cannot reuse (request mode mismatch) shows up as a SECOND engine request.
const engineRequests = all.filter((x) => x.kind === 'engine').length
runs.push({ runs.push({
engineRequests,
loaded, styled, bundleEnd: bundleReq && bundleReq.end, engineStart: eng && eng.start, engineEnd: eng && eng.end, loaded, styled, bundleEnd: bundleReq && bundleReq.end, engineStart: eng && eng.start, engineEnd: eng && eng.end,
styleStart: sty && sty.start, dataStart: dat && dat.start, styleStart: sty && sty.start, dataStart: dat && dat.start,
bytes: { bundle: 0 }, bytes: { bundle: 0 },
@ -95,7 +98,7 @@ for (const ref of ONLY) {
await ctx.close() await ctx.close()
} }
const med = (k) => { const v = runs.map((x) => x[k]).filter((x) => x != null).sort((a, c) => a - c); return v.length ? v[Math.floor(v.length / 2)] : null } const med = (k) => { const v = runs.map((x) => x[k]).filter((x) => x != null).sort((a, c) => a - c); return v.length ? v[Math.floor(v.length / 2)] : null }
console.log(' measured ' + ref + ': ' + JSON.stringify(runs.map((x) => x.loaded))) console.log(' measured ' + ref + ': ' + JSON.stringify(runs.map((x) => x.loaded)) + ' · engine requests per run ' + JSON.stringify(runs.map((x) => x.engineRequests)))
rows.push({ ref, styled: med('styled'), loaded: med('loaded'), bundleEnd: med('bundleEnd'), engineStart: med('engineStart'), engineEnd: med('engineEnd'), styleStart: med('styleStart'), dataStart: med('dataStart'), runs: runs.map((x) => x.loaded) }) rows.push({ ref, styled: med('styled'), loaded: med('loaded'), bundleEnd: med('bundleEnd'), engineStart: med('engineStart'), engineEnd: med('engineEnd'), styleStart: med('styleStart'), dataStart: med('dataStart'), runs: runs.map((x) => x.loaded) })
} }
await browser.close(); srv.kill(); host.close() await browser.close(); srv.kill(); host.close()

View file

@ -366,6 +366,24 @@ virtualPages.set('/__readme-iframe-host.html', `<!doctype html>
await shot('deploy1-iframe', '/__readme-iframe-host.html', await shot('deploy1-iframe', '/__readme-iframe-host.html',
{ vp: { width: 1000, height: 700 }, wait: 11000, countFrames: true, mapboxReferer: false }) { vp: { width: 1000, height: 700 }, wait: 11000, countFrames: true, mapboxReferer: false })
// ---- THE 7 PRODUCTION MAPS: one shot each, for the README's opening gallery ----
// Each is the plugin's own standalone leaf page — the same element the Research Gallery nests.
// (UPG-100 List and Prayer Progress reuse cap4-upg100-mfe and cap2-prayer.)
await shot('prod-big-picture', '/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html', { wait: 10000, minElements: 300 })
await shot('prod-simple-research', '/doxa-maps/research-map-plus-grid/research-map-plus-grid/index.html', { wait: 10000, minElements: 200 })
await shot('prod-regions', '/doxa-maps/regions-dashboard/regions-mfe/index.html', { wait: 10000, minElements: 100 })
await shot('prod-cities-internet', '/doxa-maps/cities-internet/connectivity-map/index.html', { wait: 10000, minElements: 60 })
await shot('prod-gospel-resources', '/doxa-maps/gospel-resources/resources-map/index.html', { wait: 10000, minElements: 100 })
// ---- THE PRODUCTION MAPS ON A PHONE (390x844) — for the README's phone row ----
// Big Picture is desktop-only by ruling (on phones the Research Gallery disables it and Simple Research is
// the dashboard), so it has no phone shot. UPG-100's phone shot is deploy2-mobile-upg100. Simple Research
// (and the Research Gallery, which opens on it on phones) are left out until its phone layout is fixed.
await shot('prod-regions-phone', '/doxa-maps/regions-dashboard/regions-mfe/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-prayer-phone', '/doxa-maps/doxa-simple-map/doxa-simple-map/prayer/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-cities-internet-phone', '/doxa-maps/cities-internet/connectivity-map/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
await shot('prod-gospel-resources-phone', '/doxa-maps/gospel-resources/resources-map/index.html', { vp: { width: 390, height: 844 }, dsf: 2, wait: 10000, minElements: 60 })
// ---- DEPLOY2: mobile — the UPG-100 list as the phone app ---- // ---- DEPLOY2: mobile — the UPG-100 list as the phone app ----
// The mobile example is UPG-100 (ruled 2026-09-24; Simple Research was retired as the example because // The mobile example is UPG-100 (ruled 2026-09-24; Simple Research was retired as the example because
// it was never built for phones). Shot once its owner marked the phone layout ready at 88a566f: // it was never built for phones). Shot once its owner marked the phone layout ready at 88a566f:

View file

@ -124,7 +124,11 @@ createServer((req, res) => {
// LIVE nuxtbase static layer has the same gap -- carded with the Traefik ready-fix). // LIVE nuxtbase static layer has the same gap -- carded with the Traefik ready-fix).
// ACAO:* is the standard for credential-less public assets; production parity is the // ACAO:* is the standard for credential-less public assets; production parity is the
// whole point of this server, so it must teach the same requirement. // whole point of this server, so it must teach the same requirement.
'access-control-allow-origin': '*', // LIVE_CORS=1 reproduces the LIVE static layer's rule instead (measured on nuxtbase 2026-09-25):
// ACAO on every path INSIDE a folder of the tree, NOT on the tree-root manifest.json. Off by
// default; check:gallery-embed --live-cors uses it to prove the gallery still boots cross-origin
// under the rule partners actually get.
...(process.env.LIVE_CORS === '1' && /^\/doxa-maps\/manifest\.json$/.test(pathname) ? {} : { 'access-control-allow-origin': '*' }),
}) })
createReadStream(file).pipe(res) createReadStream(file).pipe(res)
}).listen(PORT, HOST, () => { }).listen(PORT, HOST, () => {

View file

@ -0,0 +1,115 @@
#!/usr/bin/env node
/**
* set-app-start-page — the MOBILE APP opens the UPG-100 list, not the plugin studio.
*
* THE DEFECT, reported by repo: the Capacitor app launches public/index.html, which mounts
* <plugin-studio>. A recipient opening the phone app meets the DEV STUDIO rather than the map the
* README promises. RULED (Kingdom-agent, card DECISION-MOBILE-EXAMPLE-IS-UPG100-NOT-SIMPLE-
* RESEARCH): the ONE mobile-app showcase is the UPG-100 list.
*
* ⚠ THE CARD'S OWN BLUF SAYS "Simple Research" AND IS STALE. It predates that ruling, which is
* recorded as DONE in the same backlog and says Simple Research "was never mobile-friendly".
* Building to the card's wording would ship the app the ruling rejected.
*
* ★ WHY THIS RUNS AFTER `cap sync` AND WRITES ONLY INTO THE ANDROID ASSETS.
* public/index.html has THREE consumers (capacitor.config.json documents them): Railway/CDN,
* Capacitor, and local staging. The studio is CORRECT for the CDN root and for `bun run serve` —
* only the phone build wants a different landing page. Rewriting the shared root would fix the app
* by breaking the other two. `cap sync` copies webDir into
* deployment/capacitor/android/app/src/main/assets/public/, so overwriting the COPY changes what
* the app opens and leaves the served tree untouched.
*
* ★ WHY IT COPIES THE BUILT LEAF RATHER THAN HAND-WRITING A HOST PAGE.
* The leaf carries an SRI integrity hash for its bundle. A hand-written page needs its own correct
* hash, and a stale one makes the browser REFUSE the script with no console error at all: the
* element never upgrades and the app shows a blank screen. Copying the artifact the build already
* verified means there is no second copy of that hash to drift. Only the relative depth changes.
*
* Usage: node internal/set-app-start-page.mjs [--check]
* --check assert the app root is already the UPG-100 page; do not write. Exit 1 if not.
*/
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const CHECK = process.argv.includes('--check')
// The assets tree `cap sync` writes. NOT public/ — see the header.
const APP_PUBLIC = join(ROOT, 'deployment/capacitor/android/app/src/main/assets/public')
const APP_ROOT = join(APP_PUBLIC, 'index.html')
// The showcase leaf, inside the served tree.
const TREE = 'doxa-maps'
const LEAF_REL = `${TREE}/my-upg-100-list/upg-100-mfe/index.html`
const LEAF = join(APP_PUBLIC, LEAF_REL)
// A marker so --check can tell the app page apart from the studio page without parsing HTML.
const MARKER = '<!-- doxa-maps: mobile app start page (UPG-100 list) -->'
if (!existsSync(APP_PUBLIC)) {
console.log('APP-START-PAGE: SKIP — no synced assets tree.')
console.log(' Run `bun run cap:sync` first; this rewrites what that step copied.')
process.exit(0)
}
if (!existsSync(LEAF)) {
console.error(`APP-START-PAGE: FAIL — the showcase leaf is missing from the synced tree:`)
console.error(` ${LEAF_REL}`)
console.error(' The app would launch a 404. Build the tree and re-run cap:sync.')
process.exit(1)
}
if (CHECK) {
const cur = existsSync(APP_ROOT) ? readFileSync(APP_ROOT, 'utf8') : ''
if (cur.includes(MARKER)) {
console.log('APP-START-PAGE: PASS — the app opens the UPG-100 list.')
process.exit(0)
}
console.error('APP-START-PAGE: FAIL — the app root is NOT the UPG-100 page.')
console.error(cur.includes('<plugin-studio')
? ' It mounts <plugin-studio>: a recipient opening the app meets the dev studio.'
: ' No UPG-100 marker found.')
console.error(' Fix: node internal/set-app-start-page.mjs (after cap:sync).')
process.exit(1)
}
// ── REWRITE THE LEAF FOR ONE LEVEL UP ────────────────────────────────────────
// The leaf lives at <tree>/my-upg-100-list/upg-100-mfe/index.html and reaches its bundle with
// `../my-upg-100-list.js`. At the app root that same relative path resolves ABOVE the web root.
// Every leaf-relative reference gains the leaf's own directory prefix; absolute and protocol URLs
// (the Mapbox CDN tags) are left alone.
const LEAF_DIR = `${TREE}/my-upg-100-list/upg-100-mfe/`
const html = readFileSync(LEAF, 'utf8')
const rebased = html.replace(
/\b(src|href)=(["'])(?!https?:|data:|#|\/)([^"']+)\2/g,
(_m, attr, q, url) => {
// Resolve the leaf-relative url against the leaf's directory, from the web root.
const abs = new URL(url, `http://x/${LEAF_DIR}`).pathname.replace(/^\//, '')
return `${attr}=${q}${abs}${q}`
},
)
if (rebased === html) {
console.error('APP-START-PAGE: FAIL — rewrote nothing, so the paths were not what this expects.')
console.error(' Refusing to install a page whose bundle path would 404 at the app root.')
process.exit(1)
}
// Prove the rewrite points at files that EXIST, rather than trusting the regex. A start page
// whose bundle 404s is a blank white screen with no error, which is the failure mode this whole
// script exists to prevent.
const referenced = [...rebased.matchAll(/\b(?:src|href)=["']([^"':]+)["']/g)].map((m) => m[1])
const missing = referenced.filter((r) => !r.startsWith('data:') && !existsSync(join(APP_PUBLIC, r)))
if (missing.length) {
console.error('APP-START-PAGE: FAIL — the rewritten page references files that are not there:')
for (const m of missing) console.error(` ${m}`)
process.exit(1)
}
// Keep the studio page reachable rather than deleting it: a developer with the apk still wants it.
const STUDIO_KEEP = join(APP_PUBLIC, 'studio.html')
if (existsSync(APP_ROOT) && !existsSync(STUDIO_KEEP)) copyFileSync(APP_ROOT, STUDIO_KEEP)
writeFileSync(APP_ROOT, rebased.replace(/<head>/i, `<head>\n ${MARKER}`))
console.log('APP-START-PAGE: the app now opens the UPG-100 list.')
console.log(` from ${LEAF_REL}`)
console.log(` wrote assets/public/index.html (${referenced.length} reference(s) rebased, all present)`)
console.log(' the studio page is kept at assets/public/studio.html')

View file

@ -22,7 +22,11 @@
*/ */
import { tStatic } from '@map/i18n/index.js' import { tStatic } from '@map/i18n/index.js'
// See ResearchMapSideMenu for why this reads the catalogue directly rather than through useI18n. // See ResearchMapSideMenu for why this reads the catalogue directly rather than through useI18n.
const L = (key, en) => tStatic('controls.' + key) || en // Look in this panel's own namespace first, then the dashboard's: the same string can be authored
// in either, and a key that exists one namespace over should not silently render English. MEASURED:
// 'Any' and '0 active' were catalogued under dashboard.* while this file asked for controls.* only,
// so both fell back to English in all 11 locales while the catalogue already had them.
const L = (key, en) => tStatic('controls.' + key) || tStatic('dashboard.' + key) || en
import { inject, computed, watch, ref } from 'vue' import { inject, computed, watch, ref } from 'vue'
import { useShadowStyles } from '../composables/useShadowStyles.js' import { useShadowStyles } from '../composables/useShadowStyles.js'
@ -137,6 +141,11 @@ useShadowStyles(`
.rm-pill { background:rgba(124,140,248,0.12);color:#a8b8ff;padding:2px 8px;border-radius:11px;font-size:11px;font-weight:600; } .rm-pill { background:rgba(124,140,248,0.12);color:#a8b8ff;padding:2px 8px;border-radius:11px;font-size:11px;font-weight:600; }
.rm-link-btn { background:none;border:none;color:#7c8cf8;cursor:pointer;font-size:11px;padding:0; } .rm-link-btn { background:none;border:none;color:#7c8cf8;cursor:pointer;font-size:11px;padding:0; }
.rm-link-btn:hover { text-decoration:underline; } .rm-link-btn:hover { text-decoration:underline; }
/* WCAG 2.5.8 Target Size (Minimum), AA = 24x24 CSS px. Scoped to (pointer:coarse):
2.5.8 governs touch ergonomics, and these are SHARED components sitting on maps this
change does not own -- a finger gets the bigger target, the mouse keeps the density
those owners designed. min-height states the 24 rather than deriving it from a font. */
@media (pointer:coarse) { .rm-link-btn { min-height: 24px; } }
`, 'research-map-filter-panel') `, 'research-map-filter-panel')
</script> </script>
@ -155,7 +164,7 @@ useShadowStyles(`
<div class="field"> <div class="field">
<label>{{ L('engagement', 'Engagement') }}</label> <label>{{ L('engagement', 'Engagement') }}</label>
<select :value="engagement" @change="engagement = $event.target.value"> <select :value="engagement" @change="engagement = $event.target.value">
<option value="any">Any</option> <option value="any">{{ L('any', 'Any') }}</option>
<option value="engaged">{{ L('engaged', 'Engaged') }}</option> <option value="engaged">{{ L('engaged', 'Engaged') }}</option>
<option value="unengaged">{{ L('unengaged', 'Unengaged') }}</option> <option value="unengaged">{{ L('unengaged', 'Unengaged') }}</option>
</select> </select>
@ -163,7 +172,7 @@ useShadowStyles(`
<div class="field"> <div class="field">
<label>{{ L('adoption', 'Adoption') }}</label> <label>{{ L('adoption', 'Adoption') }}</label>
<select :value="adoption" @change="adoption = $event.target.value"> <select :value="adoption" @change="adoption = $event.target.value">
<option value="any">Any</option> <option value="any">{{ L('any', 'Any') }}</option>
<option value="adopted">{{ L('adopted', 'Adopted') }}</option> <option value="adopted">{{ L('adopted', 'Adopted') }}</option>
<option value="unadopted">{{ L('unadopted', 'Unadopted') }}</option> <option value="unadopted">{{ L('unadopted', 'Unadopted') }}</option>
</select> </select>
@ -203,7 +212,7 @@ useShadowStyles(`
<!-- Footer: active count + reset link ─────────────────────────────── --> <!-- Footer: active count + reset link ─────────────────────────────── -->
<div class="rm-filter-footer"> <div class="rm-filter-footer">
<span class="rm-pill">{{ activeCount }} active</span> <span class="rm-pill">{{ L('active_count', '{n} active').replace('{n}', activeCount) }}</span>
<button class="rm-link-btn" @click="resetAll">{{ L('reset_all', 'Reset all') }}</button> <button class="rm-link-btn" @click="resetAll">{{ L('reset_all', 'Reset all') }}</button>
</div> </div>
</div> </div>

View file

@ -351,6 +351,12 @@ useShadowStyles(`
.pin-hint-x { pointer-events: auto; border: none; background: none; color: #aeb7c2; .pin-hint-x { pointer-events: auto; border: none; background: none; color: #aeb7c2;
font-size: 16px; line-height: 1; cursor: pointer; padding: 0 0 0 2px; } font-size: 16px; line-height: 1; cursor: pointer; padding: 0 0 0 2px; }
.pin-hint-x:hover { color: #fff; } .pin-hint-x:hover { color: #fff; }
/* WCAG 2.5.8 Target Size (Minimum), AA = 24x24 CSS px. Scoped to (pointer:coarse):
2.5.8 governs touch ergonomics, and these are SHARED components sitting on maps this
change does not own -- a finger gets the bigger target, the mouse keeps the density
those owners designed. min-height states the 24 rather than deriving it from a font. */
@media (pointer:coarse) { .pin-hint-x { min-width: 24px; min-height: 24px;
display: inline-flex; align-items: center; justify-content: center; padding: 0; } }
.pin-hint.dark .pin-hint-pill { background: rgba(240, 243, 247, .95); color: #1b212b; } .pin-hint.dark .pin-hint-pill { background: rgba(240, 243, 247, .95); color: #1b212b; }
.pin-hint.dark .pin-hint-x { color: #5a6470; } .pin-hint.dark .pin-hint-x { color: #5a6470; }
.pin-hint.dark .pin-hint-x:hover { color: #1b212b; } .pin-hint.dark .pin-hint-x:hover { color: #1b212b; }

View file

@ -85,6 +85,11 @@ useShadowStyles(`
same bg/border/radius/hover as the desktop SemanticTreeLegend caret). */ same bg/border/radius/hover as the desktop SemanticTreeLegend caret). */
.mobile-collapse-caret{width:22px;height:20px;display:flex;align-items:center;justify-content:center;background:rgba(208,215,222,0.4);border:1px solid #d0d7de;border-radius:5px;cursor:pointer;color:#57606a;padding:0;transition:transform 0.3s ease-out,color 0.1s,background 0.1s,border-color 0.1s;transform-origin:center;outline:none;-webkit-tap-highlight-color:transparent;flex-shrink:0;} .mobile-collapse-caret{width:22px;height:20px;display:flex;align-items:center;justify-content:center;background:rgba(208,215,222,0.4);border:1px solid #d0d7de;border-radius:5px;cursor:pointer;color:#57606a;padding:0;transition:transform 0.3s ease-out,color 0.1s,background 0.1s,border-color 0.1s;transform-origin:center;outline:none;-webkit-tap-highlight-color:transparent;flex-shrink:0;}
.mobile-collapse-caret:focus{outline:none;} .mobile-collapse-caret:focus{outline:none;}
/* WCAG 2.5.8 Target Size (Minimum), AA = 24x24 CSS px. Scoped to (pointer:coarse):
2.5.8 governs touch ergonomics, and these are SHARED components sitting on maps this
change does not own -- a finger gets the bigger target, the mouse keeps the density
those owners designed. min-height states the 24 rather than deriving it from a font. */
@media (pointer:coarse){.mobile-collapse-caret{width:24px;height:24px;}}
.mobile-collapse-caret:hover{color:#3b463d;background:rgba(59,70,61,0.12);border-color:#3b463d;} .mobile-collapse-caret:hover{color:#3b463d;background:rgba(59,70,61,0.12);border-color:#3b463d;}
.legend-mobile-sheet.sheet-dark .mobile-collapse-caret{background:rgba(110,118,129,0.15);border-color:#30363d;color:#8b949e;box-shadow:none;} .legend-mobile-sheet.sheet-dark .mobile-collapse-caret{background:rgba(110,118,129,0.15);border-color:#30363d;color:#8b949e;box-shadow:none;}
.legend-mobile-sheet.sheet-dark .mobile-collapse-caret:hover{color:#c9d1d9;background:rgba(59,70,61,0.18);border-color:#73A17F;} .legend-mobile-sheet.sheet-dark .mobile-collapse-caret:hover{color:#c9d1d9;background:rgba(59,70,61,0.18);border-color:#73A17F;}

View file

@ -752,6 +752,13 @@ useShadowStyles(`
display: inline-flex; align-items: center; justify-content: center; gap: 3px; display: inline-flex; align-items: center; justify-content: center; gap: 3px;
} }
.stl-hdr-num:hover { color: #c9d1d9; } .stl-hdr-num:hover { color: #c9d1d9; }
/* WCAG 2.5.8 Target Size (Minimum), AA = 24x24 CSS px. Scoped to (pointer:coarse):
2.5.8 governs touch ergonomics, and these are SHARED components sitting on maps this
change does not own -- a finger gets the bigger target, the mouse keeps the density
those owners designed. min-height states the 24 rather than deriving it from a font. */
/* min-height ONLY. The width is var(--stl-num-w), a layout variable the header's columns are
aligned on; forcing a min-width here would push those columns apart to fix a height. */
@media (pointer:coarse) { .stl-hdr-num { min-height: 24px; } }
.stl-hdr-num.active { color: #73A17F; } .stl-hdr-num.active { color: #73A17F; }
.stl-sort-arrow { font-size: 8px; } .stl-sort-arrow { font-size: 8px; }
.stl-rows { flex: 1; overflow-y: auto; padding-bottom: 6px; } .stl-rows { flex: 1; overflow-y: auto; padding-bottom: 6px; }

View file

@ -754,6 +754,29 @@ useShadowStyles(
'padding:2px 5px;cursor:pointer;line-height:1.25;white-space:normal;overflow-wrap:break-word}' + 'padding:2px 5px;cursor:pointer;line-height:1.25;white-space:normal;overflow-wrap:break-word}' +
'.pstock:hover{border-color:#10b981;color:#10b981}' + '.pstock:hover{border-color:#10b981;color:#10b981}' +
'.pstock.is-on{border-color:#10b981;color:#0b7a5b;background:rgba(16,185,129,.10)}' + '.pstock.is-on{border-color:#10b981;color:#0b7a5b;background:rgba(16,185,129,.10)}' +
// ★ WCAG 2.5.8 Target Size (Minimum), AA = 24x24 CSS px. MEASURED at 390x844 on the UPG-100
// wall: .pbtn 76x20, .pstock 188x20, .tape-h 289x22 -- 135 controls under the minimum on one
// phone screen, and the same components fail on three other maps because they live in library/.
// SCOPED TO (pointer:coarse) ON PURPOSE: 2.5.8 is a TOUCH ergonomics rule, and these three
// controls sit in a row whose height the Driver and I tuned by eye this session -- the tape must
// straddle the border without reaching the poster's blue outline, with ~9px of artwork clearance
// left. Growing them for the mouse would spend 2px of that clearance to solve a problem a mouse
// does not have, on a wall that is another owner's map. A finger gets the bigger target; the
// desktop keeps the geometry that was signed off. Safe to grow because both stickers are now
// pinned by --tape-overlap rather than by their centres: the overlap onto the banner is the
// constant, so extra height grows into empty space instead of walking over the artwork.
// ⚠ STATE THE REQUIREMENT, DO NOT COMPUTE IT. The first attempt here grew the padding instead
// -- 2px a side on .pbtn, 2px on .pstock -- and MEASURED 23.7px and 23.0px: under the minimum by
// a third of a pixel and by one, because the height is padding PLUS a line box derived from a
// 10.5px font, and that line box is the renderer's number, not mine. min-height IS the 24 the
// spec asks for, it survives a font swap or a locale with taller glyphs, and a reader can check
// it against WCAG without doing arithmetic. Both controls centre their own label, so no padding
// change is needed to go with it.
'@media (pointer:coarse){' +
'.pbtn{min-height:24px}' +
'.pstock{min-height:24px}' +
'.tape-h{min-height:24px}' +
'}' +
// previewAction='print' only. The zoom-in cursor says "this opens something", and // previewAction='print' only. The zoom-in cursor says "this opens something", and
// the focus-visible outline is not optional: it is how a keyboard user sees where // the focus-visible outline is not optional: it is how a keyboard user sees where
// they are on a control that is otherwise just a picture. // they are on a control that is otherwise just a picture.

View file

@ -1,4 +1,5 @@
<script setup> <script setup>
import { tStatic } from '@map/i18n/index.js'
/** /**
* SearchFilterHeader.vue — SHARED (@map/components/search/SearchFilterHeader.vue). * SearchFilterHeader.vue — SHARED (@map/components/search/SearchFilterHeader.vue).
* *
@ -80,12 +81,19 @@ const truthy = (v) => !(v === false || v === 'false' || v === '0' || v === 0)
const useSearch = computed(() => truthy(props.showSearch)) const useSearch = computed(() => truthy(props.showSearch))
const useFilters = computed(() => truthy(props.showFilters)) const useFilters = computed(() => truthy(props.showFilters))
// ★ THESE DEFAULTS WERE THE LAST ENGLISH IN THE HEADER. They are the fallbacks a host that passes
// no `labels` gets — which is every host — so in ar/zh/ru/hi the filter row, its aria label, the
// chip remove buttons and the "World" empty state all read English above translated content.
// The catalogue answers first now; a host's own labels still win, which is what the prop is for.
const L = computed(() => ({ const L = computed(() => ({
show_filters: 'Show Filters', show_filters: tStatic('grid.show_filters') || 'Show Filters',
hide_filters: 'Hide Filters', hide_filters: tStatic('grid.hide_filters') || 'Hide Filters',
clear_all: 'Clear All', clear_all: tStatic('grid.clear_all') || 'Clear All',
type_to_search: 'Type to search...', type_to_search: tStatic('grid.type_to_search') || 'Type to search...',
filters: 'Filters', filters: tStatic('dashboard.filters') || 'Filters',
selected: tStatic('dashboard.selected') || 'Selected',
world: tStatic('dashboard.world') || 'World',
remove: tStatic('dashboard.remove_filter') || 'Remove',
...props.labels, ...props.labels,
})) }))
@ -208,7 +216,7 @@ function onClearAll() { emit('clear-all') }
<template v-if="chips.length"> <template v-if="chips.length">
<span v-for="c in chips" :key="c.name" class="sfh__chip"> <span v-for="c in chips" :key="c.name" class="sfh__chip">
{{ c.label }} {{ c.label }}
<button class="sfh__chip-x" type="button" :aria-label="`Remove ${c.label}`" @click="onRemoveChip(c.name)">&times;</button> <button class="sfh__chip-x" type="button" :aria-label="`${L.remove} ${c.label}`" @click="onRemoveChip(c.name)">&times;</button>
</span> </span>
<button class="sfh__clear" type="button" @click="onClearAll">{{ L.clear_all }}</button> <button class="sfh__clear" type="button" @click="onClearAll">{{ L.clear_all }}</button>
</template> </template>

View file

@ -28,5 +28,11 @@
"unengaged": "يحتاج إلى انخراط", "unengaged": "يحتاج إلى انخراط",
"language": "اللغة", "language": "اللغة",
"religion": "الدين", "religion": "الدين",
"population": "السكان" "population": "السكان",
"any": "الكل",
"active_count": "{n} نشط",
"prayer_status": "حالة الصلاة",
"close": "إغلاق",
"clear": "مسح",
"map_controls": "أدوات التحكم بالخريطة"
} }

View file

@ -48,5 +48,7 @@
"filter_by": "تصفية حسب", "filter_by": "تصفية حسب",
"remove_filter": "إزالة التصفية", "remove_filter": "إزالة التصفية",
"groups": "مجموعة", "groups": "مجموعة",
"expand": "توسيع" "expand": "توسيع",
"collapse": "طيّ",
"place": "المكان"
} }

View file

@ -28,5 +28,11 @@
"unengaged": "Braucht Engagement", "unengaged": "Braucht Engagement",
"language": "Sprache", "language": "Sprache",
"religion": "Religion", "religion": "Religion",
"population": "Bevölkerung" "population": "Bevölkerung",
"any": "Alle",
"active_count": "{n} aktiv",
"prayer_status": "Gebetsstatus",
"close": "Schließen",
"clear": "Zurücksetzen",
"map_controls": "Kartensteuerung"
} }

View file

@ -48,5 +48,7 @@
"filter_by": "Filtern nach", "filter_by": "Filtern nach",
"remove_filter": "Filter entfernen", "remove_filter": "Filter entfernen",
"groups": "Volksgruppen", "groups": "Volksgruppen",
"expand": "Aufklappen" "expand": "Aufklappen",
"collapse": "Zuklappen",
"place": "Ort"
} }

View file

@ -28,5 +28,11 @@
"unengaged": "Unengaged", "unengaged": "Unengaged",
"language": "Language", "language": "Language",
"religion": "Religion", "religion": "Religion",
"population": "Population" "population": "Population",
"any": "Any",
"active_count": "{n} active",
"prayer_status": "Prayer Status",
"close": "Close",
"clear": "Clear",
"map_controls": "Research map controls"
} }

View file

@ -48,5 +48,7 @@
"filter_by": "Filter by", "filter_by": "Filter by",
"remove_filter": "Remove filter", "remove_filter": "Remove filter",
"groups": "groups", "groups": "groups",
"expand": "Expand" "expand": "Expand",
"collapse": "Collapse",
"place": "Place"
} }

View file

@ -28,5 +28,11 @@
"unengaged": "Necesita implicación", "unengaged": "Necesita implicación",
"language": "Idioma", "language": "Idioma",
"religion": "Religión", "religion": "Religión",
"population": "Población" "population": "Población",
"any": "Cualquiera",
"active_count": "{n} activos",
"prayer_status": "Estado de oración",
"close": "Cerrar",
"clear": "Borrar",
"map_controls": "Controles del mapa"
} }

View file

@ -48,5 +48,7 @@
"filter_by": "Filtrar por", "filter_by": "Filtrar por",
"remove_filter": "Quitar filtro", "remove_filter": "Quitar filtro",
"groups": "pueblos", "groups": "pueblos",
"expand": "Expandir" "expand": "Expandir",
"collapse": "Contraer",
"place": "Lugar"
} }

View file

@ -28,5 +28,11 @@
"unengaged": "Besoin d'engagement", "unengaged": "Besoin d'engagement",
"language": "Langue", "language": "Langue",
"religion": "Religion", "religion": "Religion",
"population": "Population" "population": "Population",
"any": "Tous",
"active_count": "{n} actifs",
"prayer_status": "État de prière",
"close": "Fermer",
"clear": "Effacer",
"map_controls": "Commandes de la carte"
} }

View file

@ -48,5 +48,7 @@
"filter_by": "Filtrer par", "filter_by": "Filtrer par",
"remove_filter": "Retirer le filtre", "remove_filter": "Retirer le filtre",
"groups": "peuples", "groups": "peuples",
"expand": "Développer" "expand": "Développer",
"collapse": "Replier",
"place": "Lieu"
} }

View file

@ -28,5 +28,11 @@
"unengaged": "सहभागिता चाहिए", "unengaged": "सहभागिता चाहिए",
"language": "भाषा", "language": "भाषा",
"religion": "धर्म", "religion": "धर्म",
"population": "जनसंख्या" "population": "जनसंख्या",
"any": "कोई भी",
"active_count": "{n} सक्रिय",
"prayer_status": "प्रार्थना स्थिति",
"close": "बंद करें",
"clear": "साफ़ करें",
"map_controls": "मानचित्र नियंत्रण"
} }

View file

@ -48,5 +48,7 @@
"filter_by": "इससे फ़िल्टर करें", "filter_by": "इससे फ़िल्टर करें",
"remove_filter": "फ़िल्टर हटाएँ", "remove_filter": "फ़िल्टर हटाएँ",
"groups": "जनसमूह", "groups": "जनसमूह",
"expand": "विस्तार करें" "expand": "विस्तार करें",
"collapse": "संक्षिप्त करें",
"place": "स्थान"
} }

View file

@ -28,5 +28,11 @@
"unengaged": "Necessita impegno", "unengaged": "Necessita impegno",
"language": "Lingua", "language": "Lingua",
"religion": "Religione", "religion": "Religione",
"population": "Popolazione" "population": "Popolazione",
"any": "Tutti",
"active_count": "{n} attivi",
"prayer_status": "Stato di preghiera",
"close": "Chiudi",
"clear": "Cancella",
"map_controls": "Controlli della mappa"
} }

View file

@ -48,5 +48,7 @@
"filter_by": "Filtra per", "filter_by": "Filtra per",
"remove_filter": "Rimuovi filtro", "remove_filter": "Rimuovi filtro",
"groups": "popoli", "groups": "popoli",
"expand": "Espandi" "expand": "Espandi",
"collapse": "Comprimi",
"place": "Luogo"
} }

View file

@ -28,5 +28,11 @@
"unengaged": "Precisa de envolvimento", "unengaged": "Precisa de envolvimento",
"language": "Idioma", "language": "Idioma",
"religion": "Religião", "religion": "Religião",
"population": "População" "population": "População",
"any": "Qualquer",
"active_count": "{n} ativos",
"prayer_status": "Estado de oração",
"close": "Fechar",
"clear": "Limpar",
"map_controls": "Controlos do mapa"
} }

View file

@ -48,5 +48,7 @@
"filter_by": "Filtrar por", "filter_by": "Filtrar por",
"remove_filter": "Remover filtro", "remove_filter": "Remover filtro",
"groups": "povos", "groups": "povos",
"expand": "Expandir" "expand": "Expandir",
"collapse": "Recolher",
"place": "Lugar"
} }

View file

@ -28,5 +28,11 @@
"unengaged": "Necesită implicare", "unengaged": "Necesită implicare",
"language": "Limbă", "language": "Limbă",
"religion": "Religie", "religion": "Religie",
"population": "Populație" "population": "Populație",
"any": "Oricare",
"active_count": "{n} active",
"prayer_status": "Stare de rugăciune",
"close": "Închide",
"clear": "Șterge",
"map_controls": "Comenzile hărții"
} }

View file

@ -48,5 +48,7 @@
"filter_by": "Filtrează după", "filter_by": "Filtrează după",
"remove_filter": "Elimină filtrul", "remove_filter": "Elimină filtrul",
"groups": "popoare", "groups": "popoare",
"expand": "Extinde" "expand": "Extinde",
"collapse": "Restrânge",
"place": "Loc"
} }

View file

@ -28,5 +28,11 @@
"unengaged": "Нуждается в вовлечённости", "unengaged": "Нуждается в вовлечённости",
"language": "Язык", "language": "Язык",
"religion": "Религия", "religion": "Религия",
"population": "Население" "population": "Население",
"any": "Любой",
"active_count": "{n} активно",
"prayer_status": "Статус молитвы",
"close": "Закрыть",
"clear": "Очистить",
"map_controls": "Элементы управления картой"
} }

View file

@ -48,5 +48,7 @@
"filter_by": "Фильтровать по", "filter_by": "Фильтровать по",
"remove_filter": "Убрать фильтр", "remove_filter": "Убрать фильтр",
"groups": "народов", "groups": "народов",
"expand": "Развернуть" "expand": "Развернуть",
"collapse": "Свернуть",
"place": "Место"
} }

View file

@ -28,5 +28,11 @@
"unengaged": "需要参与", "unengaged": "需要参与",
"language": "语言", "language": "语言",
"religion": "宗教", "religion": "宗教",
"population": "人口" "population": "人口",
"any": "全部",
"active_count": "{n} 项启用",
"prayer_status": "代祷状态",
"close": "关闭",
"clear": "清除",
"map_controls": "地图控件"
} }

View file

@ -48,5 +48,7 @@
"filter_by": "筛选", "filter_by": "筛选",
"remove_filter": "移除筛选", "remove_filter": "移除筛选",
"groups": "个民族", "groups": "个民族",
"expand": "展开" "expand": "展开",
"collapse": "收起",
"place": "地点"
} }

View file

@ -24,7 +24,7 @@
"cap:verify": "test -f deployment/capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)", "cap:verify": "test -f deployment/capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)",
"check:tauri": "node internal/check-tauri-config.mjs", "check:tauri": "node internal/check-tauri-config.mjs",
"tauri:build": "./node_modules/.bin/tauri build", "tauri:build": "./node_modules/.bin/tauri build",
"apk:debug": "bun run build && bun run cap:sync && bun run cap:verify && cd deployment/capacitor/android && ./gradlew assembleDebug", "apk:debug": "bun run build && bun run cap:sync && bun run app:start-page && bun run cap:verify && cd deployment/capacitor/android && ./gradlew assembleDebug",
"apk:install": "adb install -r deployment/capacitor/android/app/build/outputs/apk/debug/app-debug.apk", "apk:install": "adb install -r deployment/capacitor/android/app/build/outputs/apk/debug/app-debug.apk",
"phone": "bun run apk:debug && bun run apk:install", "phone": "bun run apk:debug && bun run apk:install",
"shell": "node internal/install-shell.mjs --no-token", "shell": "node internal/install-shell.mjs --no-token",
@ -156,6 +156,7 @@
"check:gallery-embed:js": "node internal/check-research-gallery-embed.mjs --mode js", "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:iframe": "node internal/check-research-gallery-embed.mjs --mode iframe",
"check:gallery-embed:maplibre": "node internal/check-research-gallery-embed.mjs --provider maplibre", "check:gallery-embed:maplibre": "node internal/check-research-gallery-embed.mjs --provider maplibre",
"check:gallery-embed:live-cors": "node internal/check-research-gallery-embed.mjs --live-cors",
"check:prayer-walk-planner": "node internal/check-prayer-walk-planner.mjs", "check:prayer-walk-planner": "node internal/check-prayer-walk-planner.mjs",
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs", "check:spa-tab-order": "node internal/check-spa-tab-order.mjs",
"check:backlog-status": "node internal/check-backlog-status-enum.mjs", "check:backlog-status": "node internal/check-backlog-status-enum.mjs",
@ -192,7 +193,9 @@
"check:tree-weight": "node internal/check-published-tree-weight.mjs", "check:tree-weight": "node internal/check-published-tree-weight.mjs",
"check:i18n-never-translated": "node internal/check-i18n-never-translated.mjs", "check:i18n-never-translated": "node internal/check-i18n-never-translated.mjs",
"check:embed-base": "node internal/check-embed-base-resolution.mjs", "check:embed-base": "node internal/check-embed-base-resolution.mjs",
"check:composed-filters": "node internal/check-gospel-composed-filters.mjs" "check:composed-filters": "node internal/check-gospel-composed-filters.mjs",
"app:start-page": "node internal/set-app-start-page.mjs",
"check:app-start-page": "node internal/set-app-start-page.mjs --check"
}, },
"//scripts": { "//scripts": {
"_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.", "_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.",
@ -290,7 +293,8 @@
"check:prayer-walk-planner": "the Prayer Walk Planner is a full-page tool that prints a real walking map: the map canvas fills the page below the toolbar in its own leaf AND inside the Research Gallery at 1280x800 and 390x844 (the gallery once collapsed it to 76px / 0px); a real zone (Balat, Istanbul) renders a >=2000px print map that is not blank, saved to OUT=<dir> so a human can read it; ?lang=es gives a Spanish toolbar; 0 page errors. Run inside the shared browser locks. Exit 2 = SKIP.", "check:prayer-walk-planner": "the Prayer Walk Planner is a full-page tool that prints a real walking map: the map canvas fills the page below the toolbar in its own leaf AND inside the Research Gallery at 1280x800 and 390x844 (the gallery once collapsed it to 76px / 0px); a real zone (Balat, Istanbul) renders a >=2000px print map that is not blank, saved to OUT=<dir> so a human can read it; ?lang=es gives a Spanish toolbar; 0 page errors. Run inside the shared browser locks. Exit 2 = SKIP.",
"check:composed-filters": "CELL C — the gospel map composed above its own tables. Asserts the composite keeps the reader INSIDE it: \"Every resource in <language>\" must be a <button> that filters in place, not an <a> that navigates away and throws out the map the reader just positioned. Checks the tag, that the URL did not change, that the table NARROWED (row count down but not to zero), that the pin count survived, and that nothing 4xx'd. Picks a tier-2 pin from the live source — the first version opened pin 0, a group with nothing found, and failed a working composite on an empty table. Browser gate: run under both rendezvous locks, product lock first.", "check:composed-filters": "CELL C — the gospel map composed above its own tables. Asserts the composite keeps the reader INSIDE it: \"Every resource in <language>\" must be a <button> that filters in place, not an <a> that navigates away and throws out the map the reader just positioned. Checks the tag, that the URL did not change, that the table NARROWED (row count down but not to zero), that the pin count survived, and that nothing 4xx'd. Picks a tier-2 pin from the live source — the first version opened pin 0, a group with nothing found, and failed a working composite on an empty table. Browser gate: run under both rendezvous locks, product lock first.",
"check:vision-card-numbers": "the DOXA Vision Card must show the numbers the DATA gives: independently recomputes all four funnel steps (and DOXA-in-frontier) from people_groups.csv + the saved DOXA list, compares with vision-funnel.json, and with --url also with the numbers painted in the card. A typed-in number, a stale funnel file or a rendering bug fails it.", "check:vision-card-numbers": "the DOXA Vision Card must show the numbers the DATA gives: independently recomputes all four funnel steps (and DOXA-in-frontier) from people_groups.csv + the saved DOXA list, compares with vision-funnel.json, and with --url also with the numbers painted in the card. A typed-in number, a stale funnel file or a rendering bug fails it.",
"check:map-embed-cells": "PRODUCTION-MATRIX cells B (iframe) and C (script embed) for the seven Research Gallery maps on a genuinely foreign origin (tree on 127.0.0.1, partner page on localhost). C passes the HOST a distinct token and fails if any Mapbox request uses another. --only bundle/profile,… --cell B|C." "check:map-embed-cells": "PRODUCTION-MATRIX cells B (iframe) and C (script embed) for the seven Research Gallery maps on a genuinely foreign origin (tree on 127.0.0.1, partner page on localhost). C passes the HOST a distinct token and fails if any Mapbox request uses another. --only bundle/profile,… --cell B|C.",
"check:gallery-embed:live-cors": "the Research Gallery on a foreign page under the LIVE static layer's CORS rule (Access-Control-Allow-Origin on files inside the tree's folders, NOT on the root manifest.json, measured on nuxtbase): it must still boot every tab, by reading the CORS-readable manifest copy generate-tree writes beside its bundle."
}, },
"devDependencies": { "devDependencies": {
"@capacitor/android": "^7.6.9", "@capacitor/android": "^7.6.9",

View file

@ -92,7 +92,11 @@ let offScroll = null
// assetsBase never reached the consumer. // assetsBase never reached the consumer.
// Forwarded by KEY LIST, never by spreading the whole host config, so a pane's own // Forwarded by KEY LIST, never by spreading the whole host config, so a pane's own
// `profile`/`instanceId` cannot be overwritten (pane.config is spread last). // `profile`/`instanceId` cannot be overwritten (pane.config is spread last).
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'prayBaseUrl', 'locale', 'lang', 'isDark', 'qa'] // ★ 'provider' MUST travel. A composite that forwards the token but not the provider hands a
// partner who asked for MapLibre a Mapbox map in every nested pane -- the pane reads
// provider as undefined and falls back. Same shape of bug as the missing 'urlSync' here
// before it: the inner map cannot honour a key the shell never passed down.
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'prayBaseUrl', 'locale', 'lang', 'isDark', 'qa', 'provider']
function paneElement(pane) { function paneElement(pane) {
const el = document.createElement(TAG) const el = document.createElement(TAG)

View file

@ -1116,9 +1116,24 @@ export default defineComponent({
// run finds the real manifest instead of going dark, and a production run is unchanged // run finds the real manifest instead of going dark, and a production run is unchanged
// (candidate 1 answers, candidate 2 is never requested). // (candidate 1 answers, candidate 2 is never requested).
loadManifest() { loadManifest() {
const candidates = [this.resolve('manifest.json')] // Each candidate is { url, root }: WHERE to read the manifest, and the tree ROOT its
// tree-relative scriptUrls resolve against (normally the manifest's own directory).
const rootUrl = this.resolve('manifest.json')
const dirOf = (u) => { try { return new URL(u, location.href).href.replace(/[^/]*$/, '') } catch { return '' } }
const root = { url: rootUrl, root: dirOf(rootUrl) }
// ★ THE CORS-READABLE COPY (generate-tree writes it beside this bundle, byte-identical).
// A script embed on a partner page FETCHES the manifest cross-origin, and the live static layer
// sends Access-Control-Allow-Origin only on paths INSIDE a folder of the tree — every bundle file
// and assets/ carry it, the ROOT manifest.json does not (measured 2026-09-25 on nuxtbase: the
// root 200 without it, doxa-maps-spa/<anything> with it, even a 404). So on a foreign page the
// copy is read FIRST (no CORS error in the host's console) and the root second; same-origin keeps
// the root first. Script URLs still resolve against the TREE ROOT, never the copy's folder.
const copy = { url: this.resolve('doxa-maps-spa/manifest.json'), root: root.root }
let crossOrigin = false
try { crossOrigin = new URL(rootUrl, location.href).origin !== location.origin } catch { /* relative = same */ }
const candidates = crossOrigin ? [copy, root] : [root, copy]
const devFallback = '/public/doxa-maps/manifest.json' const devFallback = '/public/doxa-maps/manifest.json'
if (candidates.indexOf(devFallback) === -1) candidates.push(devFallback) if (!candidates.some(c => c.url === devFallback)) candidates.push({ url: devFallback, root: dirOf(devFallback) })
const tried = [] const tried = []
const attempt = (i) => { const attempt = (i) => {
if (i >= candidates.length) { if (i >= candidates.length) {
@ -1128,18 +1143,18 @@ export default defineComponent({
' — every tab except Big Picture is composed from it, so their panes cannot mount.') ' — every tab except Big Picture is composed from it, so their panes cannot mount.')
return return
} }
const url = candidates[i] const { url, root: treeRoot } = candidates[i]
tried.push(url) tried.push(url)
return fetch(url, { credentials: 'omit' }) return fetch(url, { credentials: 'omit' })
.then(r => { if (!r || !r.ok) throw new Error(url + ' → HTTP ' + (r ? r.status : '?')); return r.json() }) .then(r => { if (!r || !r.ok) throw new Error(url + ' → HTTP ' + (r ? r.status : '?')); return r.json() })
.then(m => { .then(m => {
if (!m) throw new Error(url + ' → empty') if (!m) throw new Error(url + ' → empty')
this.manifestError = '' this.manifestError = ''
// Anchor every tree-relative scriptUrl on the directory this manifest came from. // Anchor every tree-relative scriptUrl on the TREE ROOT this candidate names (the
// new URL() against location resolves '../' and rooted forms alike, so a caller- // manifest's own directory, or the root for the copy). new URL() against location
// supplied treeBase, a built-tree path and the dev fallback all normalise here. // resolves '../' and rooted forms alike, so a caller-supplied treeBase, a built-tree
try { this.manifestBase = new URL(url, location.href).href.replace(/[^/]*$/, '') } // path and the dev fallback all normalise here.
catch { this.manifestBase = '' } this.manifestBase = treeRoot || ''
this.applyManifest(m) this.applyManifest(m)
}) })
.catch(() => attempt(i + 1)) .catch(() => attempt(i + 1))

View file

@ -136,7 +136,11 @@ const paneEls = {}
// Forwarded by KEY LIST, not by spreading the whole host config: a pane's own `profile` / // Forwarded by KEY LIST, not by spreading the whole host config: a pane's own `profile` /
// `instanceId` must NOT be overwritten by the host's, which is what makes a pane a distinct // `instanceId` must NOT be overwritten by the host's, which is what makes a pane a distinct
// view at all. Anything a pane declares still wins (pane.config is spread last). // view at all. Anything a pane declares still wins (pane.config is spread last).
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'locale', 'lang', 'isDark', 'qa'] // ★ 'provider' MUST travel. A composite that forwards the token but not the provider hands a
// partner who asked for MapLibre a Mapbox map in every nested pane -- the pane reads
// provider as undefined and falls back. Same shape of bug as the missing 'urlSync' here
// before it: the inner map cannot honour a key the shell never passed down.
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'locale', 'lang', 'isDark', 'qa', 'provider']
function paneElement(pane) { function paneElement(pane) {
const el = document.createElement(TAG) const el = document.createElement(TAG)

View file

@ -883,6 +883,20 @@ useShadowStyles(
// third wrapped to its own row -- 579/579/1200 instead of their 372/372/372. DX-life needs // third wrapped to its own row -- 579/579/1200 instead of their 372/372/372. DX-life needs
// no such reset because Bebas Neue is far narrower than any fallback we can load. // no such reset because Bebas Neue is far narrower than any fallback we can load.
'.rd-switcher > *{ flex-grow:1; flex-basis:calc((700px - 100%) * 999); min-width:0; }' + '.rd-switcher > *{ flex-grow:1; flex-basis:calc((700px - 100%) * 999); min-width:0; }' +
// The last rule of their composition/_switcher.scss, ported for completeness rather than for
// effect: at six or more children every one takes a full row instead of sharing. MEASURED in the
// rendered page, not counted in the template: 3 direct children, widths 372/372/372 at 1440 --
// the same three goal cards their RegionDetail.vue lays out. (A static tag-count of this block
// said 4 and was wrong; the runtime count is the one that governs a :nth-last-child selector.)
// So the threshold of 6 is out of reach and the selector matches nothing today. It is carried
// because the brief is
// an exact clone and because a fifth or sixth goal card would otherwise silently diverge from
// their layout -- a one-line rule now, or a puzzle later about why six cards behave differently
// here than on doxa.life. The other two rules in that file are NOT ported and that is measured,
// not an oversight: .switcher-item{display:flex} never applies because their switcher's children
// are `.stack | card`, not `.switcher-item`, and .three-part-switcher is a different component
// that the regions page does not use.
'.rd-switcher > :nth-last-child(n + 6), .rd-switcher > :nth-last-child(n + 6) ~ *{ flex-basis:100%; }' +
// ★ A CONSEQUENCE OF THE FONT SUBSTITUTION, not a departure from their design. // ★ A CONSEQUENCE OF THE FONT SUBSTITUTION, not a departure from their design.
// Their h2 is clamp(--font-size-2xl, 6vw, --font-size-4xl) and resolves to var(--font-size-4xl) at // Their h2 is clamp(--font-size-2xl, 6vw, --font-size-4xl) and resolves to var(--font-size-4xl) at
// 1440 -- verified live. In Bebas that fits their 372.25px card (261.55px of inner width). // 1440 -- verified live. In Bebas that fits their 372.25px card (261.55px of inner width).

View file

@ -91,7 +91,11 @@ let offScroll = null
// It is the same lesson as the cap-4 asset-path one this file already records: page-scoped config // It is the same lesson as the cap-4 asset-path one this file already records: page-scoped config
// has to be forwarded explicitly. Note the filter below keeps `false`, which is the value that // has to be forwarded explicitly. Note the filter below keeps `false`, which is the value that
// matters here -- it rejects undefined, null and '' only. // matters here -- it rejects undefined, null and '' only.
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'prayBaseUrl', 'locale', 'lang', 'languageCode', 'isDark', 'qa', 'pinHint', 'urlSync'] // ★ 'provider' MUST travel. A composite that forwards the token but not the provider hands a
// partner who asked for MapLibre a Mapbox map in every nested pane -- the pane reads
// provider as undefined and falls back. Same shape of bug as the missing 'urlSync' here
// before it: the inner map cannot honour a key the shell never passed down.
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'prayBaseUrl', 'locale', 'lang', 'languageCode', 'isDark', 'qa', 'pinHint', 'urlSync', 'provider']
function paneElement(pane) { function paneElement(pane) {
const el = document.createElement(TAG) const el = document.createElement(TAG)

View file

@ -228,12 +228,12 @@ export default {
// axes; it stays below them as a flat utility list (`axis:null` = uncoloured), so the four // axes; it stays below them as a flat utility list (`axis:null` = uncoloured), so the four
// trees read as the stack the Driver asked for rather than as five peers. // trees read as the stack the Driver asked for rather than as five peers.
facetRail: [ facetRail: [
{ id: 'people', title: 'People', axis: 'people', drill: ['affinity_bloc', 'cluster', 'people_group'], tree: 'affinity', topN: [30, 40, 60] }, { id: 'people', get title() { return tStatic('dashboard.people') || 'People' }, axis: 'people', drill: ['affinity_bloc', 'cluster', 'people_group'], tree: 'affinity', topN: [30, 40, 60] },
{ id: 'place', title: 'Place', axis: 'place', drill: ['wagf_region', 'wagf_block', 'country_code'] }, { id: 'place', get title() { return tStatic('dashboard.place') || 'Place' }, axis: 'place', drill: ['wagf_region', 'wagf_block', 'country_code'] },
{ id: 'language', title: 'Language', axis: 'language', drill: ['language_family', 'primary_language'] }, { id: 'language', get title() { return tStatic('dashboard.language') || 'Language' }, axis: 'language', drill: ['language_family', 'primary_language'] },
// Religion was the odd flat axis; it is now the 2-tier tree the data always supported // Religion was the odd flat axis; it is now the 2-tier tree the data always supported
// (family → religion). See the `religion_family` facet above. // (family → religion). See the `religion_family` facet above.
{ id: 'religion', title: 'Religion', axis: 'religion', drill: ['religion_family', 'religion'], topN: [12, 24] }, { id: 'religion', get title() { return tStatic('dashboard.religion') || 'Religion' }, axis: 'religion', drill: ['religion_family', 'religion'], topN: [12, 24] },
// ★ THE 'Status' SECTION IS REMOVED ON DRIVER INSTRUCTION 2026-09-23 ("dont add the engable // ★ THE 'Status' SECTION IS REMOVED ON DRIVER INSTRUCTION 2026-09-23 ("dont add the engable
// filter or status filter"). It rendered engagement_status as a rail section — 2 rows, Unengaged // filter or status filter"). It rendered engagement_status as a rail section — 2 rows, Unengaged
// 2,081 / Engaged 27 — which is a status filter by another name. // 2,081 / Engaged 27 — which is a status filter by another name.
@ -415,8 +415,8 @@ export default {
// slice click narrows the DASHBOARD but not the grid below -- documented behaviour, and for // slice click narrows the DASHBOARD but not the grid below -- documented behaviour, and for
// a glanceable donut that is the right trade. The full breakdown still lives in the facet // a glanceable donut that is the right trade. The full breakdown still lives in the facet
// rail's Religion tree for anyone who needs Sunni vs Folk. // rail's Religion tree for anyone who needs Sunni vs Folk.
{ id: 'religion', type: 'donut', facet: 'religion_family', title: 'Religion', topN: 8 }, { id: 'religion', type: 'donut', facet: 'religion_family', get title() { return tStatic('dashboard.religion') || 'Religion' }, topN: 8 },
{ id: 'region', type: 'donut', facet: 'wagf_region', title: 'Region', topN: 9 }, { id: 'region', type: 'donut', facet: 'wagf_region', get title() { return tStatic('dashboard.region') || 'Region' }, topN: 9 },
{ id: 'country', type: 'bar', facet: 'country_code', title: 'Top countries', topN: 12 }, { id: 'country', type: 'bar', facet: 'country_code', title: 'Top countries', topN: 12 },
], ],

View file

@ -728,7 +728,7 @@ useShadowStyles(`
<button v-if="r.hasChildren" type="button" class="fr-caret-btn" :class="{ open: r.expanded }" <button v-if="r.hasChildren" type="button" class="fr-caret-btn" :class="{ open: r.expanded }"
:data-expand="r.facet" :data-expand-value="r.value" :data-expand="r.facet" :data-expand-value="r.value"
:aria-expanded="r.expanded ? 'true' : 'false'" :aria-expanded="r.expanded ? 'true' : 'false'"
:aria-label="(r.expanded ? 'Collapse ' : 'Expand ') + r.label"><span class="fr-caret-gl" aria-hidden="true">▸</span></button> :aria-label="(r.expanded ? L('collapse', 'Collapse') : L('expand', 'Expand')) + ' ' + r.label"><span class="fr-caret-gl" aria-hidden="true">▸</span></button>
<span v-else class="fr-caret fr-caret-leaf" aria-hidden="true"></span> <span v-else class="fr-caret fr-caret-leaf" aria-hidden="true"></span>
<!-- swatch: the cap-3 map's colour for this node. Display-only — never part of the <!-- swatch: the cap-3 map's colour for this node. Display-only — never part of the
filter value. Omitted for uncoloured sections and for the 'Other' bucket. --> filter value. Omitted for uncoloured sections and for the 'Other' bucket. -->

View file

@ -1569,6 +1569,14 @@ useShadowStyles(`
.rd-proj{ position:absolute; right:8px; top:8px; z-index:3; font-size:11px; font-weight:700; cursor:pointer; user-select:none; .rd-proj{ position:absolute; right:8px; top:8px; z-index:3; font-size:11px; font-weight:700; cursor:pointer; user-select:none;
background:#1a1a2ecc; color:#fff; border:1px solid #ffffff33; border-radius:6px; padding:4px 9px; backdrop-filter:blur(4px); } background:#1a1a2ecc; color:#fff; border:1px solid #ffffff33; border-radius:6px; padding:4px 9px; backdrop-filter:blur(4px); }
.rd-proj:hover{ background:#1a1a2e; } .rd-proj:hover{ background:#1a1a2e; }
/* ★ TOUCH TARGET: 24px, STATED. Measured on a phone (390x844, pointer:coarse) this rendered
74x23 — one pixel under WCAG 2.5.8 AA. The fix is to name the 24, not to add padding: the
height is padding PLUS a line box the renderer derives from the font, and padding arithmetic
lands at 23.7px, which rounds to the same failure. Mouse sizing is untouched; only a coarse
pointer gets the floor. (Found by regions' phone sweep across every map.) */
@media (pointer: coarse) {
.rd-proj{ min-height:24px; display:inline-flex; align-items:center; }
}
/* Zoom cluster. MapToolbar defaults to top:6px on the inline-end edge, which is exactly where /* Zoom cluster. MapToolbar defaults to top:6px on the inline-end edge, which is exactly where
the Globe/Flat pill already sits, so drop this instance below it. 34px clears the pill the Globe/Flat pill already sits, so drop this instance below it. 34px clears the pill
(top:8px + roughly 22px tall) with a small breathing gap. (top:8px + roughly 22px tall) with a small breathing gap.

File diff suppressed because one or more lines are too long

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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <all-people-groups> custom element. --> <!-- Bundle IIFE — registers the <all-people-groups> custom element. -->
<script src="../all-people-groups.js" integrity="sha384-jvdsSW2vNF+xKoQNtlAy4b8Oodc5avQOw9zn8IjiERCi/mCKMGJTEEUmtMgiJ4wF" crossorigin="anonymous"></script> <script src="../all-people-groups.js" integrity="sha384-rsq5CBTh6pLStfLv/4PHCQ4uBEQvgFIUIn459pPy1dlA8jIBrcQtFK5sKY1t2gHg" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -37,7 +37,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. --> <!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../../big-picture.js" integrity="sha384-MIotyiMkAcAJizhxGzzi0k3Vjd4souqd8URA5CoGwZdBTmdFeXLxdsOQY0A/7OKf" crossorigin="anonymous"></script> <script src="../../big-picture.js" integrity="sha384-AGFs5KcwxgY8QHHMGDyC9H3eZXCrL32t2mMrBXkTV7IdCqdcY9vHOZymJ7oUUfq2" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. --> <!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../../big-picture.js" integrity="sha384-MIotyiMkAcAJizhxGzzi0k3Vjd4souqd8URA5CoGwZdBTmdFeXLxdsOQY0A/7OKf" crossorigin="anonymous"></script> <script src="../../big-picture.js" integrity="sha384-AGFs5KcwxgY8QHHMGDyC9H3eZXCrL32t2mMrBXkTV7IdCqdcY9vHOZymJ7oUUfq2" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. --> <!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../big-picture.js" integrity="sha384-MIotyiMkAcAJizhxGzzi0k3Vjd4souqd8URA5CoGwZdBTmdFeXLxdsOQY0A/7OKf" crossorigin="anonymous"></script> <script src="../big-picture.js" integrity="sha384-AGFs5KcwxgY8QHHMGDyC9H3eZXCrL32t2mMrBXkTV7IdCqdcY9vHOZymJ7oUUfq2" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. --> <!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-DAdglvXNlk5pszrj+32A6kSxWJeFOxYHnP2TCsxmxp8oNAaIpFiFaNHJr6pFT9hX" crossorigin="anonymous"></script> <script src="../cities-internet.js" integrity="sha384-tWFKP5lpuEIney1vkcEFXPPhlr/fgu3siY7p7J25oMxBOedxS/MJjwKD+FMKnsA4" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. --> <!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-DAdglvXNlk5pszrj+32A6kSxWJeFOxYHnP2TCsxmxp8oNAaIpFiFaNHJr6pFT9hX" crossorigin="anonymous"></script> <script src="../cities-internet.js" integrity="sha384-tWFKP5lpuEIney1vkcEFXPPhlr/fgu3siY7p7J25oMxBOedxS/MJjwKD+FMKnsA4" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. --> <!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-DAdglvXNlk5pszrj+32A6kSxWJeFOxYHnP2TCsxmxp8oNAaIpFiFaNHJr6pFT9hX" crossorigin="anonymous"></script> <script src="../cities-internet.js" integrity="sha384-tWFKP5lpuEIney1vkcEFXPPhlr/fgu3siY7p7J25oMxBOedxS/MJjwKD+FMKnsA4" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. --> <!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-0KSJMk7mxBXLvGx22IXjbGnqPD9uIV15iwF+zTJl5Evo8M6m0Zf8BTwfI7b3e5cu" crossorigin="anonymous"></script> <script src="../country-posters.js" integrity="sha384-P1ECIwoWVV0RxJB1K0ASPPCojtDw7ygyE1Cz3Qy1GeVP0kAHnzR8gzguOLxMxhQP" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. --> <!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-0KSJMk7mxBXLvGx22IXjbGnqPD9uIV15iwF+zTJl5Evo8M6m0Zf8BTwfI7b3e5cu" crossorigin="anonymous"></script> <script src="../country-posters.js" integrity="sha384-P1ECIwoWVV0RxJB1K0ASPPCojtDw7ygyE1Cz3Qy1GeVP0kAHnzR8gzguOLxMxhQP" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. --> <!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-0KSJMk7mxBXLvGx22IXjbGnqPD9uIV15iwF+zTJl5Evo8M6m0Zf8BTwfI7b3e5cu" crossorigin="anonymous"></script> <script src="../country-posters.js" integrity="sha384-P1ECIwoWVV0RxJB1K0ASPPCojtDw7ygyE1Cz3Qy1GeVP0kAHnzR8gzguOLxMxhQP" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. --> <!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../../dashboard-plus-grid.js" integrity="sha384-f4Hc989WsNYgAn/eTyG/tfJ9sMJYbTY9D1P6oaZVvvxyU2Bm6rbU/P9Jh4fN8lcX" crossorigin="anonymous"></script> <script src="../../dashboard-plus-grid.js" integrity="sha384-1v751sDfxZg0uCHzKVXM5dYBxPBdB0xeJTb+lBwUeVy5fQX3++U+7ei1NZbxjD8r" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. --> <!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../dashboard-plus-grid.js" integrity="sha384-f4Hc989WsNYgAn/eTyG/tfJ9sMJYbTY9D1P6oaZVvvxyU2Bm6rbU/P9Jh4fN8lcX" crossorigin="anonymous"></script> <script src="../dashboard-plus-grid.js" integrity="sha384-1v751sDfxZg0uCHzKVXM5dYBxPBdB0xeJTb+lBwUeVy5fQX3++U+7ei1NZbxjD8r" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <demo-map> custom element. --> <!-- Bundle IIFE — registers the <demo-map> custom element. -->
<script src="../demo-map.js" integrity="sha384-kk/cmUlZOPJKofRPRZ6xB1SPUnPewaFZPSNBVGWjWj10LPsiYA50/Q/D+zvl/nAz" crossorigin="anonymous"></script> <script src="../demo-map.js" integrity="sha384-h5/KFpBTK/gix8IyOdm4xLGRYGFGeuqXiuK1u1t+HzjpVUMda91x21fC/NmrWDs3" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <demo-map> custom element. --> <!-- Bundle IIFE — registers the <demo-map> custom element. -->
<script src="../../demo-map.js" integrity="sha384-kk/cmUlZOPJKofRPRZ6xB1SPUnPewaFZPSNBVGWjWj10LPsiYA50/Q/D+zvl/nAz" crossorigin="anonymous"></script> <script src="../../demo-map.js" integrity="sha384-h5/KFpBTK/gix8IyOdm4xLGRYGFGeuqXiuK1u1t+HzjpVUMda91x21fC/NmrWDs3" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. --> <!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
<script src="../../doxa-maps-spa.js" integrity="sha384-0x9kTbgn/VFIqzPuAhwj4FHZE4+SrcZu798Cyxc1o/8VvS9qBFQaaZW/Oonj5K7s" crossorigin="anonymous"></script> <script src="../../doxa-maps-spa.js" integrity="sha384-SSR99fy7d4D0pg07vxksTDEXhf7Saky5hhVJCkPlQ1O45Il0/VkW4cj/yFar6QaP" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. --> <!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
<script src="../doxa-maps-spa.js" integrity="sha384-0x9kTbgn/VFIqzPuAhwj4FHZE4+SrcZu798Cyxc1o/8VvS9qBFQaaZW/Oonj5K7s" crossorigin="anonymous"></script> <script src="../doxa-maps-spa.js" integrity="sha384-SSR99fy7d4D0pg07vxksTDEXhf7Saky5hhVJCkPlQ1O45Il0/VkW4cj/yFar6QaP" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because it is too large Load diff

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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. --> <!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../doxa-research-map.js" integrity="sha384-ZblpRN2LkrkSwRQwkcgAW6HliqJHNC6UroZfApRBE4LufTWKxVHYvhgXo7CHnMlE" crossorigin="anonymous"></script> <script src="../doxa-research-map.js" integrity="sha384-xw4YfIP9k6/ifT585HK5MNhV3S20HJ+plQk1BHQpQ1iPVglJweZQqgBsqaLb/Ke1" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. --> <!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../../doxa-research-map.js" integrity="sha384-ZblpRN2LkrkSwRQwkcgAW6HliqJHNC6UroZfApRBE4LufTWKxVHYvhgXo7CHnMlE" crossorigin="anonymous"></script> <script src="../../doxa-research-map.js" integrity="sha384-xw4YfIP9k6/ifT585HK5MNhV3S20HJ+plQk1BHQpQ1iPVglJweZQqgBsqaLb/Ke1" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. --> <!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-UoEAocfZB2vPv7YfmlzcpOe/sdh1FV8XeZ8UdT6xIhmVia1arbnT+AG4m3vcpAd2" crossorigin="anonymous"></script> <script src="../../doxa-simple-map.js" integrity="sha384-bHZzOpmlz2zEoz+T3Sb3u/qMZgxnOZGsqQUSycHCK+oZql/afP15VSV0QbEwNuOS" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. --> <!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-UoEAocfZB2vPv7YfmlzcpOe/sdh1FV8XeZ8UdT6xIhmVia1arbnT+AG4m3vcpAd2" crossorigin="anonymous"></script> <script src="../../doxa-simple-map.js" integrity="sha384-bHZzOpmlz2zEoz+T3Sb3u/qMZgxnOZGsqQUSycHCK+oZql/afP15VSV0QbEwNuOS" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. --> <!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../doxa-simple-map.js" integrity="sha384-UoEAocfZB2vPv7YfmlzcpOe/sdh1FV8XeZ8UdT6xIhmVia1arbnT+AG4m3vcpAd2" crossorigin="anonymous"></script> <script src="../doxa-simple-map.js" integrity="sha384-bHZzOpmlz2zEoz+T3Sb3u/qMZgxnOZGsqQUSycHCK+oZql/afP15VSV0QbEwNuOS" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // 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> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. --> <!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-UoEAocfZB2vPv7YfmlzcpOe/sdh1FV8XeZ8UdT6xIhmVia1arbnT+AG4m3vcpAd2" crossorigin="anonymous"></script> <script src="../../doxa-simple-map.js" integrity="sha384-bHZzOpmlz2zEoz+T3Sb3u/qMZgxnOZGsqQUSycHCK+oZql/afP15VSV0QbEwNuOS" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,11 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. --> <!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<<<<<<< HEAD
<script src="../gospel-resources.js" integrity="sha384-WsPPwgoFC9vy1BPi/sT71i/Tl6OoGupGHFMd8j/iMXhyE1VfbtHrHtL+4CTv5Iul" crossorigin="anonymous"></script> <script src="../gospel-resources.js" integrity="sha384-WsPPwgoFC9vy1BPi/sT71i/Tl6OoGupGHFMd8j/iMXhyE1VfbtHrHtL+4CTv5Iul" crossorigin="anonymous"></script>
=======
<script src="../gospel-resources.js" integrity="sha384-SQD84HjvCJ8hyWy/4iHK/Ddo9KGh1JuilIxbn/hvTJuOA0rKVo4ZvJjcTEyur2rg" crossorigin="anonymous"></script>
>>>>>>> origin/main
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,11 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. --> <!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<<<<<<< HEAD
<script src="../gospel-resources.js" integrity="sha384-WsPPwgoFC9vy1BPi/sT71i/Tl6OoGupGHFMd8j/iMXhyE1VfbtHrHtL+4CTv5Iul" crossorigin="anonymous"></script> <script src="../gospel-resources.js" integrity="sha384-WsPPwgoFC9vy1BPi/sT71i/Tl6OoGupGHFMd8j/iMXhyE1VfbtHrHtL+4CTv5Iul" crossorigin="anonymous"></script>
=======
<script src="../gospel-resources.js" integrity="sha384-SQD84HjvCJ8hyWy/4iHK/Ddo9KGh1JuilIxbn/hvTJuOA0rKVo4ZvJjcTEyur2rg" crossorigin="anonymous"></script>
>>>>>>> origin/main
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,11 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. --> <!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
<<<<<<< HEAD
<script src="../gospel-resources.js" integrity="sha384-WsPPwgoFC9vy1BPi/sT71i/Tl6OoGupGHFMd8j/iMXhyE1VfbtHrHtL+4CTv5Iul" crossorigin="anonymous"></script> <script src="../gospel-resources.js" integrity="sha384-WsPPwgoFC9vy1BPi/sT71i/Tl6OoGupGHFMd8j/iMXhyE1VfbtHrHtL+4CTv5Iul" crossorigin="anonymous"></script>
=======
<script src="../gospel-resources.js" integrity="sha384-SQD84HjvCJ8hyWy/4iHK/Ddo9KGh1JuilIxbn/hvTJuOA0rKVo4ZvJjcTEyur2rg" crossorigin="anonymous"></script>
>>>>>>> origin/main
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

Some files were not shown because too many files have changed in this diff Show more