Merge remote-tracking branch 'origin/development' into regions/pane-config-land

# Conflicts:
#	package.json
This commit is contained in:
Doxa Maps 2026-09-29 19:12:46 +00:00
commit 656503a28d
33 changed files with 496 additions and 2355 deletions

View file

@ -43,8 +43,11 @@ jobs:
# (drop-portability, inline-parse, bundles-shipped and the rest). A guard
# failure fails the deploy, which is the point of having them.
- name: Build
# BUILD_ALLOW=1: build-lock.sh refuses an unannounced build (exit 75) so two agents on one box
# cannot build at once; a CI runner is the only builder on its machine, so it opts in.
env:
MAPBOX_TOKEN: ${{ secrets.MAPBOX_TOKEN }}
BUILD_ALLOW: '1'
run: bun run build
# Fail loudly rather than publishing a site whose maps cannot draw.

3
.gitignore vendored
View file

@ -155,3 +155,6 @@ data-private/
# gated bundles (gatedProfiles) build here, never into public/ (vite.config.js)
/.gated/
/deployment/src-tauri/binaries/
# vite writes these beside the config while it bundles it; never commit one (it embeds absolute paths)
vite.config.js.timestamp-*

View file

@ -64,6 +64,59 @@ capabilities, three outputs, one shared library, one build.
| **Shared library, private plugins** — reuse without breaking each other | `bun run check:library-index` |
| **One agent per plugin, one shepherd, one working `main`** | [agent swarm](#build-it-with-an-agent-swarm) |
## For vibe coders: build, open the Studio, embed a map
**1. Open the Plugin Studio** (every map, live, updating as you or your agent edit):
```bash
bun install --frozen-lockfile
bun run dev # open the address it prints: that page is the Plugin Studio
```
**2. Build.** The built maps are already in the repo. After a change, rebuild just that map, or all of them:
```bash
BUNDLE=<map> bun x vite build # one map (seconds)
BUILD_ALLOW=1 bun run build # every map (~4 min); the output is public/dx-maps/
```
When the DX Life site's repo sits next to this one (`../doxa-life`), the full build also copies the maps into
its `public/`. That is the one path set in `vite.doxa-maps-build-config.json` (`dropin.dest`); nothing else on
your computer is searched. No site there: the step says so and skips. `DOXA_DROPIN=off` turns it off, and
`bun run dropin` copies on demand.
**3. Embed a map.** Every map has its own embed script: its `embed.script` in `public/dx-maps/manifest.json` (an
`embed.iframe` too). A snippet is two parts, the map's script and its tag:
```html
<script src="/dx-maps/doxa-simple-map/doxa-simple-map.js" integrity="sha384-…" crossorigin="anonymous"></script>
<doxa-map profile-config='{"profile":"doxa-simple-map","assetsBase":"/dx-maps/assets"}'></doxa-map>
```
On DX Life, the maps are served from its own `public/dx-maps/`, so a page loads `/dx-maps/<map>/<map>.js` and
places the map's tag. Copy each snippet from the manifest: its `integrity` hash changes with every build. More:
[the three routes](#where-the-folder-goes-three-routes) and [how a page shows a map](#how-a-page-shows-a-map).
## For agents: what this repo is for
This repo builds **mobilization maps**: maps that help disciples, churches and leaders reach their city and the
people groups of the world. When you work here, the goal is one of these:
| Goal | The production maps that serve it |
|---|---|
| **Reach a city** | Cities with Internet (`cities-internet`); Regions (`regions-dashboard`) and the country map (`country-map`) |
| **Mobilize for people groups** | Big Picture (`dashboard-plus-grid`); Research Map + Grid (`research-map-plus-grid`) |
| **Plan outreach** | Gospel Resources (`gospel-resources`): what exists in each language; Regions, by country |
| **Pray: packets and prayer resources** | UPG-100 (`my-upg-100-list`): prayer packets; Prayer Progress (`doxa-simple-map`) |
- **Start with the skill:** [`skills/plugin-designer`](./skills/plugin-designer) teaches the patterns, the shared
library and the checks.
- **One plugin per folder** in `plugin-designs/`; shared code goes in `library/` only if it stays reusable for
every other plugin.
- **Land on `development`**, one change at a time through the lock, with `bun run gates:pre` passing. `main` is
production and moves only on a publish.
- These 8 maps are in production. The experimental maps are frozen: leave them as they are.
*Click any ▸ to read more. Everything below is folded so you can scroll the pictures first.*
---
@ -183,9 +236,9 @@ embed just the one you need.
|---|---|---|
| a **translator** (or an AI translation pipeline) | Gospel Resources (map, list, data tables) | which people groups have no Scripture, film or audio in their language, and which bridge language already reaches them |
| **media to movements** | Cities with Internet · Gospel × Internet | where unreached people groups live with internet access, country by country and down to province where data exists |
| a **catalyst** or mobilizer | Big Picture · Simple Research · Regions · Gospel × Internet | filter the 2,107 groups by region, religion, language and engagement; **Export CSV** or **Export this selection**; open a group's **Full Profile**; find **gospel resources** for the people you are reaching |
| a **catalyst** or mobilizer | Big Picture · Prayer Walk Planner · UPG 100 List · Gospel × Internet | find **gospel resources** for the people you are reaching real time, build prayer walking maps for real outreach, as you prayer walk each key zone in your cities of choice, find indigenous disciples, groups, and leaders mobilize them each to have prayer packets, cards and posters to blanket all the people groups in prayer as a prayer mobilizer catalyst |
| **praying** | Prayer Progress · UPG-100 List | where prayer is covered and where it is still needed; print a **Prayer List**, **Poster Map** or **Card Deck** for a list of 100 |
| a **teacher** or presenter | the Research Gallery | every map in one app, in 11 languages, ready to show |
| a **teacher** or presenter | the Research Gallery | every map in one app, multiple languages support |
<details>
<summary><b>For AI agents</b></summary>

View file

@ -141,6 +141,8 @@ and a change here can break other maps.
`import … from '@map/composables/poster/usePosterLayout.js'`
- **printInPlace** — printInPlace.js — SHARED (@map/composables/printInPlace.js).
`import … from '@map/composables/printInPlace.js'`
- **retryFetch** — retryFetch.js — fetch(), with the same transient-blip retry the lazy components already get.
`import … from '@map/composables/retryFetch.js'`
- **retryableAsyncComponent** — retryableAsyncComponent.js — SHARED (@map/composables/retryableAsyncComponent.js).
`import … from '@map/composables/retryableAsyncComponent.js'`
- **useAvatarGenerator** — Avatar Generator Composable

View file

@ -83,7 +83,8 @@ check('an unknown list -> empty, not a crash', L.listLabel('project-9999', tFrom
}
check('the English catalogue matches the registry, word for word', bad, []) }
{ const missing = []
for (const lc of readdirSync(join(ROOT, 'library/i18n/locales'))) {
// Locale FOLDERS only: the folder also holds a README.md, which is not a locale.
for (const lc of readdirSync(join(ROOT, 'library/i18n/locales'), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)) {
const c = cat(lc)
for (const l of L.peopleGroupLists()) for (const f of ['label', 'definition']) if (!c[l.id]?.[f]) missing.push(`${lc}:${l.id}.${f}`)
}

View file

@ -300,7 +300,9 @@ for (const [file, helper, fallbackFrag] of SITES) {
const used = [...new Set([...literal, ...catIds])]
ok(used.length > literal.length,
`all-people-groups: ${used.length} keys verified = ${literal.length} literal + ${catIds.length} reached only through tr(cat.id)`)
const locales = readdirSync(new URL('../library/i18n/locales', import.meta.url))
// Locale FOLDERS only: the folder also holds a README.md (bba33e783), which is not a locale.
const locales = readdirSync(new URL('../library/i18n/locales', import.meta.url), { withFileTypes: true })
.filter((d) => d.isDirectory()).map((d) => d.name)
const gaps = []
for (const k of used) for (const loc of locales) {
let v

View file

@ -0,0 +1,324 @@
#!/usr/bin/env node
// check-shadow-style-labels-unique.mjs — `bun run check:shadow-style-labels-unique`. Source-only, headless, no network.
//
// Every useShadowStyles(css, label) call must use its OWN label.
//
// WHY A SHARED LABEL IS A BUG, not a naming nit: useShadowStyles keys its <style> by the label, and when a
// tag with that key is already in the shadow root and its text differs, it OVERWRITES the text (the
// hot-update branch in library/composables/useShadowStyles.js). Two components that share a label and
// mount in one root therefore keep only the LAST one's CSS, silently. The regions list lost its whole
// sheet this way on 2026-09-29: RegionSectionCard.vue, extracted from regions-list.vue, kept the label
// 'regions-list'. The cards mount after the list's fetch, so they overwrote .rl-root/.rl-scroll and the
// list stopped scrolling. It was latent for two days, because the list's own label was being swallowed
// into its CSS by a trailing comma; fixing that comma made the label real and the collision live.
//
// ★ GLOBAL, NOT PER SHADOW ROOT, on purpose. The live defect needs two labels in the SAME root, but proving
// which components can co-mount needs an import graph (aliases, import.meta.glob, dynamic imports), and
// every edge such a resolver misses UNDER-reports — the "0 collisions" false green this gate exists to
// prevent. Both collisions found so far were born the same way: a copy (an extracted component, a forked
// profile) kept its source's label. A global rule catches that at the moment of the copy; a per-root rule
// catches it only once the two happen to co-mount. Compliance costs a one-word rename.
//
// THE OTHER WAYS TWO CALLS END UP WITH ONE KEY, each an arm below:
// - no label: the key is 'hmr-' + the COMPONENT's HMR id under the dev server (a hash of the CSS in a
// build), so two unlabelled calls in one component, or one in a composable its caller also makes,
// share a key in dev;
// - a label that is fixed while the CSS is not: a wrapper `useX(css) { useShadowStyles(css, 'x') }`, or
// CSS built from setup(props), gives every caller the same key with different text.
//
// ★ A CALL THE WALK CANNOT SEE MUST NOT READ AS "NO CALL". A hand-rolled scan of this question
// under-reported twice on 2026-09-29, and a first version of this gate cross-checked its AST walk with a
// token scan that used the SAME rule ("the name followed by '('"), so `useShadowStyles?.(…)`, `.call(…)`,
// a rebinding and a re-export slipped past both. So: every REFERENCE to the function (by name or by an
// import alias) must be classified as an import, a call, or the definition, and anything else FAILS; and
// the count of references in Babel's TOKEN stream must equal the count in its AST.
// The parsers are the real ones (the SFC parser and Babel), as in check:shadow-css-parses.
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parse as sfcParse } from '@vue/compiler-sfc'
import { parse as babelParse } from '@babel/parser'
// Each copy of this repo grades ITS OWN tree (check:no-abs-paths).
const REPO = process.env.DOXA_REPO || resolve(dirname(fileURLToPath(import.meta.url)), '..')
const COMPOSABLE = 'library/composables/useShadowStyles.js'
const NAME = 'useShadowStyles'
const FROM_COMPOSABLE = /(^|\/)useShadowStyles(\.js)?$/
const SOURCE_EXT = /\.(vue|js|mjs|cjs|jsx|ts|tsx|mts|cts)$/
// Keys useShadowStyles itself writes. A component using one would overwrite, or be overwritten by, the injector.
const RESERVED = [{ test: (l) => l === 'reduced-motion', why: 'the per-root reduced-motion reset uses this key' },
{ test: (l) => l.startsWith('hmr-'), why: 'dev-server hot-update keys use this prefix' }]
let fails = 0
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
function pluginsFor(lang) {
if (lang === 'ts' || lang === 'mts' || lang === 'cts') return ['typescript']
if (lang === 'tsx') return ['typescript', 'jsx']
return ['jsx']
}
function scriptsOf(path, src) {
const ext = path.split('.').pop()
if (ext !== 'vue') return [{ code: src, offset: 0, lang: ext }]
const { descriptor } = sfcParse(src, { filename: path })
return [descriptor.script, descriptor.scriptSetup].filter(Boolean)
.map((b) => ({ code: b.content, offset: b.loc.start.line - 1, lang: b.lang || 'js' }))
}
// Names every identifier a pattern binds (function parameters).
function boundNames(p, out = new Set()) {
if (!p) return out
if (p.type === 'Identifier') out.add(p.name)
else if (p.type === 'AssignmentPattern') boundNames(p.left, out)
else if (p.type === 'RestElement') boundNames(p.argument, out)
else if (p.type === 'ObjectPattern') p.properties.forEach((q) => boundNames(q.type === 'RestElement' ? q : q.value, out))
else if (p.type === 'ArrayPattern') p.elements.forEach((q) => boundNames(q, out))
else if (p.type === 'TSParameterProperty') boundNames(p.parameter, out)
return out
}
// Identifiers READ inside an expression (not property names, not object keys).
function readNames(node, out = new Set()) {
if (!node || typeof node !== 'object') return out
if (Array.isArray(node)) { node.forEach((n) => readNames(n, out)); return out }
if (node.type === 'Identifier') { out.add(node.name); return out }
for (const k of Object.keys(node)) {
if (k === 'loc' || /Comments$/.test(k)) continue
if (k === 'property' && node.type === 'MemberExpression' && !node.computed) continue
if (k === 'key' && node.type === 'ObjectProperty' && !node.computed) continue
readNames(node[k], out)
}
return out
}
// The token-stream count of references, compared with the AST's. Pure, so the self-tests can show it FAILS.
function referenceCountsAgree(tokens, astStarts, names) {
let n = 0
for (const t of tokens) if (t.type && t.type.label === 'name' && names.has(t.value)) n++
return { agree: n === astStarts.size, tokens: n, ast: astStarts.size }
}
// One file: the calls with their labels, and every problem that stops the file being graded honestly.
function gradeFile(path, src) {
const blocks = scriptsOf(path, src)
const parsed = blocks.map((b) => ({ ...b, ast: babelParse(b.code, { sourceType: 'module', plugins: pluginsFor(b.lang), tokens: true }) }))
// Aliases are collected across BOTH blocks: <script> and <script setup> share their imports.
const names = new Set([NAME])
const problems = []
for (const b of parsed) {
for (const n of b.ast.program.body) {
if (n.type === 'ImportDeclaration' && FROM_COMPOSABLE.test(n.source.value)) {
for (const s of n.specifiers) {
if (s.type === 'ImportSpecifier' && (s.imported.name || s.imported.value) === NAME) names.add(s.local.name)
else problems.push(`${path}:${n.loc.start.line + b.offset}: import useShadowStyles BY NAME ({ useShadowStyles }); a default or namespace import hides its calls from this gate`)
}
}
if (n.type === 'ExportAllDeclaration' && FROM_COMPOSABLE.test(n.source.value)) {
problems.push(`${path}:${n.loc.start.line + b.offset}: re-exports the composable (export *); its callers would call it by another name this gate cannot follow`)
}
}
}
const calls = []
for (const b of parsed) {
const at = (node) => `${path}:${node.loc.start.line + b.offset}`
const refStarts = new Set()
const calleeOf = new Set() // identifiers that are a callee (or a callee's property)
;(function walk(node, parent, fnParams) {
if (!node || typeof node !== 'object') return
if (Array.isArray(node)) { node.forEach((n) => walk(n, parent, fnParams)); return }
if (node.type === 'ImportExpression' || (node.type === 'CallExpression' && node.callee.type === 'Import')) {
const s = node.source || node.arguments[0]
if (s && s.type === 'StringLiteral' && FROM_COMPOSABLE.test(s.value)) problems.push(`${at(node)}: imports the composable dynamically; import { useShadowStyles } statically so its calls can be graded`)
}
if (node.type === 'CallExpression' || node.type === 'OptionalCallExpression') {
const c = node.callee
const direct = c.type === 'Identifier' && names.has(c.name)
const member = (c.type === 'MemberExpression' || c.type === 'OptionalMemberExpression') && !c.computed && c.property.name === NAME
if (direct || member) {
calleeOf.add(direct ? c : c.property)
const args = node.arguments
let label
if (args.slice(0, 2).some((a) => a.type === 'SpreadElement')) label = { kind: 'computed', text: '...spread' }
else {
const a = args[1]
if (!a || (a.type === 'Identifier' && a.name === 'undefined') || a.type === 'NullLiteral' ||
(a.type === 'StringLiteral' && a.value === '') ||
(a.type === 'TemplateLiteral' && !a.expressions.length && a.quasis[0].value.cooked === '')) label = { kind: 'absent' }
else if (a.type === 'StringLiteral') label = { kind: 'literal', text: a.value }
else if (a.type === 'TemplateLiteral' && !a.expressions.length) label = { kind: 'literal', text: a.quasis[0].value.cooked }
else label = { kind: 'computed', text: b.code.slice(a.start, a.end).slice(0, 60) }
}
// A fixed label over CSS that varies per caller: the CSS reads a parameter of an enclosing function.
const varying = [...readNames(args[0])].filter((n) => fnParams.has(n))
calls.push({ at: at(node), label, varying })
}
}
if (node.type === 'Identifier' && names.has(node.name)) refStarts.add(node.start)
let params = fnParams
if (/Function|Method/.test(node.type) && node.params) {
params = new Set(fnParams)
boundNames({ type: 'ArrayPattern', elements: node.params }, params)
}
for (const k of Object.keys(node)) {
if (k === 'loc' || /Comments$/.test(k)) continue
walk(node[k], node, params)
}
})(b.ast.program, null, new Set())
// CLASSIFY EVERY REFERENCE. An import specifier, a callee and the composable's own definition are the only
// positions a reference may take; anything else (rebinding, .call/.apply, a destructured key, a re-export,
// passing it as a value) is a call this gate cannot follow.
;(function classify(node, parent) {
if (!node || typeof node !== 'object') return
if (Array.isArray(node)) { node.forEach((n) => classify(n, parent)); return }
const isRef = (node.type === 'Identifier' && names.has(node.name))
if (isRef && !calleeOf.has(node)) {
const fine = (parent?.type === 'ImportSpecifier') ||
(path === COMPOSABLE && parent?.type === 'FunctionDeclaration' && parent.id === node)
if (!fine) {
problems.push(`${at(node)}: '${node.name}' is used as ${parent?.type || 'a value'}, not called; this gate can only grade direct calls ` +
'(a rebinding, .call/.apply, a destructured key, a re-export or a method of that name all hide a call)')
}
}
for (const k of Object.keys(node)) {
if (k === 'loc' || /Comments$/.test(k)) continue
classify(node[k], node)
}
})(b.ast.program, null)
const cc = referenceCountsAgree(b.ast.tokens, refStarts, names)
if (!cc.agree) problems.push(`${path}: the token stream holds ${cc.tokens} reference(s) to useShadowStyles and the AST ${cc.ast}; the file cannot be graded`)
}
return { calls, problems }
}
// Grades a set of { path, src } files. Returns the problems found (empty = pass) and what was measured.
function grade(files) {
const byLabel = new Map()
const problems = []
let calls = 0, absent = 0
for (const { path, src } of files) {
if (!/useShadowStyles/.test(src)) continue
let r
try { r = gradeFile(path, src) } catch (e) { problems.push(`${path}: could not be parsed (${e.message.slice(0, 80)})`); continue }
problems.push(...r.problems)
const unlabelled = r.calls.filter((c) => c.label.kind === 'absent')
if (unlabelled.length && !path.endsWith('.vue')) {
problems.push(`${unlabelled.map((c) => c.at).join(' , ')}: an unlabelled call outside a .vue file is keyed by its CALLER's HMR id under the dev server, ` +
'so it overwrites the caller\'s own unlabelled sheet; give it a label')
} else if (unlabelled.length > 1) {
problems.push(`${unlabelled.map((c) => c.at).join(' , ')}: ${unlabelled.length} unlabelled calls in one component share its HMR key under the dev server; label them`)
}
for (const c of r.calls) {
calls++
if (c.varying.length) {
problems.push(`${c.at}: the CSS reads ${c.varying.join(', ')}, a parameter of the enclosing function, under one label; ` +
'every caller (or instance) then shares the key with different CSS. Pass fixed CSS, or give each caller its own label')
}
if (c.label.kind === 'absent') { absent++; continue }
if (c.label.kind === 'computed') {
problems.push(`${c.at}: the label is computed (${c.label.text}); pass a string literal so its uniqueness can be checked`)
continue
}
const res = RESERVED.find((x) => x.test(c.label.text))
if (res) problems.push(`${c.at}: label '${c.label.text}' is reserved (${res.why})`)
if (!byLabel.has(c.label.text)) byLabel.set(c.label.text, [])
byLabel.get(c.label.text).push(c.at)
}
}
for (const [label, sites] of byLabel) {
if (sites.length > 1) {
problems.push(`label '${label}' is used by ${sites.length} calls: ${sites.join(' , ')}\n` +
' whichever mounts last in a shadow root overwrites the other\'s CSS; give each call its own label (its file\'s name)')
}
}
return { problems, calls, absent, labels: byLabel.size }
}
// ── SELF-TESTS: the grader must FAIL on the damage it exists to catch, and pass its absence ──────────
console.log('SELF-TESTS (a check that cannot fail proves nothing)')
const f = (path, src) => ({ path, src })
const imp = "import { useShadowStyles } from '@map/composables/useShadowStyles.js'\n"
const sfc = (body) => `<script setup>\n${imp}${body}\n</script>\n<template><div/></template>`
const one = (files, re, m) => { const r = grade(files); ok(r.problems.length === 1 && re.test(r.problems[0]), `${m}${r.problems.length === 1 ? '' : ` (got ${r.problems.length} problems)`}`) }
{
const r = grade([f('a.js', imp + "useShadowStyles('.a{}', 'aa')"), f('b.vue', sfc("useShadowStyles('.b{}', 'bb')"))])
ok(!r.problems.length && r.calls === 2, 'distinct labels in two files pass')
}
one([f('list.vue', sfc("useShadowStyles('.rl-root{}', 'regions-list')")), f('Card.vue', sfc("useShadowStyles('.card{}', 'regions-list')"))],
/'regions-list' is used by 2 calls/, 'NEG CONTROL: the regions-list collision (two .vue files, one label) FAILS')
one([f('a.js', imp + 'useShadowStyles(`\n' + '.x{ color:red; }\n'.repeat(263) + "`, 'research-map')"), f('b.js', imp + "useShadowStyles('.b{}', 'research-map')")],
/'research-map'/, 'NEG CONTROL: a label 263 lines after its call, after a template literal, is still read')
one([f('a.js', "import { useShadowStyles as inject } from './useShadowStyles.js'\ninject('.a{}', 'dup')"), f('b.js', imp + "useShadowStyles('.b{}', 'dup')")],
/'dup' is used by 2/, 'NEG CONTROL: a call through an import ALIAS is graded')
one([f('a.vue', `<script>\nimport { useShadowStyles as inject } from './useShadowStyles.js'\n</script>\n<script setup>\ninject('.a{}', 'dup')\n</script>`), f('b.js', imp + "useShadowStyles('.b{}', 'dup')")],
/'dup' is used by 2/, 'NEG CONTROL: an alias imported in <script> and called in <script setup> is graded')
one([f('a.js', imp + "useShadowStyles?.('.a{}', 'dup')"), f('b.js', imp + "useShadowStyles('.b{}', 'dup')")],
/'dup' is used by 2/, 'NEG CONTROL: an optional call ?.() is graded')
one([f('a.js', imp + "useShadowStyles('.a{}', `dup`)"), f('b.js', imp + "useShadowStyles('.b{}', 'dup')")],
/'dup' is used by 2/, 'NEG CONTROL: a template-literal label is read as its text')
one([f('a.js', imp + "useShadowStyles('.a{}', 'dup')\nuseShadowStyles('.b{}', 'dup')")], /'dup' is used by 2/, 'NEG CONTROL: two calls in ONE file with one label FAIL')
one([f('a.js', imp + "const L = 'x'\nuseShadowStyles('.a{}', L)")], /computed/, 'NEG CONTROL: a computed label FAILS (it cannot be checked)')
one([f('a.js', imp + "const args = ['.a{}', 'x']\nuseShadowStyles(...args)")], /computed/, 'NEG CONTROL: spread arguments FAIL as computed, not pass as unlabelled')
one([f('a.js', imp + "useShadowStyles('.a{}', 'reduced-motion')")], /reserved/, 'NEG CONTROL: a reserved key FAILS')
one([f('a.js', imp + "const inject = useShadowStyles\ninject('.a{}', 'x')")], /not called/, 'NEG CONTROL: a rebinding FAILS (the call behind it is invisible)')
one([f('a.js', imp + "useShadowStyles.call(null, '.a{}', 'x')")], /not called/, 'NEG CONTROL: .call() FAILS')
one([f('a.js', "export { useShadowStyles as injectCss } from './useShadowStyles.js'")], /not called/, 'NEG CONTROL: a re-export under another name FAILS')
one([f('a.js', "import * as S from './useShadowStyles.js'\nS.useShadowStyles('.a{}', 'x')")], /BY NAME/, 'NEG CONTROL: a namespace import FAILS')
one([f('a.js', imp + "const o = { useShadowStyles(css) { return css } }\nuseShadowStyles('.a{}', 'x')")], /not called/, 'NEG CONTROL: a method NAMED useShadowStyles is refused, not counted as a call')
one([f('a.js', imp + "export function useX(css) { useShadowStyles(css, 'x') }")], /parameter of the enclosing function/,
'NEG CONTROL: a wrapper forwarding its caller\'s CSS under one fixed label FAILS')
{
const r = grade([f('a.js', imp + "const CSS = '.a{}'\nexport function useCard() { useShadowStyles(CSS, 'card') }")])
ok(!r.problems.length, 'a wrapper injecting FIXED CSS under its label passes (useCardSurface\'s shape)')
}
one([f('a.vue', sfc("useShadowStyles('.a{}')\nuseShadowStyles('.b{}')"))], /2 unlabelled calls in one component/, 'NEG CONTROL: two unlabelled calls in one component FAIL (one dev HMR key)')
one([f('a.js', imp + "useShadowStyles('.a{}')")], /outside a \.vue file/, 'NEG CONTROL: an unlabelled call in a composable FAILS (it takes its caller\'s HMR key)')
{
const r = grade([f('a.vue', sfc("useShadowStyles('.a{}')")), f('b.vue', sfc("useShadowStyles('.b{}', '')"))])
ok(!r.problems.length && r.absent === 2, "one unlabelled call per component passes, and '' is read as no label (the runtime's label || …)")
}
{
const r = grade([f('a.js', imp + "// useShadowStyles('.a{}', 'dup')\nconst s = \"useShadowStyles('.a{}', 'dup')\"\nuseShadowStyles /* why */ ('.b{}', 'dup')")])
ok(!r.problems.length && r.calls === 1, 'a call in a comment or a string is not a call, and a comment before the ( does not trip the count')
}
{
const good = referenceCountsAgree([{ type: { label: 'name' }, value: NAME }], new Set([7]), new Set([NAME]))
const bad = referenceCountsAgree([{ type: { label: 'name' }, value: NAME }, { type: { label: 'name' }, value: NAME }], new Set([7]), new Set([NAME]))
ok(good.agree && !bad.agree, 'NEG CONTROL: the token-vs-AST reference count FAILS when the two disagree (2 tokens, 1 node)')
}
// ── the tree ─────────────────────────────────────────────────────────────────────────────────────────
// Tracked files plus untracked-but-not-ignored ones: a gitignored local copy (an _archive/ backup of a
// component, keeping its label) must not turn one machine red. Outside a git checkout (DOXA_REPO pointed at
// an exported tree), every source file is read.
function sourceFiles() {
try {
const top = execFileSync('git', ['-C', REPO, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
if (resolve(top) === resolve(REPO)) {
return execFileSync('git', ['-C', REPO, 'ls-files', '--cached', '--others', '--exclude-standard', '--', 'library', 'plugin-designs'],
{ encoding: 'utf8', maxBuffer: 1 << 26 }).split('\n').filter((p) => SOURCE_EXT.test(p))
}
} catch { /* not a checkout of its own: walk it */ }
const walk = (d) => readdirSync(d).flatMap((n) => {
const p = join(d, n)
if (statSync(p).isDirectory()) return n === 'node_modules' || n.startsWith('.') ? [] : walk(p)
return SOURCE_EXT.test(n) ? [p.slice(REPO.length + 1)] : []
})
return [...walk(join(REPO, 'library')), ...walk(join(REPO, 'plugin-designs'))]
}
const files = sourceFiles().sort().flatMap((p) => { try { return [f(p, readFileSync(join(REPO, p), 'utf8'))] } catch { return [] } })
console.log(`\nSHADOW-STYLE LABELS across library/ and plugin-designs/ (${files.length} source files)`)
const r = grade(files)
for (const p of r.problems) console.log(` FAIL - ${p}`)
fails += r.problems.length
ok(r.calls > 0, `${r.calls} useShadowStyles call(s) graded: ${r.labels} distinct label(s), ${r.absent} unlabelled (0 calls would mean this measured nothing)`)
console.log(fails ? `\nSHADOW-STYLE-LABELS: ${fails} FAIL(s)` : `\nSHADOW-STYLE-LABELS: every one of ${r.calls - r.absent} labelled call(s) has its own label`)
process.exit(fails ? 1 : 0)

View file

@ -9,7 +9,7 @@
"check:shot-fonts": "checks that the MACHINE taking README screenshots can render emoji (fc-list): a property of that box, not of the product, so it runs before a screenshot session, not in the gates",
"check:coverage-consistent": "the full run includes clause L, the age of the oldest link verification (45 days), which depends on the CALENDAR: chained, it would turn gates red on a date (first 2026-11-08) with no code change. Run it with the link re-verification; every other clause runs in gates:pre as check:coverage-consistent:chained",
"check:snippets-draw": "AWAITING ITS FIRST BROWSER RUN (rega): the browser suite lists a gate only after it has run. Moves into verify-browser-all's GATES, and out of this list, once that run is recorded.",
"check:snippets-draw:mapbox": "AWAITING ITS FIRST BROWSER RUN (rega): the browser suite lists a gate only after it has run. Moves into verify-browser-all's GATES, and out of this list, once that run is recorded. Needs MAP_TOKEN.",
"check:region-paint-single-source": "TEMPORARY, remove when chained. Asserts the regions idle paint expression has one source in the library rather than a copy per map. Half-adopted on purpose: research-map.vue took the library export (2026-09-29), countries-map.vue is the other plugin owner's file and its two assertions are red until they land their half. Chaining it now would hold gates:post red for everyone over another agent's unlanded work. It joins gates:post in the SAME landing as that half, and this entry is deleted in that commit"
"check:grid-merge-defaults": "WAITS FOR ITS CODE: it asserts the defaults of three grid props (filtersToggle, backdrop, backdropFloor) that are not on development, because the grid merge (b1e839a33) has not landed. Chain it in gates:pre in the same landing as that merge.",
"check:snippets-draw:mapbox": "AWAITING ITS FIRST BROWSER RUN (rega): the browser suite lists a gate only after it has run. Moves into verify-browser-all's GATES, and out of this list, once that run is recorded. Needs MAP_TOKEN."
}
}

View file

@ -253,10 +253,15 @@ async function main() {
const serial = results.reduce((a, r) => a + r.ms, 0)
const failed = results.filter((r) => r.bucket === 'FAIL')
if (VERBOSE) for (const r of results) if (r.out.trim()) console.log(`\n───── ${r.name} ─────\n${r.out}`)
// A failure must be readable in full: these gates explain themselves in their output, and
// a truncated explanation is how a real defect gets re-diagnosed as a flake.
else for (const r of failed) console.log(`\n───── FAILED: ${r.name} (exit ${r.code}) ─────\n${r.out}`)
// ★ BRACED ON PURPOSE. Unbraced, this `else` bound to the INNER `if (r.out.trim())`, so without
// --verbose a failed gate printed only its name and exit code, never why.
if (VERBOSE) {
for (const r of results) if (r.out.trim()) console.log(`\n───── ${r.name} ─────\n${r.out}`)
} else {
for (const r of failed) console.log(`\n───── FAILED: ${r.name} (exit ${r.code}) ─────\n${r.out}`)
}
// COUNT THE SKIPS IN THE SUMMARY, because the summary is the line a reader quotes and
// the one that lands in a report. "35/35 passed" was true of a wave where a third of the

View file

@ -39,7 +39,7 @@
* Refuses to CREATE <dest>: the drop-in folder is the host's decision, not this script's.
* Gate: `node internal/public-hygiene-check.mjs <dest> --strict` must pass before a public push.
*
* Usage: node internal/sync-dropin.mjs <dest> [--from public] [--tree doxa-maps] [--keep-staging]
* Usage: node internal/sync-dropin.mjs <dest> [--from public] [--tree doxa-maps] [--keep-staging] [--if-present]
* [--only a,b | --from-config] [--prune] [--protect a.txt,other] [--dry-run]
* e.g. bun run dropin (= --from-config, into dropin.dest: ../doxa-life/public)
* Prints: copied · unchanged · deleted (stale inside the allowlist) · pruned (outside it) ·
@ -96,6 +96,13 @@ if (PRUNE && basename(dest) === 'public') {
console.error(`[sync-dropin] refusing --prune into ${dest}: it is a site's web-root, and --prune would delete every file of the site outside the drop-in. Sync without --prune (bun run dropin); stale files INSIDE the drop-in are still removed.`)
process.exit(1)
}
// --if-present (the full build's last step): the configured host is OPTIONAL. Its absence is a skip, not an error.
// Only the ONE configured path is checked (dropin.dest, or DOXA_DROPIN_DEST): nothing on the filesystem is searched
// for other repos or sites. DOXA_DROPIN=off skips it even when the host is there.
if (args.includes('--if-present')) {
if (process.env.DOXA_DROPIN === 'off') { console.log('[sync-dropin] skipped: DOXA_DROPIN=off'); process.exit(0) }
if (!existsSync(dest) || !statSync(dest).isDirectory()) { console.log(`[sync-dropin] skipped: no host at the configured path ${dest}`); process.exit(0) }
}
if (!existsSync(dest) || !statSync(dest).isDirectory()) { console.error(`[sync-dropin] dest does not exist or is not a directory: ${dest} — create the drop-in folder in the host first`); process.exit(1) }
if (!existsSync(FROM)) { console.error(`[sync-dropin] source missing: ${FROM} — run \`bun run build\` first`); process.exit(1) }

View file

@ -25,7 +25,10 @@ import { getCurrentInstance, onMounted } from 'vue'
* Idempotent — uses a data-attribute key to avoid duplicate injection.
*
* @param {string} css - Raw CSS text to inject
* @param {string} [label] - Optional label for the <style> tag (for debugging)
* @param {string} [label] - The <style> tag's KEY in its shadow root, not a debugging name: a second call
* with the same label and different CSS OVERWRITES the first one's sheet. Use a
* label no other call uses (the file's own name); a copied or forked component
* must get a new one. Enforced by check:shadow-style-labels-unique.
*/
export function useShadowStyles(css, label) {
// Captured in setup (onMounted's callback cannot see it reliably).

View file

@ -13,7 +13,7 @@
"build": "bash internal/build-lock.sh",
"build:bundle": "bun x vite build",
"build:bundles": "node internal/run-parallel.mjs --bundles",
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post",
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && node internal/sync-dropin.mjs --from-config --if-present",
"build:watch": "bun x vite build --watch",
"cap:sync": "./node_modules/.bin/cap sync android",
"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)",
@ -286,9 +286,11 @@
"check:embed-url-absolute": "node internal/check-embed-url-absolute.mjs",
"check:grid-image-loading": "node internal/check-grid-image-loading.mjs",
"check:retry-fetch": "node internal/check-retry-fetch.mjs",
"check:grid-merge-defaults": "node internal/check-grid-merge-defaults.mjs",
"check:setup-order": "node internal/check-setup-order.mjs",
"check:sfc-parse": "node internal/check-sfc-script-parses.mjs",
"check:shadow-css-parses": "node internal/check-shadow-css-parses.mjs",
"check:shadow-style-labels-unique": "node internal/check-shadow-style-labels-unique.mjs",
"check:shadow-reduced-motion": "node internal/check-shadow-reduced-motion.mjs",
"check:regions-uses-library": "node plugin-designs/regions-dashboard/scripts/check-regions-uses-library.mjs",
"check:religion-label-not-value": "node plugin-designs/regions-dashboard/scripts/check-religion-label-not-value.mjs",
@ -339,7 +341,7 @@
"fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:template-i18n check:list-pattern-doc",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch",
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
"library-index": "node internal/generate-library-index.mjs",
@ -521,6 +523,7 @@
"check:mobile": "deploy-2 CONTRACT check: needs NO JDK/SDK/device. cap sync is a file copy + plugin wiring, so it runs anywhere; cap:verify then asserts assets/public/index.html exists. Guards the SOFT FAILURE capacitor.config.json warns about: a wrong webDir yields an EMPTY assets/public and a BLANK WHITE SCREEN at launch, not an error.",
"check:mst-clustering": "the MST clustering mode draws a true minimum spanning tree/forest (same edge count and length as a reference Kruskal) and 2,107 pins take under 3 s on the main thread. Headless.",
"check:nav-reachable": "a cap-5 shell must have ONE on-screen control that opens its tab rail at phone width (needs `bun run serve` on 4173; browser gate, not in build:unlocked)",
"check:grid-merge-defaults": "the people-group grid's merge props (filtersToggle, backdrop, backdropFloor) must DEFAULT to today's grid, read from the SFC's defineProps by parser. ON DEMAND: those props are not on development yet (the grid merge, b1e839a33, is not landed), so it would be red in any chain; it chains with that merge.",
"check:no-abs-paths": "no /home/… or /Users/… in any TRACKED file (a clone must work on any machine)",
"check:no-codename": "the development-era INTERNAL CODE NAME may not ship in any TRACKED file. Same hand-off family as check:no-abs-paths: meaningless to the Doxa team who inherit this, and invisible to every other gate (none read for vocabulary). Deliberately does NOT fail on the same letters appearing as the unix USERNAME inside a /home/… path — that is a portability defect check:no-abs-paths already reports better, and two gates disagreeing about one string teaches everyone to ignore both.",
"check:no-conflict-markers": "Nothing tracked may carry an unresolved merge. Exists because a merge from this worktree committed a BUILT bundle with 21 conflict markers - both sides concatenated, 1.13MB - and every gospel leaf rendered dead while the SRI hash matched and every request returned 200. The cause was a landing script that resolved one expected file then ran \"git add -A public\", staging every other file including the ones still holding markers. For a built file: take either side, REBUILD, commit the rebuild, then run this before pushing. --poison for the negative test.",
@ -587,6 +590,7 @@
"check:setup-order": "a line's POSITION in a setup block is not the MOMENT it runs, and two P0s from the Driver's console came from that gap. ARM A: `watch(..., { immediate: true })` runs during setup, so anything it reaches must be declared ABOVE it - research-map.vue threw `Cannot access '_lastFitKey' before initialization` and blanked the whole map, because the watcher called a HOISTED function 350 lines down whose own constants were still in their temporal dead zone. A hoisted function promises that its NAME resolves early, not that its BODY can run early, and that asymmetry is invisible from both ends. ARM B: a lifecycle registrar closes over Vue's current instance, which exists only while setup is on the stack, so `onMounted(async () => { await init(); onBeforeUnmount(clean) })` drops the registration silently - DashboardMapPanel.vue leaked a ResizeObserver on document.body plus a window listener, warning only, feature working, and research-map.vue had a second copy this gate found. The fix is never 'move the line': declare the handle at setup scope and tear it down in a hook registered before the first await. Its own false positives are the interesting part - it went 6 hits -> 1 over three rounds, and every one it dropped was prose or shadowing read as code: `onBeforeUnmount` inside a console.error STRING, `table` inside `new Error(`table not found (${r.status})`)` in four files, and a `const [pins, byCountry]` destructured INSIDE the function, shadowing a computed 220 lines away. So it blanks comments, string text and template text (keeping ${...}, which is code) and subtracts every name a span declares for itself. Self-tests on both shipped defects, both fixes, and all three decoys; exits 3 if any arm goes blind.",
"check:sfc-parse": "every .vue script block must parse as the BUILD parses it — catches the LIVE-CSS backtick that check:template-literal cannot see (it reads CSS comments only). 0 false positives on 83 files, where 3 lookahead heuristics scored 98 and 136.",
"check:shadow-css-parses": "the CSS we assemble as JS STRINGS must parse as CSS — brace balance and declarations stranded outside any rule. check:sfc-parse proves only that the JS parses, so it passed a stylesheet with 9 orphaned declarations and braces at 12 open / 21 close (REGLIB batch 1); nothing else in the tree validates CSS syntax. Reads every useShadowStyles() argument off the @babel/parser AST the build already uses, because three hand-rolled lexers each produced a confident FALSE RED: a /* CSS comment */ read as a declaration, an apostrophe in a .vue prose header opening a phantom string literal, and a regex literal containing a quote. 9 self-tests run inline on every invocation, including the exact batch-1 damage, so it cannot silently stop being able to fail.",
"check:shadow-style-labels-unique": "every useShadowStyles call has its OWN label: the label keys the <style>, and a second component with the same label overwrites the first one's CSS in a shared shadow root (the regions list lost its sheet this way). Global, not per-root, so a copied component is caught when it is copied. AST walk cross-checked by a token count.",
"check:shadow-reduced-motion": "every animating library component honours prefers-reduced-motion: a page's own reset never crosses a shadow boundary, so useShadowStyles injects one per shadow root, and a component styled any other way must carry its own @media (prefers-reduced-motion: reduce) block. Self-test runs every time.",
"check:shared-reached": "a shared library fix is only as deployed as the last rebuild of EVERY bundle that consumes it. Each bundle ships its own COMPILED copy, so a one-line fix in library/ is live in whichever bundles happened to be rebuilt after it and silently absent from the rest — and every other gate measures ONE leaf, which renders the old behaviour perfectly. Asserts a distinctive marker string per tracked change; consumers are DERIVED from imports, never hardcoded.",
"check:shot-distinctness": "two README slots must not show the SAME view. README shipped \"List — page 1\" and \"page 2\" as the same picture: the shot chased a Next control and a scroll that DO NOT EXIST, every attempt inside try/catch so the miss was silent, and the PNGs differed by 0.16% render noise so no checksum caught them. Keys on url+VIEWPORT — same url at a different vp (deploy2 mobile) is intended.",

View file

@ -183,6 +183,9 @@ import { RESEARCH_LEGEND_OPTIONS } from '@map/composables/legends/researchLegend
const PosterDialog = retryableAsyncComponent(() => import('@map/components/poster/PosterDialog.vue'))
// ─── Shadow-DOM style injection ──────────────────────────────────────────────
// Labelled 'countries-map', its own name. This file began as a fork of research-map.vue and kept that
// file's label; a shared label lets whichever mounts last overwrite the other's sheet
// (check:shadow-style-labels-unique).
useShadowStyles(`
/* ── Canonical geocoder + legend container CSS (clone of doxa-simple-map.vue
lines 50-138) so tabs 1-3 render the geocoder + legend at the same
@ -362,7 +365,7 @@ useShadowStyles(`
.map-scroll-edge { position:absolute;top:0;bottom:0;width:40px;z-index:2;touch-action:pan-y;pointer-events:auto;background:transparent; }
.map-scroll-edge--left { left:0; }
.map-scroll-edge--right { right:0; }
`, 'research-map')
`, 'countries-map')
// ─── Config from ProfileLoader (provide/inject — never direct imports) ──────
const profileConfig = inject('profileConfig')

View file

@ -1536,8 +1536,8 @@ provide('legendExportBusy', exportBusy)
// Neither export hands over a file any more: the poster prints through printImageBlob and the packet
// through the shared usePosterPrints.printPacket, so the download helper that served packet.pdf is gone.
async function openPacket(variant = 'full') {
if (_packetBusy) return
_packetBusy = true
if (packetBusy.value) return
packetBusy.value = true
try {
// ★ THE PACKET PRINTS, AND IT PRINTS THROUGH THE SHARED RENDERER (audit card 2, and
// the standing rule: an export button opens the print dialog, never hands over a file). This

View file

@ -289,7 +289,8 @@ const tabs = computed(() => profileConfig?.value?.tabs ?? DEFAULT_TABS)
// profileConfig.tabs is the host's own words and is shown as given.
const DEFAULT_LABELS = Object.fromEntries(DEFAULT_TABS.map((d) => [d.id, d.label]))
const tabLabel = (tab) => ((!tab.label || tab.label === DEFAULT_LABELS[tab.id]) && te('tabs.' + tab.id)) ? t('tabs.' + tab.id) : tab.label
// before giving an instance a 2nd tab: the tab bar needs role="tablist"/"tab" + :aria-selected (the underline already marks the active tab by shape, not only colour)
// With 2+ tabs (the research page passes three) the bar is a tablist: a screen reader announces each
// button as a tab and which one is selected. The underline marks the active tab by shape, not only colour.
const showTabBar = computed(() => tabs.value.length > 1)
// ─── Tab state ────────────────────────────────────────────────────────────────
@ -1406,12 +1407,15 @@ onBeforeUnmount(() => {
>
<!-- In-map tab bar (only when 2+ tabs) -->
<div v-if="showTabBar" class="dsm-tab-bar">
<div v-if="showTabBar" class="dsm-tab-bar" role="tablist">
<button
v-for="tab in tabs"
:key="tab.id"
type="button"
role="tab"
class="dsm-tab"
:class="{ active: activeTabId === tab.id }"
:aria-selected="activeTabId === tab.id ? 'true' : 'false'"
@click="switchTab(tab.id)"
>
{{ tabLabel(tab) }}

View file

@ -1,4 +1,6 @@
{
"stage": "production",
"_stage_comment": "Marked production because the Deep Research gallery MOUNTS this profile: doxa-maps-spa.meta.json composes 'gospel-resources/resources-map' and doxa-maps-spa.vue's TAB_DEFS names it as the Gospel Resources tab. The stage was absent here, so the manifest defaulted it to experimental while a production page shipped it — the record disagreed with what ships. This changes the manifest only; nothing a reader sees.",
"type": "single",
"label": "Gospel Resources — resources map",
"config": {

View file

@ -22,19 +22,6 @@
"show-search": "false",
"show-filters": "false"
}
},
{
"id": "regions-look",
"title": "People Groups Grid — the regions-dashboard look (link toggle + root watermark)",
"_why": "A merge preview (2026-09-28): SAME component and SAME cards as the standalone instance above, with only the two props he chose actually differing. Open this beside 'people-groups-grid' — if anything other than the filters toggle's styling and the watermark's placement differs between the two pages, the merge has changed something it was not asked to.",
"props": {
"profile": "people-groups-grid",
"provider": "maplibre",
"instanceId": "people-groups-grid-regions-look",
"pray-base-url": "https://pray.doxa.life",
"filters-toggle": "link",
"backdrop": "root"
}
}
]
}

View file

@ -70,7 +70,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -61,7 +61,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -81,7 +81,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -200,7 +200,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "dx-maps/country-posters/country-posters.js",
"integrity": "sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o",
"integrity": "sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt",
"elements": [
"country-posters-map"
],
@ -223,8 +223,8 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -240,7 +240,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -256,8 +256,8 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -416,7 +416,7 @@
"bundle": "doxa-research-map",
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
"integrity": "sha384-SaeVc0/eAh41GHtiLB+uFgg/SWmEU70lUPiO+tJJB+ONNLmwtZ8yC8EsxM3D1uva",
"integrity": "sha384-ba3eaJqjueCJT+061487aaNKQyoFqP8vYYjWjBrwzbYtMUYhnwmHXW5IO0dG4J4m",
"elements": [
"doxa-research-map"
],
@ -435,8 +435,8 @@
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-research-map/research-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-SaeVc0/eAh41GHtiLB+uFgg/SWmEU70lUPiO+tJJB+ONNLmwtZ8yC8EsxM3D1uva\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-SaeVc0/eAh41GHtiLB+uFgg/SWmEU70lUPiO+tJJB+ONNLmwtZ8yC8EsxM3D1uva\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-ba3eaJqjueCJT+061487aaNKQyoFqP8vYYjWjBrwzbYtMUYhnwmHXW5IO0dG4J4m\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-ba3eaJqjueCJT+061487aaNKQyoFqP8vYYjWjBrwzbYtMUYhnwmHXW5IO0dG4J4m\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
},
"instances": [
{
@ -462,7 +462,7 @@
"bundle": "doxa-simple-map",
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"output": "dx-maps/doxa-simple-map/doxa-simple-map.js",
"integrity": "sha384-uVjVvk2lTU1akrc6T0SouQ9zOIX/35Hrltso16QO4xkaN1Nyy+6QuP8BhWG/74wE",
"integrity": "sha384-9DC6IjuVnKHJrXixRzkEyt4XKBfhkhD4xT0aAcXzOJWSMSZFsoA4576Fn6+C0nMV",
"elements": [
"doxa-map",
"doxa-simple-map"
@ -482,8 +482,8 @@
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-uVjVvk2lTU1akrc6T0SouQ9zOIX/35Hrltso16QO4xkaN1Nyy+6QuP8BhWG/74wE\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-uVjVvk2lTU1akrc6T0SouQ9zOIX/35Hrltso16QO4xkaN1Nyy+6QuP8BhWG/74wE\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-9DC6IjuVnKHJrXixRzkEyt4XKBfhkhD4xT0aAcXzOJWSMSZFsoA4576Fn6+C0nMV\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-9DC6IjuVnKHJrXixRzkEyt4XKBfhkhD4xT0aAcXzOJWSMSZFsoA4576Fn6+C0nMV\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
},
"instances": [
{
@ -859,7 +859,7 @@
},
{
"profile": "resources-map",
"stage": "experimental",
"stage": "production",
"label": "Gospel Resources — resources map",
"file": "resources-map.vue",
"description": "12,379 people-group pins over a country choropleth: tap a pin for the Scripture, audio, film, Bible-story and training links that exist in that group's own language with real URLs, tap a country for every group in it, search by group, language or ISO 639-3 code, and filter to the groups where nothing was found.",
@ -1001,24 +1001,6 @@
"embed": {
"iframe": "<iframe src=\"./people-groups-grid/people-groups-grid/driven/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "regions-look",
"title": "People Groups Grid — the regions-dashboard look (link toggle + root watermark)",
"apiBaseUrl": "",
"props": {
"profile": "people-groups-grid",
"provider": "maplibre",
"instanceId": "people-groups-grid-regions-look",
"pray-base-url": "https://pray.doxa.life",
"filters-toggle": "link",
"backdrop": "root"
},
"element": "people-groups-grid",
"iframeUrl": "./people-groups-grid/people-groups-grid/regions-look/index.html",
"embed": {
"iframe": "<iframe src=\"./people-groups-grid/people-groups-grid/regions-look/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
}
]
}

File diff suppressed because one or more lines are too long

View file

@ -70,7 +70,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../doxa-research-map.js" integrity="sha384-SaeVc0/eAh41GHtiLB+uFgg/SWmEU70lUPiO+tJJB+ONNLmwtZ8yC8EsxM3D1uva" crossorigin="anonymous"></script>
<script src="../doxa-research-map.js" integrity="sha384-ba3eaJqjueCJT+061487aaNKQyoFqP8vYYjWjBrwzbYtMUYhnwmHXW5IO0dG4J4m" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -66,7 +66,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../../doxa-research-map.js" integrity="sha384-SaeVc0/eAh41GHtiLB+uFgg/SWmEU70lUPiO+tJJB+ONNLmwtZ8yC8EsxM3D1uva" crossorigin="anonymous"></script>
<script src="../../doxa-research-map.js" integrity="sha384-ba3eaJqjueCJT+061487aaNKQyoFqP8vYYjWjBrwzbYtMUYhnwmHXW5IO0dG4J4m" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -70,7 +70,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-uVjVvk2lTU1akrc6T0SouQ9zOIX/35Hrltso16QO4xkaN1Nyy+6QuP8BhWG/74wE" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-9DC6IjuVnKHJrXixRzkEyt4XKBfhkhD4xT0aAcXzOJWSMSZFsoA4576Fn6+C0nMV" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -70,7 +70,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-uVjVvk2lTU1akrc6T0SouQ9zOIX/35Hrltso16QO4xkaN1Nyy+6QuP8BhWG/74wE" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-9DC6IjuVnKHJrXixRzkEyt4XKBfhkhD4xT0aAcXzOJWSMSZFsoA4576Fn6+C0nMV" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -70,7 +70,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../doxa-simple-map.js" integrity="sha384-uVjVvk2lTU1akrc6T0SouQ9zOIX/35Hrltso16QO4xkaN1Nyy+6QuP8BhWG/74wE" crossorigin="anonymous"></script>
<script src="../doxa-simple-map.js" integrity="sha384-9DC6IjuVnKHJrXixRzkEyt4XKBfhkhD4xT0aAcXzOJWSMSZFsoA4576Fn6+C0nMV" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -70,7 +70,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-uVjVvk2lTU1akrc6T0SouQ9zOIX/35Hrltso16QO4xkaN1Nyy+6QuP8BhWG/74wE" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-9DC6IjuVnKHJrXixRzkEyt4XKBfhkhD4xT0aAcXzOJWSMSZFsoA4576Fn6+C0nMV" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -200,7 +200,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "dx-maps/country-posters/country-posters.js",
"integrity": "sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o",
"integrity": "sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt",
"elements": [
"country-posters-map"
],
@ -223,8 +223,8 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -240,7 +240,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -256,8 +256,8 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -416,7 +416,7 @@
"bundle": "doxa-research-map",
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"output": "dx-maps/doxa-research-map/doxa-research-map.js",
"integrity": "sha384-SaeVc0/eAh41GHtiLB+uFgg/SWmEU70lUPiO+tJJB+ONNLmwtZ8yC8EsxM3D1uva",
"integrity": "sha384-ba3eaJqjueCJT+061487aaNKQyoFqP8vYYjWjBrwzbYtMUYhnwmHXW5IO0dG4J4m",
"elements": [
"doxa-research-map"
],
@ -435,8 +435,8 @@
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-research-map/research-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-SaeVc0/eAh41GHtiLB+uFgg/SWmEU70lUPiO+tJJB+ONNLmwtZ8yC8EsxM3D1uva\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-SaeVc0/eAh41GHtiLB+uFgg/SWmEU70lUPiO+tJJB+ONNLmwtZ8yC8EsxM3D1uva\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-ba3eaJqjueCJT+061487aaNKQyoFqP8vYYjWjBrwzbYtMUYhnwmHXW5IO0dG4J4m\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-ba3eaJqjueCJT+061487aaNKQyoFqP8vYYjWjBrwzbYtMUYhnwmHXW5IO0dG4J4m\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
},
"instances": [
{
@ -462,7 +462,7 @@
"bundle": "doxa-simple-map",
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"output": "dx-maps/doxa-simple-map/doxa-simple-map.js",
"integrity": "sha384-uVjVvk2lTU1akrc6T0SouQ9zOIX/35Hrltso16QO4xkaN1Nyy+6QuP8BhWG/74wE",
"integrity": "sha384-9DC6IjuVnKHJrXixRzkEyt4XKBfhkhD4xT0aAcXzOJWSMSZFsoA4576Fn6+C0nMV",
"elements": [
"doxa-map",
"doxa-simple-map"
@ -482,8 +482,8 @@
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-uVjVvk2lTU1akrc6T0SouQ9zOIX/35Hrltso16QO4xkaN1Nyy+6QuP8BhWG/74wE\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-uVjVvk2lTU1akrc6T0SouQ9zOIX/35Hrltso16QO4xkaN1Nyy+6QuP8BhWG/74wE\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-9DC6IjuVnKHJrXixRzkEyt4XKBfhkhD4xT0aAcXzOJWSMSZFsoA4576Fn6+C0nMV\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>",
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-9DC6IjuVnKHJrXixRzkEyt4XKBfhkhD4xT0aAcXzOJWSMSZFsoA4576Fn6+C0nMV\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
},
"instances": [
{
@ -859,7 +859,7 @@
},
{
"profile": "resources-map",
"stage": "experimental",
"stage": "production",
"label": "Gospel Resources — resources map",
"file": "resources-map.vue",
"description": "12,379 people-group pins over a country choropleth: tap a pin for the Scripture, audio, film, Bible-story and training links that exist in that group's own language with real URLs, tap a country for every group in it, search by group, language or ISO 639-3 code, and filter to the groups where nothing was found.",
@ -1001,24 +1001,6 @@
"embed": {
"iframe": "<iframe src=\"./people-groups-grid/people-groups-grid/driven/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
"id": "regions-look",
"title": "People Groups Grid — the regions-dashboard look (link toggle + root watermark)",
"apiBaseUrl": "",
"props": {
"profile": "people-groups-grid",
"provider": "maplibre",
"instanceId": "people-groups-grid-regions-look",
"pray-base-url": "https://pray.doxa.life",
"filters-toggle": "link",
"backdrop": "root"
},
"element": "people-groups-grid",
"iframeUrl": "./people-groups-grid/people-groups-grid/regions-look/index.html",
"embed": {
"iframe": "<iframe src=\"./people-groups-grid/people-groups-grid/regions-look/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
}
]
}

View file

@ -1,244 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
<script>window.MAP_OWNS_PAGE = true;</script>
<title>People Groups Grid — the regions-dashboard look (link toggle + root watermark) · people-groups-grid</title>
<link rel="icon" href="data:,">
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
<style>
/* BARE full-page map — no chrome. The custom element IS the page, so the
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, people-groups-grid { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
padding: 24px; text-align: center; color: #9aa7b8;
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
#status.err { color: #f87171; }
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
Two things every generated leaf got wrong on paper, because no generated page has ever
carried an @media print rule (measured: 0 across the tree).
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
flooded black, and that is the reader who most wants the printout.
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
a full sheet reading "loading…" above the content that did in fact load. It is hidden
only while it is NOT an error — an error still prints, because a blank sheet with the
explanation suppressed is worse than a sheet that says what went wrong. */
@media print {
html, body { height: auto; background: #fff; }
#status:not(.err) { display: none; }
/* ★ RELEASE THE FIXED HOST, MEASURED (rega, RQ-RGPRINT on a035408e2). The leaf pins both the
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
then blank to the bottom of the sheet.
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
so height:auto with nothing else can leave the element a zero-height box and print an empty
page — a worse failure than the one being fixed, and the discriminator is not isMap
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
box for the map case while letting a taller document grow onto page two. */
#slot, people-groups-grid { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
}
</style>
</head>
<body>
<div id="slot"></div>
<div id="status">loading…</div>
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <people-groups-grid> custom element. -->
<script src="../../people-groups-grid.js" integrity="sha384-WuJGImacQL5qJH3B0VNeB4yo1QuLbbnWI1y1YfZmKkO7deeYxuCkR+pTktzqp2Oh" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
// 2. ?tk= URL param — token carried in the shared link
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
// *endpoint* is resolved and fetched:
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
// means "read that global"; anything else is used as a URL — so both a global
// name and a plain URL are safe (no SyntaxError footgun).
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"people-groups-grid","provider":"maplibre","instanceId":"people-groups-grid-regions-look","pray-base-url":"https://pray.doxa.life","filters-toggle":"link","backdrop":"root"};
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
// the ladder below is skipped entirely and the element mounts immediately.
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
var TOKEN_FREE = true;
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
// rather than this page's profile-config. With no baked token (the public copy), it must get
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
var NESTS_MAPS = false;
var TOKEN_SRC = "window.MAP_TOKEN_URL";
var BAKED_TOKEN = "";
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
var DEFAULT_TOKEN_URL = '/api/maps/token';
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
function askToken() {
if (!status) return;
status.className = 'err';
status.innerHTML = 'map needs a Mapbox token — ' +
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
var go = document.getElementById('tkGo');
if (go) go.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (!v) return;
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
mount(v);
});
}
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
mounted element does not cover shows it through, because every layer above body is
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
unpainted canvas as first supposed. Clearing it here (the one place we already know the
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
function done() {
if (status && status.parentNode) { status.parentNode.removeChild(status); }
document.documentElement.style.background = '#f3f3f1';
document.body.style.background = '#f3f3f1';
}
function mount(token) {
if (!token && !TOKEN_FREE) { return askToken(); }
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
try {
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
var el = document.createElement("people-groups-grid");
el.setAttribute('profile-config', JSON.stringify(cfg));
slot.innerHTML = '';
slot.appendChild(el);
done();
} catch (e) { fail(e && e.message ? e.message : String(e)); }
}
function qp(name) {
try { return new URLSearchParams(location.search).get(name) || ''; }
catch (e) { return ''; }
}
function extractToken(data) {
if (typeof data === 'string') return data.trim();
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
return '';
}
// Rungs 1-3b: an actual token → mount directly, no fetch.
// 2b = localStorage: the paste-once store shared with the staging pages
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
var stored = '';
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
var urlTk = qp('tk');
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
if (TOKEN_FREE) {
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
mount('');
// A token-free composite with no token anywhere: its content renders, but its nested maps
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
var bar = document.createElement('div');
bar.id = 'status';
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
document.body.appendChild(bar);
status = bar;
askToken();
var goBtn = document.getElementById('tkGo');
if (goBtn) goBtn.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (v) { location.reload(); }
});
}
return;
}
if (directToken) { return mount(directToken); }
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
// A single hard-coded pick used to kill the page when its one url 404'd or was
// blocked by CORS; a shared link must work wherever the tree is hosted.
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function fetchToken(i, errors) {
if (i >= candidates.length) {
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
askToken();
if (status) {
var d = document.createElement('div');
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
status.insertBefore(d, status.firstChild);
}
return;
}
var url = candidates[i];
return fetch(url)
.then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
var ct = r.headers.get('content-type') || '';
return ct.indexOf('json') !== -1 ? r.json() : r.text();
})
.then(function (data) {
var tk = extractToken(data);
if (!tk) throw new Error(url + ' → no token in response');
mount(tk);
})
.catch(function (e) {
errors.push(e && e.message ? e.message : String(e));
return fetchToken(i + 1, errors);
});
}
fetchToken(0, []);
})();
</script>
</body>
</html>

File diff suppressed because one or more lines are too long