Update from source fc2fa2e4
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

This commit is contained in:
Doxa Maps 2026-09-26 08:25:21 +00:00
parent 6ed22cdba7
commit 8757b31228
53 changed files with 485 additions and 230 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 926 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 827 KiB

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 954 KiB

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 KiB

After

Width:  |  Height:  |  Size: 771 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 KiB

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 KiB

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -1,23 +1,23 @@
{ {
"cap1-single-element": { "cap1-single-element": {
"bundle": "all-people-groups", "bundle": "all-people-groups",
"sha256": "483a04b2ae46e00b0e0530c9291070b30c9baeaddf2940cd61e04cdffb382095" "sha256": "9e94ebdea30cb458a3bf6d28342ecbe7c4f403157b27b3b0bde9543a1882ac53"
}, },
"cap2-adoption": { "cap2-adoption": {
"bundle": "doxa-simple-map", "bundle": "doxa-simple-map",
"sha256": "646efc14670963e1d3e1b1982ff2130562f28183a5908ef3c0c5dbd8d7f9597a" "sha256": "e429739d0fe8ca87943f8e7c1192193d0979a6a7be9d3aada3229833d722a9ec"
}, },
"cap2-engagement": { "cap2-engagement": {
"bundle": "doxa-simple-map", "bundle": "doxa-simple-map",
"sha256": "646efc14670963e1d3e1b1982ff2130562f28183a5908ef3c0c5dbd8d7f9597a" "sha256": "e429739d0fe8ca87943f8e7c1192193d0979a6a7be9d3aada3229833d722a9ec"
}, },
"cap2-prayer": { "cap2-prayer": {
"bundle": "doxa-simple-map", "bundle": "doxa-simple-map",
"sha256": "646efc14670963e1d3e1b1982ff2130562f28183a5908ef3c0c5dbd8d7f9597a" "sha256": "e429739d0fe8ca87943f8e7c1192193d0979a6a7be9d3aada3229833d722a9ec"
}, },
"cap3-monolithic-research": { "cap3-monolithic-research": {
"bundle": "doxa-research-map", "bundle": "doxa-research-map",
"sha256": "9a7161f8337ff9efffcf4c8092157b5a8932de91c69fb541672f92c040033716" "sha256": "11ea5ecc91ecdbfd16e0638cfceecf9c835e0a88f3de2a2aea4fb282828c287f"
}, },
"cap4-upg100-list": { "cap4-upg100-list": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
@ -25,78 +25,78 @@
}, },
"cap4-upg100-mfe": { "cap4-upg100-mfe": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "eca013c97650809080806967337b54bbdf5c5817dc318554ccffc00fc66c601f" "sha256": "a7bbbab52d5418e2745215ef204ac548daa551335859b5f58ed81fd611ed850b"
}, },
"cap4-upg100-poster-wall": { "cap4-upg100-poster-wall": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "eca013c97650809080806967337b54bbdf5c5817dc318554ccffc00fc66c601f" "sha256": "a7bbbab52d5418e2745215ef204ac548daa551335859b5f58ed81fd611ed850b"
}, },
"cap5-doxa-maps-spa": { "cap5-doxa-maps-spa": {
"bundle": "doxa-maps-spa", "bundle": "doxa-maps-spa",
"sha256": "9093c700a38d42a9444650690d99ce3b8b66f0c73e03e00ac49780c3833807c5" "sha256": "a8b01a95f0362c7a97fa510eb41e668abe0b13671abc3bb174ae2b97dda8280d"
}, },
"cap5-plugin-studio": { "cap5-plugin-studio": {
"bundle": "plugin-studio", "bundle": "plugin-studio",
"sha256": "0034c901862e25c575966dd48f78d2fa739834db401cf37f1de9653e3034ae05" "sha256": "9108858e3f055a0b09a56206d7f0354e2f1f2b8a5d80d5f6f9fe12eea54ed484"
}, },
"deploy2-mobile-upg100": { "deploy2-mobile-upg100": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "eca013c97650809080806967337b54bbdf5c5817dc318554ccffc00fc66c601f" "sha256": "a7bbbab52d5418e2745215ef204ac548daa551335859b5f58ed81fd611ed850b"
}, },
"exp-prayer-walk-planner": { "exp-prayer-walk-planner": {
"bundle": "prayer-walk-planner", "bundle": "prayer-walk-planner",
"sha256": "0042dda28574e86cb29543b53ece4abab835bccb995316470e138479e42de2b3" "sha256": "5f0e799cf04331c65e7724820ae34b96ee5a4c0583dd66b5a40c7ea106822acf"
}, },
"exp-wall-of-people-groups": { "exp-wall-of-people-groups": {
"bundle": "wall-of-people-groups", "bundle": "wall-of-people-groups",
"sha256": "1ce6d49a495aa794200b97005e620db37fca3ade59325fa359d3a59d200cbcba" "sha256": "9a8fc9c4a39c7113590a9dab8e9e3f5f4ecaa274467ab1d095fb717e60ac5765"
}, },
"lang-arabic-upg100": { "lang-arabic-upg100": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "eca013c97650809080806967337b54bbdf5c5817dc318554ccffc00fc66c601f" "sha256": "a7bbbab52d5418e2745215ef204ac548daa551335859b5f58ed81fd611ed850b"
}, },
"prod-big-picture": { "prod-big-picture": {
"bundle": "dashboard-plus-grid", "bundle": "dashboard-plus-grid",
"sha256": "e8e78401cbc35ab285d6906f7f9ccbe4bc9db7a41c37dd1cde301addda7cd745" "sha256": "fb9da1169851cc2ab49c4c1f289935367841f2367fe071d74c01849f45337ffa"
}, },
"prod-cities-internet": { "prod-cities-internet": {
"bundle": "cities-internet", "bundle": "cities-internet",
"sha256": "236a1a98ec06562224f77af8ca8290e4381b4df8e81aa143ecb886f5f14f7113" "sha256": "35d8936e0164f94a41d5330f53105dcabf41aefc356090743144c0fce4847bd1"
}, },
"prod-cities-internet-phone": { "prod-cities-internet-phone": {
"bundle": "cities-internet", "bundle": "cities-internet",
"sha256": "236a1a98ec06562224f77af8ca8290e4381b4df8e81aa143ecb886f5f14f7113" "sha256": "35d8936e0164f94a41d5330f53105dcabf41aefc356090743144c0fce4847bd1"
}, },
"prod-gospel-resources": { "prod-gospel-resources": {
"bundle": "gospel-resources", "bundle": "gospel-resources",
"sha256": "88d7705148ff7f48862ef5be164b8dfc5b8d71afa04a9e86bb0fdb499d830109" "sha256": "44e9f631ec66d74b50f142811424583cb4df946c725c3264a02f8306db35096d"
}, },
"prod-gospel-resources-phone": { "prod-gospel-resources-phone": {
"bundle": "gospel-resources", "bundle": "gospel-resources",
"sha256": "88d7705148ff7f48862ef5be164b8dfc5b8d71afa04a9e86bb0fdb499d830109" "sha256": "44e9f631ec66d74b50f142811424583cb4df946c725c3264a02f8306db35096d"
}, },
"prod-prayer-phone": { "prod-prayer-phone": {
"bundle": "doxa-simple-map", "bundle": "doxa-simple-map",
"sha256": "646efc14670963e1d3e1b1982ff2130562f28183a5908ef3c0c5dbd8d7f9597a" "sha256": "e429739d0fe8ca87943f8e7c1192193d0979a6a7be9d3aada3229833d722a9ec"
}, },
"prod-regions": { "prod-regions": {
"bundle": "regions-dashboard", "bundle": "regions-dashboard",
"sha256": "85a9367643be850d554fbcc704ab57f850e30388ef6cfe5e001032bc3f323f0c" "sha256": "b036622f55012b6917bf691d87f738d9f64c1705c5ee78db2127f57c315e41d1"
}, },
"prod-regions-phone": { "prod-regions-phone": {
"bundle": "regions-dashboard", "bundle": "regions-dashboard",
"sha256": "85a9367643be850d554fbcc704ab57f850e30388ef6cfe5e001032bc3f323f0c" "sha256": "b036622f55012b6917bf691d87f738d9f64c1705c5ee78db2127f57c315e41d1"
}, },
"prod-research-gallery-phone": { "prod-research-gallery-phone": {
"bundle": "doxa-maps-spa", "bundle": "doxa-maps-spa",
"sha256": "9093c700a38d42a9444650690d99ce3b8b66f0c73e03e00ac49780c3833807c5" "sha256": "a8b01a95f0362c7a97fa510eb41e668abe0b13671abc3bb174ae2b97dda8280d"
}, },
"prod-simple-research": { "prod-simple-research": {
"bundle": "research-map-plus-grid", "bundle": "research-map-plus-grid",
"sha256": "e1f8190ed14f461801753c2743d32537fde1094e9a26555b3b40d1e22863bfae" "sha256": "a393659aa63df90410b93cfb1625fcc50cb9fe2b639c4ac286faedbaf658a078"
}, },
"prod-simple-research-phone": { "prod-simple-research-phone": {
"bundle": "research-map-plus-grid", "bundle": "research-map-plus-grid",
"sha256": "e1f8190ed14f461801753c2743d32537fde1094e9a26555b3b40d1e22863bfae" "sha256": "a393659aa63df90410b93cfb1625fcc50cb9fe2b639c4ac286faedbaf658a078"
} }
} }

View file

@ -130,8 +130,6 @@ const NO_LINKS = {
// ── D: late getCurrentInstance() sites already routed to an owner ───────────────────────────── // ── D: late getCurrentInstance() sites already routed to an owner ─────────────────────────────
const LATE_KNOWN = { const LATE_KNOWN = {
// emitFilterState runs from a watcher, so `map-filtered` is never dispatched (map -> grid dead).
[`${PD}/doxa-research-map/research-map.vue#emitFilterState`]: 'doxa-research-map owner, routed via dx-shepherd 2026-09-26',
// reflectDir runs from watch(textDir): a host switching lang to ar AFTER mount gets no dir=rtl. // reflectDir runs from watch(textDir): a host switching lang to ar AFTER mount gets no dir=rtl.
[`${PD}/dashboard-plus-grid/dashboard-plus-grid.vue#reflectDir`]: 'composite owner, routed via dx-shepherd 2026-09-26', [`${PD}/dashboard-plus-grid/dashboard-plus-grid.vue#reflectDir`]: 'composite owner, routed via dx-shepherd 2026-09-26',
[`${PD}/research-map-plus-grid/research-map-plus-grid.vue#reflectDir`]: 'composite owner, routed via dx-shepherd 2026-09-26', [`${PD}/research-map-plus-grid/research-map-plus-grid.vue#reflectDir`]: 'composite owner, routed via dx-shepherd 2026-09-26',

View file

@ -63,17 +63,61 @@ if (!existsSync(BUILT)) {
process.exit(0) process.exit(0)
} }
/**
* The hexes of the PALETTE export ONLY: from `export const PALETTE` to its matching closing brace, and
* only inside string literals. It used to match every hex from the export to the END OF THE FILE,
* comments included, so a later note such as "REJECTED #AF7AC5, scored 2.95, under the 3.0 floor"
* (affinity-block.js) obliged every consumer to ship a colour rejected on WCAG grounds.
* Returns null when the module has no PALETTE export.
*/
export function paletteHexes(src) {
const at = src.indexOf('export const PALETTE')
if (at < 0) return null
const open = src.indexOf('{', at), openArr = src.indexOf('[', at)
const start = [open, openArr].filter((i) => i >= 0).sort((a, b) => a - b)[0]
if (start === undefined) return []
const close = { '{': '}', '[': ']' }[src[start]]
let depth = 0, i = start, quote = null
const strings = []
let cur = ''
for (; i < src.length; i++) {
const c = src[i], n = src[i + 1]
if (quote) {
if (c === '\\') { cur += c + n; i++; continue }
if (c === quote) { strings.push(cur); cur = ''; quote = null; continue }
cur += c; continue
}
if (c === '/' && n === '/') { i = src.indexOf('\n', i); if (i < 0) break; continue }
if (c === '/' && n === '*') { i = src.indexOf('*/', i + 2) + 1; if (i <= 0) break; continue }
if (c === "'" || c === '"' || c === '`') { quote = c; continue }
if (c === src[start]) depth++
else if (c === close && --depth === 0) break
}
return [...new Set(strings.flatMap((t) => [...t.matchAll(/#[0-9a-fA-F]{6}\b/g)].map((m) => m[0].toLowerCase())))]
}
if (process.argv.includes('--self-test')) {
const src = [
"export const PALETTE = {",
" a: '#112233', // not #445566 (a comment inside the export)",
" /* nor #778899 */ b: \"#aabbcc\",",
"}",
"// REJECTED #af7ac5, scored 2.95, under the 3.0 floor",
"export const OTHER = { c: '#ddeeff' }",
].join('\n')
const got = paletteHexes(src).join(',')
if (got !== '#112233,#aabbcc') { console.error(`ONE-PALETTE: SELF-TEST FAIL — read ${got}, want #112233,#aabbcc`); process.exit(1) }
console.log('ONE-PALETTE: SELF-TEST PASS — only the PALETTE export\'s string values are read; comments and later code are not.')
process.exit(0)
}
// ── the expected palettes, read from the strategy modules the MAP uses ──────────────────────── // ── the expected palettes, read from the strategy modules the MAP uses ────────────────────────
const strategies = {} const strategies = {}
for (const f of readdirSync(COLORS)) { for (const f of readdirSync(COLORS)) {
if (!f.endsWith('.js') || f.startsWith('_') || f.startsWith('.')) continue if (!f.endsWith('.js') || f.startsWith('_') || f.startsWith('.')) continue
const src = readFileSync(join(COLORS, f), 'utf8') const src = readFileSync(join(COLORS, f), 'utf8')
// Only the PALETTE export — a module may also contain incidental hexes (a default, a comment) const hexes = paletteHexes(src)
// that no consumer is obliged to ship. if (hexes && hexes.length) strategies[f.replace(/\.js$/, '')] = hexes
const at = src.indexOf('export const PALETTE')
if (at < 0) continue
const hexes = [...new Set([...src.slice(at).matchAll(/#[0-9a-fA-F]{6}/g)].map((m) => m[0].toLowerCase()))]
if (hexes.length) strategies[f.replace(/\.js$/, '')] = hexes
} }
// ── the consumers that must agree ───────────────────────────────────────────────────────────── // ── the consumers that must agree ─────────────────────────────────────────────────────────────

View file

@ -11,6 +11,9 @@
* Every one of those landings passed its own checks. Nothing refused them. * Every one of those landings passed its own checks. Nothing refused them.
* *
* PRECONDITIONS, in order (cheap first): * PRECONDITIONS, in order (cheap first):
* 0. NO MERGE MARKERS no file in the COMMIT (public/ included) holds a conflict marker. d6bf410a put them
* into 10 built files under public/ and every other precondition passed; a built bundle with
* markers throws on load while its SRI still matches. --markers-self-test proves the scan refuses.
* 1. CLEAN the working tree equals HEAD for everything the checks read — otherwise they would judge * 1. CLEAN the working tree equals HEAD for everything the checks read — otherwise they would judge
* files that are not in the commit. * files that are not in the commit.
* 2. ONE FOLDER public/ holds exactly the configured tree (vite.doxa-maps-build-config.json "name") and * 2. ONE FOLDER public/ holds exactly the configured tree (vite.doxa-maps-build-config.json "name") and
@ -20,6 +23,7 @@
* 4. FRESH every composite rebuilds to exactly its committed bytes (check:composite-freshness). * 4. FRESH every composite rebuilds to exactly its committed bytes (check:composite-freshness).
* *
* USAGE node internal/landing-preconditions.mjs exit 0 = may land · 1 = refused * USAGE node internal/landing-preconditions.mjs exit 0 = may land · 1 = refused
* node internal/landing-preconditions.mjs --markers-self-test proves step 0 refuses a conflict
* DOXA_SKIP_FRESHNESS=1 skips (4) — for a DRY check only; the merge-queue check never sets it. * DOXA_SKIP_FRESHNESS=1 skips (4) — for a DRY check only; the merge-queue check never sets it.
*/ */
import { spawnSync } from 'node:child_process' import { spawnSync } from 'node:child_process'
@ -36,6 +40,30 @@ const refuse = (why, detail = '') => {
} }
const run = (cmd, args) => spawnSync(cmd, args, { cwd: ROOT, encoding: 'utf8', env: process.env }) const run = (cmd, args) => spawnSync(cmd, args, { cwd: ROOT, encoding: 'utf8', env: process.env })
// 0. NO MERGE MARKERS — scanned in the COMMIT's tree (git grep), so public/ and every file is covered,
// whatever the working tree holds. Markers are built from char codes so this file never contains one.
const OPEN = String.fromCharCode(60).repeat(7), MID = String.fromCharCode(61).repeat(7), CLOSE = String.fromCharCode(62).repeat(7)
function markerFiles(ref) {
const g = run('git', ['grep', '-I', '-l', '-E', '-e', `^(${OPEN}( |$)|${MID}$|${CLOSE}( |$))`, ref, '--'])
if (g.status > 1) refuse('could not scan the commit for merge markers', g.stderr.trim())
return g.stdout.split('\n').filter(Boolean).map((l) => l.replace(new RegExp('^' + ref + ':'), ''))
}
if (process.argv.includes('--markers-self-test')) {
// A throwaway commit object (no branch, no working-tree change) holding one built file with a textbook
// conflict; the scan must name it.
const blob = spawnSync('git', ['hash-object', '-w', '--stdin'], { cwd: ROOT, encoding: 'utf8', input: `a\n${OPEN} HEAD\nmine\n${MID}\ntheirs\n${CLOSE} other\n` }).stdout.trim()
const tree = spawnSync('git', ['mktree'], { cwd: ROOT, encoding: 'utf8', input: `100644 blob ${blob}\tpoison.js\n` }).stdout.trim()
const pub = spawnSync('git', ['mktree'], { cwd: ROOT, encoding: 'utf8', input: `040000 tree ${tree}\tpublic\n` }).stdout.trim()
const commit = spawnSync('git', ['commit-tree', pub, '-m', 'marker self-test'], { cwd: ROOT, encoding: 'utf8' }).stdout.trim()
const hits = markerFiles(commit)
if (hits.includes('public/poison.js')) { console.log('LANDING-PRECONDITIONS: markers self-test PASS — a conflict in a public/ file is refused.'); process.exit(0) }
console.error('LANDING-PRECONDITIONS: markers self-test FAIL — a textbook conflict under public/ was not found.'); process.exit(1)
}
const markers = markerFiles('HEAD')
if (markers.length) refuse(`${markers.length} file(s) in this commit hold merge-conflict markers`,
markers.slice(0, 10).join('\n') + '\nFor a BUILT file: take either side, REBUILD it, commit the rebuild. Never `git add -A public` mid-merge.')
console.log('LANDING-PRECONDITIONS: 0 markers — no file in the commit holds a merge-conflict marker')
// 1. CLEAN // 1. CLEAN
const dirty = run('git', ['status', '--porcelain', '--', 'public', 'plugin-designs', 'library', 'internal', 'vite.config.js', 'vite.doxa-maps-build-config.json', 'package.json']) const dirty = run('git', ['status', '--porcelain', '--', 'public', 'plugin-designs', 'library', 'internal', 'vite.config.js', 'vite.doxa-maps-build-config.json', 'package.json'])
if (dirty.status !== 0) refuse('could not read git status') if (dirty.status !== 0) refuse('could not read git status')

View file

@ -132,6 +132,7 @@
"check:no-orphan-gates": "node internal/check-no-orphan-gates.mjs", "check:no-orphan-gates": "node internal/check-no-orphan-gates.mjs",
"check:no-public-binds": "node internal/check-no-public-binds.mjs", "check:no-public-binds": "node internal/check-no-public-binds.mjs",
"check:one-palette": "node internal/check-one-palette.mjs", "check:one-palette": "node internal/check-one-palette.mjs",
"check:one-palette:self-test": "node internal/check-one-palette.mjs --self-test",
"check:packet-browser": "node internal/check-packet-browser.mjs", "check:packet-browser": "node internal/check-packet-browser.mjs",
"check:packet-fixture": "node internal/generate-packet-fixture.mjs --check", "check:packet-fixture": "node internal/generate-packet-fixture.mjs --check",
"check:packet-render": "node internal/check-packet-render.mjs", "check:packet-render": "node internal/check-packet-render.mjs",
@ -199,7 +200,7 @@
"dev": "bun x vite", "dev": "bun x vite",
"fix:sri": "node internal/fix-sri.mjs", "fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs", "gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check: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: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", "gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check: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: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",
"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", "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",
"library-index": "node internal/generate-library-index.mjs", "library-index": "node internal/generate-library-index.mjs",
"lists": "node library/scripts/people-groups/build-lists.mjs", "lists": "node library/scripts/people-groups/build-lists.mjs",

View file

@ -49,6 +49,7 @@
* Becomes tab-1 of the Cat-5 SPA. * Becomes tab-1 of the Cat-5 SPA.
*/ */
import { computed, reactive, ref, watchEffect } from 'vue' import { computed, reactive, ref, watchEffect } from 'vue'
import { tStatic } from '@map/i18n/index.js'
import { SUPPORTED_LOCALES, setRuntimeLocale } from '@map/i18n/index.js' import { SUPPORTED_LOCALES, setRuntimeLocale } from '@map/i18n/index.js'
import BpResearchHeader from './src/components/BpResearchHeader.vue' import BpResearchHeader from './src/components/BpResearchHeader.vue'
// The REAL cap-2 grid — same component the standalone <people-groups-grid> ships (no fork). // The REAL cap-2 grid — same component the standalone <people-groups-grid> ships (no fork).
@ -152,7 +153,7 @@ function onGroupsFiltered(payload) {
architecture note (header↔grid via props/events, no shared bus, tab-1 of the SPA) architecture note (header↔grid via props/events, no shared bus, tab-1 of the SPA)
lives in this file's header block, not in the UI where an end user reads it. --> lives in this file's header block, not in the UI where an end user reads it. -->
<footer class="bp-foot"> <footer class="bp-foot">
Search and filter the world's people groups · least-reached shown first {{ tStatic('dashboard.bp_subtitle') || "Search and filter the world's people groups · least-reached shown first" }}
</footer> </footer>
</div> </div>
</template> </template>

View file

@ -154,6 +154,11 @@ import { useLegendData } from '@map/composables/legends/useLegendData.js'
import { mapDefaults } from '@map/constants/mapDefaults.js' import { mapDefaults } from '@map/constants/mapDefaults.js'
import { getColorStrategy } from '@map/colors/_registry.js' import { getColorStrategy } from '@map/colors/_registry.js'
import { RELIGION_FAMILIES } from '@map/colors/religion.js' import { RELIGION_FAMILIES } from '@map/colors/religion.js'
import { tStatic } from '@map/i18n/index.js'
// This map's own chrome, read out of the catalogue. tStatic and not useI18n: this component is the
// root of a custom element, where useI18n() cannot resolve its injection and throws.
const T = (key, en) => tStatic(key) || en
import { DataSourceManager } from '@map/utils/DataSourceManager.js' import { DataSourceManager } from '@map/utils/DataSourceManager.js'
import sourcesConfig from '@map/api/sources.json' import sourcesConfig from '@map/api/sources.json'
@ -2136,10 +2141,29 @@ if (typeof window !== 'undefined') {
} }
} }
// ★ THE INSTANCE IS CAPTURED IN SETUP, BECAUSE A WATCHER IS NOT SETUP.
// This read getCurrentInstance() INSIDE the function, and the function is only ever called from a
// watcher callback (just below). Vue only returns an instance during setup — from a watcher it is
// null — so `host` was null on every call, emitFilterState returned at the guard, and the
// `map-filtered` event this map owes its composite was NEVER dispatched. The whole map -> grid
// direction was dead.
//
// It looked verified because the check that "proved" it DISPATCHED THE EVENT ITSELF and then
// asserted the grid reacted. That tested the composite's listener and the grid, which do work; it
// never tested the one line that emits. A synthetic event cannot prove an emitter fires — it
// replaces it.
//
// Captured here, at setup top level, and resolved to the host lazily so it also survives being
// called before mount.
const _selfInstance = getCurrentInstance()
function selfHostElement() {
try { return _selfInstance?.proxy?.$el?.getRootNode?.()?.host || null } catch (_) { return null }
}
// Report OUR filter state upward, so a composite can drive its other half with it. // Report OUR filter state upward, so a composite can drive its other half with it.
function emitFilterState() { function emitFilterState() {
if (applyingExternal || typeof window === 'undefined') return if (applyingExternal || typeof window === 'undefined') return
const host = getCurrentInstance()?.proxy?.$el?.getRootNode?.()?.host const host = selfHostElement()
if (!host) return if (!host) return
// Report in the vocabulary a HOST speaks: the family NAME, not the legend's letter key. Same // Report in the vocabulary a HOST speaks: the family NAME, not the legend's letter key. Same
// shared RELIGION_FAMILIES table the inbound direction reads, used the other way round. // shared RELIGION_FAMILIES table the inbound direction reads, used the other way round.
@ -2250,18 +2274,36 @@ function applyFitSlugs(slugs) {
} }
if (!n) return // nothing this map holds — say nothing rather than fly nowhere if (!n) return // nothing this map holds — say nothing rather than fly nowhere
const duration = prefersReducedMotion() ? 0 : FLY_DURATION_MS
const spanLng = maxLng - minLng const spanLng = maxLng - minLng
const spanLat = maxLat - minLat const spanLat = maxLat - minLat
if (spanLng > WIDE_SPAN_DEG) { const centre = { longitude: (minLng + maxLng) / 2, latitude: (minLat + maxLat) / 2 }
mapFly.flyToCoords?.({ longitude: (minLng + maxLng) / 2, latitude: (minLat + maxLat) / 2 }, WORLD_ZOOM, { duration }) const point = spanLng > WIDE_SPAN_DEG ? { c: centre, z: WORLD_ZOOM }
return : (n === 1 || (spanLng < DEGENERATE_SPAN_DEG && spanLat < DEGENERATE_SPAN_DEG))
? { c: { longitude: minLng, latitude: minLat }, z: SINGLE_ROW_ZOOM }
: null
// ★ REDUCED MOTION DRIVES THE MAP DIRECTLY, and that is not belt-and-braces.
// MEASURED by rega with the browser's prefers-reduced-motion: reduce set: the framing move still
// animated for ~5s on MapLibre and ~6.7s on Mapbox — essentially the same as without it, where a
// jump is ~0ms. So the preference was being honoured in THIS file and thrown away downstream:
// useMapFly's flyToCoords(coords, zoom) takes NO options argument at all, so the `{ duration: 0 }`
// I was passing it went nowhere. (fitBounds does accept one; flyToCoords does not — a difference
// between two neighbours in one file, invisible at the call site.)
//
// A photosensitive reader is the whole reason this branch exists, so it no longer depends on
// whether a helper happens to forward an option: it calls the engine's own jump. The eased paths
// are unchanged and still go through mapFly, so ordinary motion keeps one owner.
const m = map.value
if (prefersReducedMotion() && m) {
try {
if (point) m.jumpTo({ center: [point.c.longitude, point.c.latitude], zoom: point.z })
else m.fitBounds([[minLng, minLat], [maxLng, maxLat]], { padding: FIT_PADDING_PX, maxZoom: FIT_MAX_ZOOM, duration: 0 })
return
} catch (_) { /* fall through to the eased path rather than not moving at all */ }
} }
if (n === 1 || (spanLng < DEGENERATE_SPAN_DEG && spanLat < DEGENERATE_SPAN_DEG)) {
mapFly.flyToCoords?.({ longitude: minLng, latitude: minLat }, SINGLE_ROW_ZOOM, { duration }) if (point) { mapFly.flyToCoords?.(point.c, point.z); return }
return mapFly.fitBounds?.([[minLng, minLat], [maxLng, maxLat]], FIT_PADDING_PX, { maxZoom: FIT_MAX_ZOOM, duration: FLY_DURATION_MS })
}
mapFly.fitBounds?.([[minLng, minLat], [maxLng, maxLat]], FIT_PADDING_PX, { maxZoom: FIT_MAX_ZOOM, duration })
} }
// ─── Clusters wiring: TWO CONTROLS, ONE STATE ──────────────────────────────── // ─── Clusters wiring: TWO CONTROLS, ONE STATE ────────────────────────────────
@ -2538,8 +2580,8 @@ onBeforeUnmount(() => {
</div> </div>
<div class="rm-map-area"> <div class="rm-map-area">
<div v-if="!appReady && !mapError" class="rm-loading">Research map loading…</div> <div v-if="!appReady && !mapError" class="rm-loading">{{ T('dashboard.map_loading', 'Research map loading…') }}</div>
<div v-if="mapError" class="rm-error">The map could not be loaded. {{ mapError }}</div> <div v-if="mapError" class="rm-error">{{ T('allPeopleGroups.loadError', 'The map could not be loaded.') }} {{ mapError }}</div>
<div ref="mapContainer" class="rm-map-canvas" /> <div ref="mapContainer" class="rm-map-canvas" />
<div class="map-scroll-edge map-scroll-edge--left" /> <div class="map-scroll-edge map-scroll-edge--left" />
<div class="map-scroll-edge map-scroll-edge--right" /> <div class="map-scroll-edge map-scroll-edge--right" />

View file

@ -0,0 +1,122 @@
#!/usr/bin/env node
/**
* check-reduced-motion-jumps.mjs — with prefers-reduced-motion, the grid→map framing must JUMP.
*
* ★ THE DRIVER IS PHOTOSENSITIVE. This is not a polish item: a camera that sweeps the globe every
* time a list is filtered is the strobe class this repo has already paid for once.
*
* THE DEFECT, measured by rega in a real browser with the preference set: the framing move still
* animated for ~5.0s on MapLibre and ~6.7s on Mapbox — indistinguishable from no preference at all,
* where a jump is ~0ms. The preference WAS being read correctly in research-map.vue and then thrown
* away one layer down: useMapFly's `flyToCoords(coords, zoom)` takes NO options argument, so the
* `{ duration: 0 }` being passed to it went nowhere. Its neighbour `fitBounds(bounds, padding,
* options)` does accept one — two functions in one file, different signatures, and nothing at the
* call site to show it.
*
* ★ WHY THIS RUNS WITHOUT A BROWSER, WHICH IS THE POINT. rega found it by watching movestart →
* moveend for six seconds; only rega can do that, and only occasionally. What can be checked any
* time, in milliseconds, is the DECISION: given the preference, which camera call does the code
* make? So this lifts the real camera block out of the SFC, runs it against a mocked matchMedia and
* a recording map, and asserts the call that comes out.
*
* It cannot prove the engine honours duration 0 — that is rega's half. It proves we ASK for a jump,
* which is the half that was wrong.
*
* ASSERTIONS:
* reduced motion ON the engine's own jumpTo / fitBounds(duration:0) is called, and the eased
* helper (mapFly) is NOT — a single eased call is the whole bug.
* reduced motion OFF the eased path is used, so ordinary motion is unchanged.
* one row / a tight cluster / a globe-spanning set all take their own branch under both.
*
* Exit: 0 pass · 1 fail · 2 the camera block could not be found (reported, never silently skipped).
*/
import { readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parse as parseSFC } from '@vue/compiler-sfc'
const TAG = 'REDUCED-MOTION-JUMPS'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
const FILE = join(ROOT, 'plugin-designs', 'doxa-research-map', 'research-map.vue')
const src = readFileSync(FILE, 'utf8')
const { descriptor } = parseSFC(src, { filename: FILE })
const script = (descriptor.scriptSetup || descriptor.script || {}).content || ''
// The block is delimited by its own banner comments, so a refactor that moves it renames the
// markers with it rather than leaving this gate quietly reading the wrong lines.
const START = 'const FIT_PADDING_PX'
const END = '// ─── Clusters wiring: TWO CONTROLS, ONE STATE'
// indexOf from the START, not from 0: an identical-looking line can appear earlier in a file this
// size, and a block that runs from the right start to the wrong end parses into nonsense.
const a = script.indexOf(START)
const b = a >= 0 ? script.indexOf(END, a) : -1
if (a < 0 || b < 0 || b < a) {
console.log(`${TAG}: FAIL — could not find the camera block in research-map.vue (markers moved?).`)
process.exit(2)
}
const block = script.slice(a, b)
if (!/function applyFitSlugs/.test(block) || !/function prefersReducedMotion/.test(block)) {
console.log(`${TAG}: FAIL — the block found does not contain applyFitSlugs and prefersReducedMotion.`)
process.exit(2)
}
/** Build the block with its collaborators injected, so the REAL code runs against fakes. */
function makeCamera({ reduce }) {
const calls = []
const rec = (name) => (...args) => calls.push({ name, args })
const map = { value: { jumpTo: rec('map.jumpTo'), fitBounds: rec('map.fitBounds'), easeTo: rec('map.easeTo'), flyTo: rec('map.flyTo') } }
const mapFly = { flyToCoords: rec('mapFly.flyToCoords'), fitBounds: rec('mapFly.fitBounds'), flyTo: rec('mapFly.flyTo') }
const rows = []
const mapData = { normalizedPeopleGroups: { value: rows } }
const win = { matchMedia: (q) => ({ matches: reduce && /prefers-reduced-motion/.test(q) }) }
// eslint-disable-next-line no-new-func
const factory = new Function('map', 'mapData', 'mapFly', 'window', `${block}\n;return { applyFitSlugs, prefersReducedMotion }`)
const api = factory(map, mapData, mapFly, win)
return { ...api, calls, rows }
}
const cases = [
{ name: 'several peoples', rows: [
{ slug: 'a', longitude: 10, latitude: 5 }, { slug: 'b', longitude: 14, latitude: 9 }, { slug: 'c', longitude: 12, latitude: 7 },
], slugs: ['a', 'b', 'c'] },
{ name: 'one people', rows: [{ slug: 'a', longitude: 10, latitude: 5 }], slugs: ['a'] },
{ name: 'tight cluster', rows: [
{ slug: 'a', longitude: 10, latitude: 5 }, { slug: 'b', longitude: 10.001, latitude: 5.001 },
], slugs: ['a', 'b'] },
{ name: 'globe-spanning', rows: [
{ slug: 'a', longitude: -170, latitude: -40 }, { slug: 'b', longitude: 170, latitude: 60 },
], slugs: ['a', 'b'] },
]
const fails = []
const lines = []
for (const c of cases) {
for (const reduce of [true, false]) {
const cam = makeCamera({ reduce })
cam.rows.push(...c.rows)
cam.applyFitSlugs(c.slugs)
const names = cam.calls.map((k) => k.name)
const easedUsed = names.some((n) => n.startsWith('mapFly.'))
const jumped = names.some((n) => n === 'map.jumpTo')
|| cam.calls.some((k) => k.name === 'map.fitBounds' && k.args[1] && k.args[1].duration === 0)
lines.push(` · ${c.name.padEnd(16)} reduce=${String(reduce).padEnd(5)} → ${names.join(', ') || '(no camera call)'}`)
if (!names.length) { fails.push(`${c.name} (reduce=${reduce}) made no camera call at all.`); continue }
if (reduce) {
if (!jumped) fails.push(`${c.name}: with reduced motion the code did not ask for a jump — it called ${names.join(', ')}.`)
if (easedUsed) fails.push(`${c.name}: with reduced motion it still used the EASED helper (${names.filter((n) => n.startsWith('mapFly.')).join(', ')}) — a single eased call is the whole defect.`)
} else {
if (!easedUsed) fails.push(`${c.name}: without the preference the eased path should be used, got ${names.join(', ')} — ordinary motion must be unchanged.`)
}
}
}
for (const l of lines) console.log(l)
if (fails.length) {
console.log(`\n${TAG}: FAIL`)
for (const f of fails) console.log(` ✗ ${f}`)
console.log('\n The agent is photosensitive. A camera that sweeps on every filter is the defect.')
process.exit(1)
}
console.log(`${TAG}: PASS — reduced motion asks the engine to jump on every branch, and ordinary motion is untouched.`)

View file

@ -23,6 +23,13 @@
* DashboardSearchHeader (DOXA-LIB-3) remains available in the kit but is not mounted here. * DashboardSearchHeader (DOXA-LIB-3) remains available in the kit but is not mounted here.
*/ */
import { computed, watch, inject, ref, unref, onMounted } from 'vue' import { computed, watch, inject, ref, unref, onMounted } from 'vue'
import { tStatic } from '@map/i18n/index.js'
// This component's chrome, read out of the catalogue. tStatic and not useI18n: these mount inside
// custom elements that install no i18n plugin, and an unguarded useI18n() throws on the way in and
// takes the subtree with it. The English stays as the fallback, so a missing key degrades to what
// shipped rather than to a key name.
const T = (key, en) => tStatic(key) || en
import { useShadowStyles } from '@map/composables/useShadowStyles.js' import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { useDashboardShell, fmt } from './src/composables/useDashboardShell.js' import { useDashboardShell, fmt } from './src/composables/useDashboardShell.js'
import { useDashboardLayout } from './src/composables/useDashboardLayout.js' import { useDashboardLayout } from './src/composables/useDashboardLayout.js'
@ -732,12 +739,12 @@ onMounted(() => {
owns, so the two input methods can never disagree about the current value. --> owns, so the two input methods can never disagree about the current value. -->
<div v-show="!isNarrow && !facetCollapsed" class="rd-rz rd-rz-facet" data-resize="facet" <div v-show="!isNarrow && !facetCollapsed" class="rd-rz rd-rz-facet" data-resize="facet"
role="separator" aria-orientation="vertical" tabindex="0" role="separator" aria-orientation="vertical" tabindex="0"
title="Drag or use arrow keys to resize the filters rail" aria-label="Resize filters rail" :title="T('aria.resizeFiltersRailHint', 'Drag or use arrow keys to resize the filters rail')" :aria-label="T('aria.resizeFiltersRail', 'Resize filters rail')"
:aria-valuenow="facetW" aria-valuemin="160" aria-valuemax="420" :aria-valuenow="facetW" aria-valuemin="160" aria-valuemax="420"
@pointerdown="startFacetResize" @keydown="onResizeKeydown('facet', $event)"></div> @pointerdown="startFacetResize" @keydown="onResizeKeydown('facet', $event)"></div>
<div v-show="!isNarrow && !railCollapsed" class="rd-rz rd-rz-rail" data-resize="rail" <div v-show="!isNarrow && !railCollapsed" class="rd-rz rd-rz-rail" data-resize="rail"
role="separator" aria-orientation="vertical" tabindex="0" role="separator" aria-orientation="vertical" tabindex="0"
title="Drag or use arrow keys to resize the charts rail" aria-label="Resize charts rail" :title="T('aria.resizeChartsRailHint', 'Drag or use arrow keys to resize the charts rail')" :aria-label="T('aria.resizeChartsRail', 'Resize charts rail')"
:aria-valuenow="railW" aria-valuemin="200" aria-valuemax="520" :aria-valuenow="railW" aria-valuemin="200" aria-valuemax="520"
@pointerdown="startRailResize" @keydown="onResizeKeydown('rail', $event)"></div> @pointerdown="startRailResize" @keydown="onResizeKeydown('rail', $event)"></div>
<!-- ★ FIXED (BUG-SHELL-HANDLE-BOUND-TO-WRONG-DIVIDER): this handle is the MAP/TABLE SPLIT <!-- ★ FIXED (BUG-SHELL-HANDLE-BOUND-TO-WRONG-DIVIDER): this handle is the MAP/TABLE SPLIT
@ -762,8 +769,8 @@ onMounted(() => {
band), so there is nothing for this handle to do outside page-scroll. --> band), so there is nothing for this handle to do outside page-scroll. -->
<div v-show="!isNarrow && pageScroll" class="rd-rz rd-rz-shell" data-resize="shell" <div v-show="!isNarrow && pageScroll" class="rd-rz rd-rz-shell" data-resize="shell"
role="separator" aria-orientation="horizontal" tabindex="0" role="separator" aria-orientation="horizontal" tabindex="0"
title="Drag or use arrow keys to resize the dashboard's height" :title="T('aria.resizeDashboardHeightHint', `Drag or use arrow keys to resize the dashboard's height`)"
aria-label="Resize dashboard height" :aria-label="T('aria.resizeDashboardHeight', 'Resize dashboard height')"
:aria-valuenow="shellH" aria-valuemin="400" aria-valuemax="2600" :aria-valuenow="shellH" aria-valuemin="400" aria-valuemax="2600"
@pointerdown="startShellResize" @keydown="onResizeKeydown('shell', $event)"></div> @pointerdown="startShellResize" @keydown="onResizeKeydown('shell', $event)"></div>
<div class="rd-seg" data-segmented @click="setView"> <div class="rd-seg" data-segmented @click="setView">
@ -771,8 +778,10 @@ onMounted(() => {
</div> </div>
<div v-if="selCount" class="rd-exportbar" data-export-bar @click="onBarClick"> <div v-if="selCount" class="rd-exportbar" data-export-bar @click="onBarClick">
<b>{{ selCount }} selected</b><span class="sp"></span> <b>{{ selCount }} selected</b><span class="sp"></span>
<button class="x-clr" data-bar-clear>Clear</button> <button class="x-clr" data-bar-clear>{{ T('controls.clear', 'Clear') }}</button>
<button class="x-go" data-bar-export>Export CSV ↓</button> <!-- The glyph stays a glyph and the words come from the catalogue — one spelling of this
action across the dashboard, not two. -->
<button class="x-go" data-bar-export>{{ T('dashboard.export_csv', 'Export CSV') }} ↓</button>
</div> </div>
</div> </div>
</template> </template>

View file

@ -43,7 +43,7 @@ useShadowStyles(`
</script> </script>
<template> <template>
<nav class="rd-breadcrumb" data-breadcrumb @click="onClick" aria-label="Filter breadcrumb"> <nav class="rd-breadcrumb" data-breadcrumb @click="onClick" :aria-label="tStatic('aria.filterBreadcrumb') || 'Filter breadcrumb'">
<span class="rd-crumb root" data-crumb="__root__">{{ L('world', 'World') }}</span> <span class="rd-crumb root" data-crumb="__root__">{{ L('world', 'World') }}</span>
<template v-for="(c, i) in crumbs" :key="c.k"> <template v-for="(c, i) in crumbs" :key="c.k">
<span class="rd-sep">▸</span> <span class="rd-sep">▸</span>

View file

@ -313,7 +313,7 @@ useShadowStyles(`
So the second tab renders DISABLED with an explicit reason rather than being hidden. A hidden So the second tab renders DISABLED with an explicit reason rather than being hidden. A hidden
tab is indistinguishable from an unbuilt one; a disabled tab that says why is a status report. tab is indistinguishable from an unbuilt one; a disabled tab that says why is a status report.
--> -->
<div class="rd-tabs" role="tablist" aria-label="Rail views"> <div class="rd-tabs" role="tablist" :aria-label="tStatic('aria.railViews') || 'Rail views'">
<button <button
v-for="t in tabs" :key="t.id" v-for="t in tabs" :key="t.id"
class="rd-tab" :class="{ on: activeTab === t.id, off: t.disabled }" class="rd-tab" :class="{ on: activeTab === t.id, off: t.disabled }"

View file

@ -806,10 +806,10 @@ useShadowStyles(`
<template v-if="selCount"> <template v-if="selCount">
<button class="rd-vtog" :class="{ on: onlySelected }" data-only-selected <button class="rd-vtog" :class="{ on: onlySelected }" data-only-selected
:aria-pressed="onlySelected ? 'true' : 'false'" :aria-pressed="onlySelected ? 'true' : 'false'"
title="Show only the selected people groups" @click="toggleOnlySelected">Only selected</button> :title="T('dashboard.only_selected_hint', 'Show only the selected people groups')" @click="toggleOnlySelected">{{ T('dashboard.only_selected', 'Only selected') }}</button>
<button class="rd-vtog" :class="{ on: selectedFirst }" data-selected-first <button class="rd-vtog" :class="{ on: selectedFirst }" data-selected-first
:aria-pressed="selectedFirst ? 'true' : 'false'" :aria-pressed="selectedFirst ? 'true' : 'false'"
title="Float the selected people groups to the top, keeping your column sort" @click="toggleSelectedFirst">Selected first</button> :title="T('dashboard.selected_first_hint', 'Float the selected people groups to the top, keeping your column sort')" @click="toggleSelectedFirst">{{ T('dashboard.selected_first', 'Selected first') }}</button>
</template> </template>
<span class="rd-bar-sp"></span> <span class="rd-bar-sp"></span>
<!-- <!--

View file

@ -157,13 +157,13 @@ useShadowStyles(`
<section class="rd-detail" data-region="detail-drawer"> <section class="rd-detail" data-region="detail-drawer">
<div class="rd-dh"> <div class="rd-dh">
<span>{{ T('dashboard.detail', 'Detail') }}</span> <span>{{ T('dashboard.detail', 'Detail') }}</span>
<button v-if="record" class="rd-detail-close" type="button" aria-label="Close detail" data-detail-close @click="clearDetail">✕</button> <button v-if="record" class="rd-detail-close" type="button" :aria-label="T('aria.closeDetail', 'Close detail')" data-detail-close @click="clearDetail">✕</button>
</div> </div>
<div v-if="!record" class="rd-empty" data-detail-empty>{{ T('dashboard.detail_empty', 'Select a people group to see detail') }}</div> <div v-if="!record" class="rd-empty" data-detail-empty>{{ T('dashboard.detail_empty', 'Select a people group to see detail') }}</div>
<template v-else> <template v-else>
<div class="rd-top"> <div class="rd-top">
<img v-if="showPhoto" class="rd-thumb" :src="record.image_url" :alt="record.name" data-detail-thumb /> <img v-if="showPhoto" class="rd-thumb" :src="record.image_url" :alt="record.name" data-detail-thumb />
<div v-else class="rd-ph" data-detail-thumb>No photo</div> <div v-else class="rd-ph" data-detail-thumb>{{ T('dashboard.no_photo', 'No photo') }}</div>
<div> <div>
<div class="rd-name" data-detail-name>{{ record.name || '—' }}</div> <div class="rd-name" data-detail-name>{{ record.name || '—' }}</div>
<div class="rd-meta">{{ lbl(record.country_code) || '—' }} · {{ lbl(record.religion) || '—' }}</div> <div class="rd-meta">{{ lbl(record.country_code) || '—' }} · {{ lbl(record.religion) || '—' }}</div>
@ -180,14 +180,14 @@ useShadowStyles(`
</a> </a>
</div> </div>
<div class="rd-rows"> <div class="rd-rows">
<div class="rd-row"><span class="k">Language</span><span class="v">{{ lbl(record.primary_language) || '—' }}<span v-if="record.imb_language_family" class="fam"> ({{ record.imb_language_family }})</span></span></div> <div class="rd-row"><span class="k">{{ T('dashboard.language', 'Language') }}</span><span class="v">{{ lbl(record.primary_language) || '—' }}<span v-if="record.imb_language_family" class="fam"> ({{ record.imb_language_family }})</span></span></div>
<div class="rd-row"><span class="k">Population</span><span class="v">{{ fmt(record.population) }}</span></div> <div class="rd-row"><span class="k">{{ T('dashboard.population', 'Population') }}</span><span class="v">{{ fmt(record.population) }}</span></div>
<div class="rd-row"><span class="k">People praying</span><span class="v">{{ fmt(record.people_praying) }}</span></div> <div class="rd-row"><span class="k">{{ T('dashboard.people_praying', 'People praying') }}</span><span class="v">{{ fmt(record.people_praying) }}</span></div>
<div class="rd-row"><span class="k">People committed</span><span class="v">{{ fmt(record.people_committed) }}</span></div> <div class="rd-row"><span class="k">{{ T('dashboard.people_committed', 'People committed') }}</span><span class="v">{{ fmt(record.people_committed) }}</span></div>
<div class="rd-row"><span class="k">Adopted by churches</span><span class="v">{{ fmt(record.adopted_by_churches) }}</span></div> <div class="rd-row"><span class="k">{{ T('dashboard.adopted_by_churches', 'Adopted by churches') }}</span><span class="v">{{ fmt(record.adopted_by_churches) }}</span></div>
<div class="rd-row"><span class="k">Engagement</span><span class="v">{{ lbl(record.engagement_status) || '—' }}</span></div> <div class="rd-row"><span class="k">{{ T('dashboard.engagement', 'Engagement') }}</span><span class="v">{{ lbl(record.engagement_status) || '—' }}</span></div>
</div> </div>
<button class="rd-exp" type="button" data-detail-export @click="exportThis">Export this selection ↓</button> <button class="rd-exp" type="button" data-detail-export @click="exportThis">{{ T('dashboard.export_selection', 'Export this selection') }} ↓</button>
</template> </template>
</section> </section>
</template> </template>

View file

@ -5,6 +5,12 @@
* and is what CSV export reads later. Click delegation on the bar (defineCustomElement-safe). * and is what CSV export reads later. Click delegation on the bar (defineCustomElement-safe).
*/ */
import { inject, computed } from 'vue' import { inject, computed } from 'vue'
import { tStatic } from '@map/i18n/index.js'
// This component's chrome, read out of the catalogue. tStatic and not useI18n: these mount inside
// custom elements that install no i18n plugin, and an unguarded useI18n() throws on the way in and
// takes the subtree with it.
const T = (key, en) => tStatic(key) || en
import { useShadowStyles } from '@map/composables/useShadowStyles.js' import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { useTreeLegendLabels } from '../../composables/useTreeLegendLabels.js' import { useTreeLegendLabels } from '../../composables/useTreeLegendLabels.js'
@ -82,6 +88,10 @@ useShadowStyles(`
:title="c.facet + ': ' + c.text"> :title="c.facet + ': ' + c.text">
<span class="fc">{{ c.facet }}:</span><span class="rd-chip-val">{{ c.text }}</span> <i>✕</i> <span class="fc">{{ c.facet }}:</span><span class="rd-chip-val">{{ c.text }}</span> <i>✕</i>
</span> </span>
<span class="rd-chip rd-clear" data-chip="__all__">Clear all</span> <!-- ★ REUSES controls.reset_all rather than adding a second key. This chip clears every
filter, which is the same action that button names, and two keys for one action is how
a control ends up saying two different things in two languages. The English shifts from
"Clear all" to "Reset all" — a deliberate consequence of having one name for one thing. -->
<span class="rd-chip rd-clear" data-chip="__all__">{{ T('controls.reset_all', 'Reset all') }}</span>
</div> </div>
</template> </template>

View file

@ -1679,7 +1679,7 @@ useShadowStyles(`
<span class="rd-proj" role="button" tabindex="0" data-proj-toggle @click="toggleProjection"> <span class="rd-proj" role="button" tabindex="0" data-proj-toggle @click="toggleProjection">
{{ projection === 'globe' ? '🌐 Globe' : '▭ Flat' }} {{ projection === 'globe' ? '🌐 Globe' : '▭ Flat' }}
</span> </span>
<div class="rd-legend-map" data-map-legend aria-label="UPG count legend"> <div class="rd-legend-map" data-map-legend :aria-label="T('aria.upgCountLegend', 'UPG count legend')">
<!-- ★ COMPOSED FROM TWO EXISTING CATALOGUE ENTRIES, not from new wording. This legend title <!-- ★ COMPOSED FROM TWO EXISTING CATALOGUE ENTRIES, not from new wording. This legend title
was the last piece of English chrome here that could be translated without inventing a was the last piece of English chrome here that could be translated without inventing a
phrase in ten languages: legend.columns.upgs and dashboard.country both already ship in phrase in ten languages: legend.columns.upgs and dashboard.country both already ship in

View file

@ -297,8 +297,8 @@ useShadowStyles(`
<span class="rd-spacer" /> <span class="rd-spacer" />
<div v-if="showTopBarSearch" class="rd-search" data-search> <div v-if="showTopBarSearch" class="rd-search" data-search>
<span class="ic">⌕</span> <span class="ic">⌕</span>
<input type="text" v-model="q" placeholder="Search people groups…" data-search-input <input type="text" v-model="q" :placeholder="tStatic('grid.search') || 'Search people groups…'" data-search-input
aria-label="Search people groups" @input="onInput" @focus="onInput" @keydown="onKeydown" /> :aria-label="tStatic('aria.searchPeopleGroups') || 'Search people groups'" @input="onInput" @focus="onInput" @keydown="onKeydown" />
<div v-if="open && q.trim().length >= 2" class="rd-results" data-search-results @click="onResultsClick"> <div v-if="open && q.trim().length >= 2" class="rd-results" data-search-results @click="onResultsClick">
<template v-if="results.length"> <template v-if="results.length">
<div v-for="(r, i) in results" :key="r.slug" class="rd-res" :class="{ on: i === active }" :data-slug="r.slug"> <div v-for="(r, i) in results" :key="r.slug" class="rd-res" :class="{ on: i === active }" :data-slug="r.slug">
@ -306,7 +306,7 @@ useShadowStyles(`
<span class="sub">{{ lbl(r.country_code) }} · {{ lbl(r.religion) }}</span> <span class="sub">{{ lbl(r.country_code) }} · {{ lbl(r.religion) }}</span>
</div> </div>
</template> </template>
<div v-else class="rd-empty">No people groups match “{{ q.trim() }}”.</div> <div v-else class="rd-empty">{{ (tStatic('dashboard.no_match') || 'No people groups match “{q}”.').replace('{q}', q.trim()) }}</div>
</div> </div>
</div> </div>
<span class="rd-align" role="button" tabindex="0" data-align-btn <span class="rd-align" role="button" tabindex="0" data-align-btn

File diff suppressed because one or more lines are too long

View file

@ -43,7 +43,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. --> <!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../../big-picture.js" integrity="sha384-Q7nhazsV9dqeNW/ZGwrGXSVInr6Itsf4o98/eKRfJgqVY4jNLu6UkZSkhqtuT7g/" crossorigin="anonymous"></script> <script src="../../big-picture.js" integrity="sha384-f0QwbiCCDjTDqiQVxWY5KwZIGQ/eEcHdrSPLR8GqCZfc4r5PkfObe4GMN4lxK8ki" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -43,7 +43,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. --> <!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../../big-picture.js" integrity="sha384-Q7nhazsV9dqeNW/ZGwrGXSVInr6Itsf4o98/eKRfJgqVY4jNLu6UkZSkhqtuT7g/" crossorigin="anonymous"></script> <script src="../../big-picture.js" integrity="sha384-f0QwbiCCDjTDqiQVxWY5KwZIGQ/eEcHdrSPLR8GqCZfc4r5PkfObe4GMN4lxK8ki" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -43,7 +43,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. --> <!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../big-picture.js" integrity="sha384-Q7nhazsV9dqeNW/ZGwrGXSVInr6Itsf4o98/eKRfJgqVY4jNLu6UkZSkhqtuT7g/" crossorigin="anonymous"></script> <script src="../big-picture.js" integrity="sha384-f0QwbiCCDjTDqiQVxWY5KwZIGQ/eEcHdrSPLR8GqCZfc4r5PkfObe4GMN4lxK8ki" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -47,7 +47,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. --> <!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../../dashboard-plus-grid.js" integrity="sha384-X3RgGbiXGIZfp85WQ5H4/NyF3soIpCRnU4vsNH3+TNJSrBcUMSMn2E3QEAYBBtXN" crossorigin="anonymous"></script> <script src="../../dashboard-plus-grid.js" integrity="sha384-dmlUkR/A54stQ9oHUOIrUbiGInFwp/pn8O6fJhlPKG7gfreqLjIQFTP8xkB1FOz9" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -47,7 +47,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. --> <!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../dashboard-plus-grid.js" integrity="sha384-X3RgGbiXGIZfp85WQ5H4/NyF3soIpCRnU4vsNH3+TNJSrBcUMSMn2E3QEAYBBtXN" crossorigin="anonymous"></script> <script src="../dashboard-plus-grid.js" integrity="sha384-dmlUkR/A54stQ9oHUOIrUbiGInFwp/pn8O6fJhlPKG7gfreqLjIQFTP8xkB1FOz9" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -38,7 +38,7 @@
"bundle": "big-picture", "bundle": "big-picture",
"scriptUrl": "./big-picture/big-picture.js", "scriptUrl": "./big-picture/big-picture.js",
"output": "dx-maps/big-picture/big-picture.js", "output": "dx-maps/big-picture/big-picture.js",
"integrity": "sha384-Q7nhazsV9dqeNW/ZGwrGXSVInr6Itsf4o98/eKRfJgqVY4jNLu6UkZSkhqtuT7g/", "integrity": "sha384-f0QwbiCCDjTDqiQVxWY5KwZIGQ/eEcHdrSPLR8GqCZfc4r5PkfObe4GMN4lxK8ki",
"elements": [ "elements": [
"bp-big-picture", "bp-big-picture",
"bp-research-header", "bp-research-header",
@ -60,7 +60,7 @@
"scriptUrl": "./big-picture/big-picture.js", "scriptUrl": "./big-picture/big-picture.js",
"embed": { "embed": {
"iframe": "<iframe src=\"./big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>", "iframe": "<iframe src=\"./big-picture/big-picture/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=\"./big-picture/big-picture.js\" integrity=\"sha384-Q7nhazsV9dqeNW/ZGwrGXSVInr6Itsf4o98/eKRfJgqVY4jNLu6UkZSkhqtuT7g/\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>" "script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./big-picture/big-picture.js\" integrity=\"sha384-f0QwbiCCDjTDqiQVxWY5KwZIGQ/eEcHdrSPLR8GqCZfc4r5PkfObe4GMN4lxK8ki\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
}, },
"instances": [ "instances": [
{ {
@ -214,7 +214,7 @@
"bundle": "dashboard-plus-grid", "bundle": "dashboard-plus-grid",
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js", "scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js", "output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
"integrity": "sha384-X3RgGbiXGIZfp85WQ5H4/NyF3soIpCRnU4vsNH3+TNJSrBcUMSMn2E3QEAYBBtXN", "integrity": "sha384-dmlUkR/A54stQ9oHUOIrUbiGInFwp/pn8O6fJhlPKG7gfreqLjIQFTP8xkB1FOz9",
"elements": [ "elements": [
"dashboard-plus-grid" "dashboard-plus-grid"
], ],
@ -234,7 +234,7 @@
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js", "scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"embed": { "embed": {
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>", "iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-X3RgGbiXGIZfp85WQ5H4/NyF3soIpCRnU4vsNH3+TNJSrBcUMSMn2E3QEAYBBtXN\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>" "script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-dmlUkR/A54stQ9oHUOIrUbiGInFwp/pn8O6fJhlPKG7gfreqLjIQFTP8xkB1FOz9\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
}, },
"instances": [ "instances": [
{ {
@ -353,7 +353,7 @@
"bundle": "doxa-research-map", "bundle": "doxa-research-map",
"scriptUrl": "./doxa-research-map/doxa-research-map.js", "scriptUrl": "./doxa-research-map/doxa-research-map.js",
"output": "dx-maps/doxa-research-map/doxa-research-map.js", "output": "dx-maps/doxa-research-map/doxa-research-map.js",
"integrity": "sha384-rScvP3yQFMFvEQ6SCPE/w1biJ3gJrzsWExhpaJ/L9O9e+9m5OUxUHYKvD45Cy/Z+", "integrity": "sha384-IE7GadcBdYFEn58dO/atf9zTDaFcWkpRWY242m9noDv/Hqh2OiAEZvib3AyvpUbA",
"elements": [ "elements": [
"doxa-research-map" "doxa-research-map"
], ],
@ -369,7 +369,7 @@
"scriptUrl": "./doxa-research-map/doxa-research-map.js", "scriptUrl": "./doxa-research-map/doxa-research-map.js",
"embed": { "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>", "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": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-rScvP3yQFMFvEQ6SCPE/w1biJ3gJrzsWExhpaJ/L9O9e+9m5OUxUHYKvD45Cy/Z+\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>" "script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-IE7GadcBdYFEn58dO/atf9zTDaFcWkpRWY242m9noDv/Hqh2OiAEZvib3AyvpUbA\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
}, },
"instances": [ "instances": [
{ {
@ -944,7 +944,7 @@
"bundle": "research-dashboard", "bundle": "research-dashboard",
"scriptUrl": "./research-dashboard/research-dashboard.js", "scriptUrl": "./research-dashboard/research-dashboard.js",
"output": "dx-maps/research-dashboard/research-dashboard.js", "output": "dx-maps/research-dashboard/research-dashboard.js",
"integrity": "sha384-iJTJdnn7mwnqLrA6sXQ5QoTx91XkqclHfElKkd7crsKd9WsNjy1or5vnOqWU5LQ+", "integrity": "sha384-ZRsuN2LD7TXf/n/k1P+ivab365wtEfj7NLV2ImVfEW8fmCN++ui1VTXx+qBiulyG",
"elements": [ "elements": [
"research-dashboard-map" "research-dashboard-map"
], ],
@ -960,7 +960,7 @@
"scriptUrl": "./research-dashboard/research-dashboard.js", "scriptUrl": "./research-dashboard/research-dashboard.js",
"embed": { "embed": {
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>", "iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-iJTJdnn7mwnqLrA6sXQ5QoTx91XkqclHfElKkd7crsKd9WsNjy1or5vnOqWU5LQ+\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>" "script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-ZRsuN2LD7TXf/n/k1P+ivab365wtEfj7NLV2ImVfEW8fmCN++ui1VTXx+qBiulyG\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
}, },
"bundle": "research-dashboard" "bundle": "research-dashboard"
} }

File diff suppressed because one or more lines are too long

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. --> <!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../doxa-research-map.js" integrity="sha384-rScvP3yQFMFvEQ6SCPE/w1biJ3gJrzsWExhpaJ/L9O9e+9m5OUxUHYKvD45Cy/Z+" crossorigin="anonymous"></script> <script src="../doxa-research-map.js" integrity="sha384-IE7GadcBdYFEn58dO/atf9zTDaFcWkpRWY242m9noDv/Hqh2OiAEZvib3AyvpUbA" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -33,7 +33,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. --> <!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../../doxa-research-map.js" integrity="sha384-rScvP3yQFMFvEQ6SCPE/w1biJ3gJrzsWExhpaJ/L9O9e+9m5OUxUHYKvD45Cy/Z+" crossorigin="anonymous"></script> <script src="../../doxa-research-map.js" integrity="sha384-IE7GadcBdYFEn58dO/atf9zTDaFcWkpRWY242m9noDv/Hqh2OiAEZvib3AyvpUbA" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -38,7 +38,7 @@
"bundle": "big-picture", "bundle": "big-picture",
"scriptUrl": "./big-picture/big-picture.js", "scriptUrl": "./big-picture/big-picture.js",
"output": "dx-maps/big-picture/big-picture.js", "output": "dx-maps/big-picture/big-picture.js",
"integrity": "sha384-Q7nhazsV9dqeNW/ZGwrGXSVInr6Itsf4o98/eKRfJgqVY4jNLu6UkZSkhqtuT7g/", "integrity": "sha384-f0QwbiCCDjTDqiQVxWY5KwZIGQ/eEcHdrSPLR8GqCZfc4r5PkfObe4GMN4lxK8ki",
"elements": [ "elements": [
"bp-big-picture", "bp-big-picture",
"bp-research-header", "bp-research-header",
@ -60,7 +60,7 @@
"scriptUrl": "./big-picture/big-picture.js", "scriptUrl": "./big-picture/big-picture.js",
"embed": { "embed": {
"iframe": "<iframe src=\"./big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>", "iframe": "<iframe src=\"./big-picture/big-picture/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=\"./big-picture/big-picture.js\" integrity=\"sha384-Q7nhazsV9dqeNW/ZGwrGXSVInr6Itsf4o98/eKRfJgqVY4jNLu6UkZSkhqtuT7g/\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>" "script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./big-picture/big-picture.js\" integrity=\"sha384-f0QwbiCCDjTDqiQVxWY5KwZIGQ/eEcHdrSPLR8GqCZfc4r5PkfObe4GMN4lxK8ki\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
}, },
"instances": [ "instances": [
{ {
@ -214,7 +214,7 @@
"bundle": "dashboard-plus-grid", "bundle": "dashboard-plus-grid",
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js", "scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js", "output": "dx-maps/dashboard-plus-grid/dashboard-plus-grid.js",
"integrity": "sha384-X3RgGbiXGIZfp85WQ5H4/NyF3soIpCRnU4vsNH3+TNJSrBcUMSMn2E3QEAYBBtXN", "integrity": "sha384-dmlUkR/A54stQ9oHUOIrUbiGInFwp/pn8O6fJhlPKG7gfreqLjIQFTP8xkB1FOz9",
"elements": [ "elements": [
"dashboard-plus-grid" "dashboard-plus-grid"
], ],
@ -234,7 +234,7 @@
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js", "scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"embed": { "embed": {
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>", "iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-X3RgGbiXGIZfp85WQ5H4/NyF3soIpCRnU4vsNH3+TNJSrBcUMSMn2E3QEAYBBtXN\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>" "script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-dmlUkR/A54stQ9oHUOIrUbiGInFwp/pn8O6fJhlPKG7gfreqLjIQFTP8xkB1FOz9\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
}, },
"instances": [ "instances": [
{ {
@ -353,7 +353,7 @@
"bundle": "doxa-research-map", "bundle": "doxa-research-map",
"scriptUrl": "./doxa-research-map/doxa-research-map.js", "scriptUrl": "./doxa-research-map/doxa-research-map.js",
"output": "dx-maps/doxa-research-map/doxa-research-map.js", "output": "dx-maps/doxa-research-map/doxa-research-map.js",
"integrity": "sha384-rScvP3yQFMFvEQ6SCPE/w1biJ3gJrzsWExhpaJ/L9O9e+9m5OUxUHYKvD45Cy/Z+", "integrity": "sha384-IE7GadcBdYFEn58dO/atf9zTDaFcWkpRWY242m9noDv/Hqh2OiAEZvib3AyvpUbA",
"elements": [ "elements": [
"doxa-research-map" "doxa-research-map"
], ],
@ -369,7 +369,7 @@
"scriptUrl": "./doxa-research-map/doxa-research-map.js", "scriptUrl": "./doxa-research-map/doxa-research-map.js",
"embed": { "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>", "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": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-rScvP3yQFMFvEQ6SCPE/w1biJ3gJrzsWExhpaJ/L9O9e+9m5OUxUHYKvD45Cy/Z+\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>" "script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-IE7GadcBdYFEn58dO/atf9zTDaFcWkpRWY242m9noDv/Hqh2OiAEZvib3AyvpUbA\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
}, },
"instances": [ "instances": [
{ {
@ -944,7 +944,7 @@
"bundle": "research-dashboard", "bundle": "research-dashboard",
"scriptUrl": "./research-dashboard/research-dashboard.js", "scriptUrl": "./research-dashboard/research-dashboard.js",
"output": "dx-maps/research-dashboard/research-dashboard.js", "output": "dx-maps/research-dashboard/research-dashboard.js",
"integrity": "sha384-iJTJdnn7mwnqLrA6sXQ5QoTx91XkqclHfElKkd7crsKd9WsNjy1or5vnOqWU5LQ+", "integrity": "sha384-ZRsuN2LD7TXf/n/k1P+ivab365wtEfj7NLV2ImVfEW8fmCN++ui1VTXx+qBiulyG",
"elements": [ "elements": [
"research-dashboard-map" "research-dashboard-map"
], ],
@ -960,7 +960,7 @@
"scriptUrl": "./research-dashboard/research-dashboard.js", "scriptUrl": "./research-dashboard/research-dashboard.js",
"embed": { "embed": {
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>", "iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-iJTJdnn7mwnqLrA6sXQ5QoTx91XkqclHfElKkd7crsKd9WsNjy1or5vnOqWU5LQ+\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>" "script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\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<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-ZRsuN2LD7TXf/n/k1P+ivab365wtEfj7NLV2ImVfEW8fmCN++ui1VTXx+qBiulyG\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
}, },
"bundle": "research-dashboard" "bundle": "research-dashboard"
} }

File diff suppressed because one or more lines are too long

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script> <script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <research-dashboard-map> custom element. --> <!-- Bundle IIFE — registers the <research-dashboard-map> custom element. -->
<script src="../research-dashboard.js" integrity="sha384-iJTJdnn7mwnqLrA6sXQ5QoTx91XkqclHfElKkd7crsKd9WsNjy1or5vnOqWU5LQ+" crossorigin="anonymous"></script> <script src="../research-dashboard.js" integrity="sha384-ZRsuN2LD7TXf/n/k1P+ivab365wtEfj7NLV2ImVfEW8fmCN++ui1VTXx+qBiulyG" crossorigin="anonymous"></script>
<script> <script>
// Mapbox token resolution — the A3 ladder, in precedence order: // Mapbox token resolution — the A3 ladder, in precedence order: